Skip to content

Comments

feat: Implement Private Send Functionality#6

Open
toruguera wants to merge 6 commits intomainfrom
feat/send-integration-provider
Open

feat: Implement Private Send Functionality#6
toruguera wants to merge 6 commits intomainfrom
feat/send-integration-provider

Conversation

@toruguera
Copy link
Contributor

This PR implements the private send flow, allowing users to send assets to MLXDR receiving addresses generated by the private receive flow. The implementation follows the same patterns as the deposit/receive flows and aligns with the sandbox reference implementation, including a dedicated confirmation screen with a preview of CREATE and SPEND operations before submission.

Overview

Users can now send funds from a private channel to a MLXDR receiving address by specifying:

  • Recipient MLXDR address (one or multiple CREATE operations)
  • Send amount
  • Entropy level (privacy level) that influences UTXO selection and change splitting

The system will:

  • Parse the recipient MLXDR into CREATE operations (receiver UTXOs)
  • Select UTXOs to cover amount + fees using UtxoBasedStellarAccount
  • Compute change and randomize its distribution across change UTXOs
  • Build and sign SPEND + CREATE operations
  • Allow the user to review all operations in the UI
  • Submit the final bundle to the privacy provider

Features

  • Send Form (SendPage)

    • Displays account, channel, and asset information
    • MLXDR receiving address textarea with basic validation
    • Amount input with validation
    • Privacy level (entropy) selection: LOW, MEDIUM, HIGH, V_HIGH
    • Estimated fee and total amount display
    • “Private Transfer” informational banner
    • Provider connection/session validation
    • “Review Transfer” button that prepares operations (but does not submit)
  • Send Confirmation (SendConfirmationPage)

    • Total amount (transfer + network fee)
    • Recipient MLXDR (truncated, with copy functionality)
    • Transaction details: privacy level, account, channel
    • Operations preview:
      • CREATE operations (receiver + change) with address and amount
      • SPEND operations (UTXOs to be spent) with number of conditions
      • Expand/collapse control and operation counters
    • Warning “Verify Before Proceeding”
    • “Execute Transaction” button that submits prepared operations
    • “Go Back” button to return to the form
  • Home Integration

    • Send button in private view navigates to the send flow

Technical Changes

Backend

  • Handler: src/background/handlers/private/send.ts

    • Validates wallet unlocked, channel, provider, and session
    • Parses MLXDR receiving address into CREATE operations
    • Derives/loads UTXOs via UtxoBasedStellarAccount
    • Selects UTXOs for transfer using selectUTXOsForTransfer with a random strategy and retry logic
    • Maps entropy levels to a target number of slots (receiver CREATEs + SPENDs + change CREATEs)
    • Builds CREATE operations for receiver and change using partitionAmountRandom to randomize change distribution
    • Builds and signs SPEND operations with conditions referencing all CREATE operations
    • Converts all operations to MLXDR for bundle submission
    • Submits bundle to the privacy provider via PrivacyProviderClient
    • Handles authentication errors and clears provider session when needed
    • Provides a reusable prepareSendOperations function for the prepare/preview flow
  • “Prepare” handler: handlePrepareSend

    • Uses prepareSendOperations to:
      • Build and sign all CREATE and SPEND operations
      • Return a serializable summary:
        • Receiver + change CREATE operations (base64 public keys, amounts, type)
        • SPEND operations (UTXO public keys, conditions count)
        • operationsMLXDR array ready for submission
        • Totals and counters: total spend, change, receiver amount, number of CREATEs/SPENDs
    • Does not submit to the provider
  • Types: src/background/handlers/private/send.types.ts

    • SendRequest: network, channelId, providerId, accountId, receiverOperationsMLXDR, amount, entropyLevel, optional preparedOperationsMLXDR
    • SendResponse: ok, id, hash, error
    • PrepareSendRequest: same shape as SendRequest (without prepared operations)
    • PrepareSendResponse: ok, createOperations, spendOperations, operationsMLXDR, totals/counters, error
  • Message system: src/background/messages.ts

    • Added MessageType.Send = "SEND"
    • Added MessageType.PrepareSend = "PREPARE_SEND"
    • Added mappings for SendRequest / SendResponse and PrepareSendRequest / PrepareSendResponse
  • Background router: src/background/handler.ts

    • Registered handleSend for MessageType.Send
    • Registered handlePrepareSend for MessageType.PrepareSend

