Skip to content

Comments

feat: Implement Private Withdraw Functionality#7

Open
toruguera wants to merge 8 commits intomainfrom
feat/withdraw-integration-provider
Open

feat: Implement Private Withdraw Functionality#7
toruguera wants to merge 8 commits intomainfrom
feat/withdraw-integration-provider

Conversation

@toruguera
Copy link
Contributor

This PR implements the private withdraw flow, allowing users to withdraw assets
from a Privacy Channel directly to an external Stellar G account address. The
implementation follows the same patterns as the send flow and aligns with the
sandbox reference implementation, including a dedicated confirmation screen with
a preview of WITHDRAW, CREATE (change), and SPEND operations before submission.

Overview

Users can now withdraw funds from a private channel to an external Stellar G
account address by specifying:

  • Destination G account address (Stellar public key)
  • Withdraw amount
  • Entropy level (privacy level) that influences UTXO selection and change
    splitting

The system will:

  • Validate the destination G account address
  • Select UTXOs from the Privacy Channel to cover amount + fees using
    UtxoBasedStellarAccount
  • Compute change and randomize its distribution across change UTXOs
  • Build and sign WITHDRAW, CREATE (for change), and SPEND operations
  • Add withdraw conditions to SPEND operations to ensure funds are sent to the
    specified G account
  • Allow the user to review all operations in the UI
  • Submit the final bundle to the privacy provider

Features

  • Withdraw Form (WithdrawPage)

    • Displays account, channel, and asset information
    • Destination address input (G account) with validation
    • Amount input with validation
    • Privacy level (entropy) selection: LOW, MEDIUM, HIGH, V_HIGH
    • Estimated fee and total amount display
    • "Withdraw to G Account" informational banner
    • Provider connection/session validation
    • "Review Withdraw" button that prepares operations (but does not submit)
  • Withdraw Confirmation (WithdrawConfirmationPage)

    • Total amount (withdraw + network fee)
    • Destination address (truncated, with copy functionality)
    • Transaction details: privacy level, account, channel
    • Operations preview:
      • WITHDRAW operation with destination address and amount
      • CREATE operations (change only) 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 Withdraw" button that submits prepared operations
    • "Go Back" button to return to the form
  • Home Integration

    • Withdraw button in private view navigates to the withdraw flow

Technical Changes

Backend

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

    • Validates wallet unlocked, channel, provider, and session
    • Validates destination G account address using StrKey.isValidEd25519PublicKey
    • Derives/loads UTXOs via UtxoBasedStellarAccount
    • Selects UTXOs from Privacy Channel for transfer using
      selectUTXOsForTransfer with a random strategy and retry logic
    • Maps entropy levels to a target number of slots (WITHDRAW + SPENDs + change
      CREATEs)
    • Creates WITHDRAW operation for the destination G account
    • Builds CREATE operations for change using partitionAmountRandom to
      randomize change distribution
    • Builds and signs SPEND operations with withdraw conditions referencing the
      WITHDRAW operation and change 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 prepareWithdrawOperations function for the
      prepare/preview flow
  • "Prepare" handler: handlePrepareWithdraw

    • Uses prepareWithdrawOperations to:
      • Build and sign all WITHDRAW, CREATE (change), and SPEND operations
      • Return a serializable summary:
        • WITHDRAW operation (destination address, amount)
        • 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, withdraw amount, number of
          CREATEs/SPENDs
    • Does not submit to the provider
  • Types: src/background/handlers/private/withdraw.types.ts

    • WithdrawRequest: network, channelId, providerId, accountId,
      destinationAddress, amount, entropyLevel, optional
      preparedOperationsMLXDR
    • WithdrawResponse: ok, id, hash, error
    • PrepareWithdrawRequest: same shape as WithdrawRequest (without prepared
      operations)
    • PrepareWithdrawResponse: ok, withdrawOperation, changeOperations,
      spendOperations, operationsMLXDR, totals/counters, error
  • Message system: src/background/messages.ts

    • Added MessageType.Withdraw = "WITHDRAW"
    • Added MessageType.PrepareWithdraw = "PREPARE_WITHDRAW"
    • Added mappings for WithdrawRequest / WithdrawResponse and
      PrepareWithdrawRequest / PrepareWithdrawResponse
  • Background router: src/background/handler.ts

    • Registered handleWithdraw for MessageType.Withdraw
    • Registered handlePrepareWithdraw for MessageType.PrepareWithdraw