Frontend

  • API services: src/popup/api/send.ts

    • send(params: SendRequest): Promise<SendResponse>
      • Wrapper for MessageType.Send
    • prepareSend(params: PrepareSendRequest): Promise<PrepareSendResponse>
      • Wrapper for MessageType.PrepareSend
  • State management: src/popup/hooks/state.tsx

    • Routes:
      • send
      • send-confirmation
    • State:
      • sendFormData?: { channelId, providerId, receiverOperationsMLXDR, amount, entropyLevel }
      • sendResult?: { createOperations, spendOperations, operationsMLXDR, totalSpendAmount, changeAmount, receiverAmount, numSpends, numCreates }
    • Actions:
      • goSend(channelId?, providerId?)
      • goSendConfirmation()
      • setSendFormData(data)
      • setSendResult(data)
      • clearSendData()
  • Pages:

    • src/popup/pages/send-page.tsx

      • Loads private channels and validates provider session
      • Manages MLXDR, amount, and entropy level form state
      • Calls prepareSend() on “Review Transfer”:
        • Saves sendFormData in state
        • Calls backend PrepareSend
        • Saves sendResult (operations and summary) in state
        • Navigates to send-confirmation
      • Disables inputs and shows loading state while preparing
    • src/popup/pages/send-confirmation-page.tsx

      • Loads private channels for channel metadata
      • Uses sendFormData and sendResult from state
      • Displays:
        • Total amount, transfer amount, network fee
        • Truncated MLXDR with copy functionality
        • Privacy level, account, channel
        • Operations section:
          • Expandable “Transaction Operations” card
          • Summary: <numSpends> SPEND operations, <numCreates> CREATE operations
          • CREATE list:
            • Receiver vs Change labels
            • Truncated UTXO public key
            • Amount in XLM
          • SPEND list:
            • Truncated UTXO public key
            • Number of conditions (CREATE operations linked to that SPEND)
      • On “Execute Transaction”:
        • Calls send() with preparedOperationsMLXDR from sendResult
        • On success, clears send state and navigates home
  • Router: src/popup/app.tsx

    • Added route handling for send and send-confirmation (if not already present)
  • Home integration:

    • src/popup/templates/home-template.tsx
      • Added onStartSend callback prop for the private Send button
    • src/popup/pages/home-page.tsx
      • Wires onStartSend={(channelId, providerId) => actions.goSend(channelId, providerId)}

Implementation Details

  • Entropy mapping:

    • LOW: 1 slot
    • MEDIUM: 5 slots
    • HIGH: 10 slots
    • V_HIGH: 15 slots
      Slots are allocated between:
    • Receiver CREATE operations (from MLXDR)
    • SPEND operations (selected UTXOs)
    • Change CREATE operations (when slots are available and change > 0)
  • UTXO selection:

    • Uses UtxoBasedStellarAccount.selectUTXOsForTransfer(totalToSpend, "random")
    • Retries up to 5 times, keeping the selection with the smallest number of UTXOs
    • Prefers selections with ≤ 10 UTXOs
    • Ensures a minimum pool of free UTXOs before selection
  • Change distribution:

    • Uses partitionAmountRandom to randomly split change across reserved change UTXOs
    • Ensures a minimum per-part amount (1 stroop)
  • Expiration handling:

    • Fetches latest ledger via RPC
    • Sets expiration to latestLedger.sequence + 1000 for SPEND signing
  • Error handling:

    • Validates:
      • Wallet locked state
      • Channel and provider existence
      • Provider session and token
      • MLXDR presence and parseability
      • Only CREATE operations in receiving MLXDR
      • Positive and finite amount
      • Sufficient balance/UTXOs for the transfer
    • Returns structured error codes (e.g., LOCKED, NOT_FOUND, INVALID_MLXDR, INSUFFICIENT_BALANCE, INVALID_SESSION, AUTH_FAILED, SEND_FAILED)
    • Clears provider session on PrivacyProviderAuthError

Files Changed

New / Significantly Updated Backend Files:

  • src/background/handlers/private/send.types.ts
  • src/background/handlers/private/send.ts

Backend Integration:

  • src/background/messages.ts — Added Send and PrepareSend message types and mappings
  • src/background/handler.ts — Registered handleSend and handlePrepareSend

New / Updated Frontend Files:

  • src/popup/api/send.ts
  • src/popup/pages/send-page.tsx
  • src/popup/pages/send-confirmation-page.tsx

State & Routing:

  • src/popup/hooks/state.tsx — Added send routes and state (sendFormData, sendResult)
  • src/popup/app.tsx — Added send and send-confirmation route handling

Home Integration:

  • src/popup/templates/home-template.tsx — Added send action prop
  • src/popup/pages/home-page.tsx — Wires send button to goSend

Testing Notes

  • Form validation

    • Ensure MLXDR is present and passes basic format checks
    • Validate amount > 0
    • Validate provider session is active
  • Prepare flow

    • Test prepareSend with valid MLXDR and various entropy levels
    • Confirm that:
      • createOperations and spendOperations are populated
      • operationsMLXDR length matches numCreates + numSpends
      • Totals (totalSpendAmount, changeAmount, receiverAmount) make sense
  • Confirmation UI

    • Verify all summary fields (total, transfer, fee, privacy level, account, channel)
    • Expand operations:
      • Check counts for SPEND and CREATE
      • Confirm receiver vs change classification
      • Verify amounts and addresses are rendered correctly
  • Execution

    • From a prepared state, click “Execute Transaction”
    • Confirm:
      • Only preparedOperationsMLXDR is submitted (no re-preparation)
      • Success clears send state and returns to home
      • Authentication errors clear provider session and show error
  • Navigation

    • Home → Send → Confirmation → Home
    • Back from confirmation returns to send form with preserved data

Future Enhancements

  • Include more detailed operation breakdown (e.g., per-operation fees if available)
  • Surface additional privacy metrics (e.g., effective anonymity set size)
  • Add advanced options for entropy/UTXO selection strategies
  • Provide a raw MLXDR bundle preview for power users
  • Add send transaction history and status tracking

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements the private send functionality for a cryptocurrency wallet, enabling users to send assets privately using MLXDR receiving addresses. The implementation includes deposit, receive, and send flows with dedicated confirmation screens, UTXO selection with privacy level controls, and integration with a privacy provider.

Changes:

  • Implemented backend handlers for deposit, receive, and private send operations with UTXO selection, change distribution, and bundle submission to privacy provider
  • Created frontend pages and templates for deposit/receive/send flows with confirmation screens showing operation previews
  • Added routing, state management, and API services to support the new flows with home screen integration

Reviewed changes