Frontend

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

    • withdraw(params: WithdrawRequest): Promise<WithdrawResponse>
      • Wrapper for MessageType.Withdraw
    • prepareWithdraw(params: PrepareWithdrawRequest): Promise<PrepareWithdrawResponse>
      • Wrapper for MessageType.PrepareWithdraw
  • State management: src/popup/hooks/state.tsx

    • Routes:
      • withdraw
      • withdraw-confirmation
    • State:
      • withdrawFormData?: { channelId, providerId, destinationAddress, amount, entropyLevel }
      • withdrawResult?: { withdrawOperation, changeOperations, spendOperations, operationsMLXDR, totalSpendAmount, changeAmount, withdrawAmount, numSpends, numCreates }
    • Actions:
      • goWithdraw(channelId?, providerId?)
      • goWithdrawConfirmation()
      • setWithdrawFormData(data)
      • setWithdrawResult(data)
      • clearWithdrawData()
  • Pages:

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

      • Loads private channels and validates provider session
      • Manages destination address, amount, and entropy level form state
      • Validates G account address format using StrKey.isValidEd25519PublicKey
      • Calls prepareWithdraw() on "Review Withdraw":
        • Saves withdrawFormData in state
        • Calls backend PrepareWithdraw
        • Saves withdrawResult (operations and summary) in state
        • Navigates to withdraw-confirmation
      • Disables inputs and shows loading state while preparing
    • src/popup/pages/withdraw-confirmation-page.tsx

      • Loads private channels for channel metadata
      • Uses withdrawFormData and withdrawResult from state
      • Displays:
        • Total amount, withdraw amount, network fee
        • Truncated destination address with copy functionality
        • Privacy level, account, channel
        • Operations section:
          • Expandable "Transaction Operations" card
          • Summary:
            1 WITHDRAW operation, <numSpends> SPEND operations, <numCreates> CREATE operations
          • WITHDRAW operation:
            • Destination address (truncated)
            • Amount in XLM
          • CREATE list (change only):
            • Change label
            • Truncated UTXO public key
            • Amount in XLM
          • SPEND list:
            • Truncated UTXO public key
            • Number of conditions (WITHDRAW + change CREATEs linked to that SPEND)
      • On "Execute Withdraw":
        • Calls withdraw() with preparedOperationsMLXDR from withdrawResult
        • On success, clears withdraw state and navigates home
  • Router: src/popup/app.tsx

    • Added route handling for withdraw and withdraw-confirmation
  • Home integration:

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

Implementation Details

  • Entropy mapping:

    • LOW: 1 slot
    • MEDIUM: 5 slots
    • HIGH: 10 slots
    • V_HIGH: 15 slots
      Slots are allocated between:
    • WITHDRAW operation (1 slot)
    • 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
    • Only selects UTXOs from the Privacy Channel
  • Change distribution:

    • Uses partitionAmountRandom to randomly split change across reserved change
      UTXOs
    • Ensures a minimum per-part amount (1 stroop)
    • Change always returns to the Privacy Channel
  • Withdraw conditions:

    • SPEND operations include withdraw conditions that reference the WITHDRAW
      operation
    • This ensures that funds are only sent to the specified G account address
    • Change CREATE operations are also added as conditions to SPEND operations
  • Operation order:

    • MLXDR operations are ordered as: CREATE (change), SPEND, WITHDRAW
    • This order ensures proper validation and execution by the privacy provider
  • 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
      • Destination address format (valid Stellar public key)
      • Positive and finite amount
      • Sufficient balance/UTXOs in Privacy Channel for the withdraw
    • Returns structured error codes (e.g., LOCKED, NOT_FOUND,
      INVALID_ADDRESS, INSUFFICIENT_BALANCE, INVALID_SESSION, AUTH_FAILED,
      WITHDRAW_FAILED, PREPARE_WITHDRAW_FAILED)
    • Clears provider session on PrivacyProviderAuthError

Files Changed

New / Significantly Updated Backend Files:

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

Backend Integration:

  • src/background/messages.ts — Added Withdraw and PrepareWithdraw message
    types and mappings
  • src/background/handler.ts — Registered handleWithdraw and
    handlePrepareWithdraw

New / Updated Frontend Files:

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

State & Routing:

  • src/popup/hooks/state.tsx — Added withdraw routes and state
    (withdrawFormData, withdrawResult)
  • src/popup/app.tsx — Added withdraw and withdraw-confirmation route
    handling

Home Integration:

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

Testing Notes

  • Form validation

    • Ensure destination address is present and passes Stellar public key format
      checks
    • Validate amount > 0
    • Validate provider session is active
    • Test invalid address formats (should show error)
  • Prepare flow

    • Test prepareWithdraw with valid G account address and various entropy
      levels
    • Confirm that:
      • withdrawOperation, changeOperations, and spendOperations are
        populated
      • operationsMLXDR length matches 1 + numCreates + numSpends (1 for
        WITHDRAW)
      • Totals (totalSpendAmount, changeAmount, withdrawAmount) make sense
      • Withdraw operation has correct destination address
  • Confirmation UI

    • Verify all summary fields (total, withdraw, fee, privacy level, account,
      channel)
    • Expand operations:
      • Check counts for WITHDRAW (1), SPEND, and CREATE
      • Confirm change classification
      • Verify amounts and addresses are rendered correctly
      • Verify destination address is displayed correctly
  • Execution

    • From a prepared state, click "Execute Withdraw"
    • Confirm:
      • Only preparedOperationsMLXDR is submitted (no re-preparation)
      • Success clears withdraw state and returns to home
      • Authentication errors clear provider session and show error
      • Funds are correctly sent to the specified G account
  • Navigation

    • Home → Withdraw → Confirmation → Home
    • Back from confirmation returns to withdraw form with preserved data
  • Edge cases

    • Test with insufficient balance in Privacy Channel
    • Test with exact amount (no change)
    • Test with change that requires multiple CREATE operations
    • Test with different entropy levels to verify UTXO selection

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 withdraw transaction history and status tracking
  • Add support for memo fields in withdraw operations
  • Implement address book for frequently used G account addresses
  • Add validation for destination account existence (optional check)

@toruguera toruguera self-assigned this Feb 20, 2026
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.

1 participant