Copilot reviewed 28 out of 29 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
src/background/handlers/private/send.ts New send handler with UTXO selection, change partitioning, and bundle preparation/submission
src/background/handlers/private/send.types.ts Type definitions for send and prepare-send requests/responses
src/background/handlers/private/receive.ts Receive handler generating MLXDR addresses from reserved UTXOs
src/background/handlers/private/receive.types.ts Type definitions for receive operations
src/background/handlers/private/deposit.ts Deposit handler with entropy-based UTXO creation
src/background/handlers/private/deposit.types.ts Type definitions for deposit operations
src/background/services/privacy-provider-client.ts Enhanced with token validation, auth error handling, and bundle submission
src/background/utils/random-partition.ts New utility for cryptographically secure random amount partitioning
src/background/messages.ts Added message types for deposit, receive, send, and prepare-send
src/background/handler.ts Registered new handlers in the message routing system
src/popup/pages/send-page.tsx Send form with MLXDR input, amount, and privacy level selection
src/popup/pages/send-confirmation-page.tsx Send confirmation with operation preview (CREATE/SPEND)
src/popup/pages/receive-page.tsx Receive form for generating MLXDR addresses
src/popup/pages/receive-confirmation-page.tsx Display generated MLXDR with UTXO breakdown
src/popup/pages/deposit-page.tsx Deposit form with method and entropy selection
src/popup/pages/deposit-review-page.tsx Deposit confirmation with operation summary
src/popup/templates/*.tsx Reusable templates for forms and confirmation screens
src/popup/api/*.ts API service wrappers for new backend handlers
src/popup/hooks/state.tsx State management for send/receive/deposit flows and routing
src/popup/app.tsx Route definitions for new pages
src/popup/templates/home-template.tsx Home UI with Send/Receive/Ramp action buttons
src/popup/pages/home-page.tsx Wired action handlers to state navigation
deno.json Removed empty lines in scripts section
deno.lock Dependency version updates

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +613 to +620
const prepared = await prepareSendOperations({
network,
channelId,
providerId,
accountId,
receiverOperationsMLXDR: receiverOperationsMLXDR!,
amount: amount!,
entropyLevel: entropyLevel!,
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using non-null assertion operators (!) here is unsafe. If receiverOperationsMLXDR, amount, or entropyLevel are undefined at this point, the function should return an error response rather than potentially causing runtime errors. Add proper validation before calling prepareSendOperations or handle the case where these values might be missing.

Suggested change
const prepared = await prepareSendOperations({
network,
channelId,
providerId,
accountId,
receiverOperationsMLXDR: receiverOperationsMLXDR!,
amount: amount!,
entropyLevel: entropyLevel!,
const missingFields: string[] = [];
if (receiverOperationsMLXDR == null) {
missingFields.push("receiverOperationsMLXDR");
}
if (amount == null) {
missingFields.push("amount");
}
if (entropyLevel == null) {
missingFields.push("entropyLevel");
}
if (missingFields.length > 0) {
return {
type: MessageType.Send,
ok: false,
error: {
code: "INVALID_REQUEST",
message: `Missing required fields: ${missingFields.join(", ")}`,
},
};
}
const prepared = await prepareSendOperations({
network,
channelId,
providerId,
accountId,
receiverOperationsMLXDR,
amount,
entropyLevel,

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +73
useMemo(() => {
if (!formData) return;
getPrivateChannels({ network })
.then((res) => {
if (res.ok) {
setPrivateChannels({
channels: res.channels,
selectedChannelId: res.selectedChannelId,
});
}
})
.catch((err) => {
console.error("Failed to load private channels", err);
});
}, [network, formData]);
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useMemo is being used incorrectly here for side effects. useMemo should only be used for memoizing computed values, not for triggering side effects like API calls. This should be useEffect instead. The current code may cause the API call to be executed multiple times unpredictably.

Copilot uses AI. Check for mistakes.
Comment on lines +723 to +724
disabled={!canStartDeposit}
onClick={action.onClick}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three action buttons (Receive, Send, Ramp) use the same canStartDeposit condition to determine if they should be disabled. However, this variable is specifically named for deposits. The Send button should potentially have its own condition check or the variable should be renamed to something more generic like canPerformPrivateAction to reflect that it gates all three operations.

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +24
function getUtxoCountFromEntropyLevel(
level: "LOW" | "MEDIUM" | "HIGH" | "V_HIGH",
): number {
switch (level) {
case "LOW":
return 1;
case "MEDIUM":
return 5;
case "HIGH":
return 10;
case "V_HIGH":
return 20;
default:
return 5;
}
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getUtxoCountFromEntropyLevel function is duplicated in both deposit-review-page.tsx and deposit.ts. This creates a maintenance issue where changes to entropy mapping must be made in multiple places. Extract this to a shared utility module.

Copilot uses AI. Check for mistakes.
Comment on lines +363 to +364
console.log("operationsMLXDR", operationsMLXDR);

Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log statement left in production code. This should either be removed or replaced with a proper logging mechanism if the MLXDR operations need to be logged.

Suggested change
console.log("operationsMLXDR", operationsMLXDR);

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +62
useMemo(() => {
if (!formData) return;
getPrivateChannels({ network })
.then((res) => {
if (res.ok) {
setPrivateChannels({
channels: res.channels,
selectedChannelId: res.selectedChannelId,
});
}
})
.catch((err) => {
console.error("Failed to load private channels", err);
});
}, [network, formData]);
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useMemo is being used incorrectly here for side effects. useMemo should only be used for memoizing computed values, not for triggering side effects like API calls. This should be useEffect instead. The current code may cause the API call to be executed multiple times unpredictably.

Copilot uses AI. Check for mistakes.
sumWeights += weight;
}

// Distribuir o remaining proporcionalmente aos pesos
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment is in Portuguese ("Distribuir o remaining proporcionalmente aos pesos" = "Distribute the remaining proportionally to the weights"). Should be in English for consistency with the rest of the codebase.

Suggested change
// Distribuir o remaining proporcionalmente aos pesos
// Distribute the remaining amount proportionally to the weights

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +30
function getFeeForEntropyLevel(level: EntropyLevel): number {
switch (level) {
case "LOW":
return 0.1;
case "MEDIUM":
return 0.25;
case "HIGH":
return 0.5;
case "V_HIGH":
return 1.0;
default:
return 0.25;
}
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getFeeForEntropyLevel function is duplicated across multiple files with slightly different return values. In send-page.tsx it returns LOW: 0.1, MEDIUM: 0.25, HIGH: 0.5, V_HIGH: 1.0, while in send-confirmation-page.tsx it has the same values, but in deposit.ts it returns LOW: 0.05, MEDIUM: 0.25, HIGH: 0.5, V_HIGH: 0.75. This function should be extracted to a shared utility file to ensure consistency and avoid maintenance issues.

Copilot uses AI. Check for mistakes.
Comment on lines +397 to +414
<CardFooter className="px-0 space-y-2">
<Button
className="w-full"
disabled={busy}
onClick={handleExecute}
loading={busy}
>
Execute Transaction
</Button>
<Button
variant="outline"
className="w-full"
disabled={busy}
onClick={() => actions.goSend()}
>
Go Back
</Button>
</CardFooter>
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CardFooter component is being used outside of a Card component. According to the typical card component pattern, CardFooter should be a child of Card. Either wrap this in a Card component or use a plain div if the card styling is not needed.

Copilot uses AI. Check for mistakes.
Comment on lines +427 to +436
<CardFooter className="px-0">
<Button
className="w-full"
disabled={!canSubmit || busy}
onClick={handleSubmit}
loading={busy}
>
Review Transfer
</Button>
</CardFooter>
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CardFooter component is being used outside of a Card component. According to the typical card component pattern, CardFooter should be a child of Card. Either wrap this in a Card component or use a plain div if the card styling is not needed.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@fazzatti fazzatti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few points and the bot also caught some interesting things that might be worth checking.

import { partitionAmountRandom } from "@/background/utils/random-partition.ts";
import { Buffer } from "node:buffer";

async function rpcGetLatestLedger(params: { rpcUrl: string }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of rebuilding this function from scratch, try using the RPC client from the stellar sdk. We have the client at the NETWORK_RPC_SERVER so you can just use it like this:

const currentLedger = (await NETWORK_RPC_SERVER.getLatestLedger()).sequence;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh wait, I got the variable from the provider platform, in case we don't have it ready here, you can initialize one importing Server from @stellar/stellar-sdk/rpc. Just initialize it with the same rpcUrl and you should be good

import { Buffer } from "node:buffer";
import { bytesToBase64 } from "@/common/utils/bytes-to-base64.ts";

async function rpcGetLatestLedger(params: { rpcUrl: string }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicated function, we can use a single one or the Server client as in the other comment

Comment on lines +112 to +126
// Distribute the leftover randomly among parts
const remainderCount = Number(remainderExtra);
const indices = new Set<number>();

// Generate unique random indices to receive +1
while (indices.size < remainderCount && indices.size < partsCount) {
const idx = Number(
randomIntInRangeBigInt(0n, BigInt(partsCount - 1)),
);
indices.add(idx);
}

// Add +1 to the selected indices
for (const idx of indices) {
amounts[idx] += 1n;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds like an issue indeed, if more leftover is unassigned, this means more money being left as 'tip'or 'fee' on the table.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants