Skip to main content

Overview

The withdraw operation is the reverse of a deposit. It moves tokens from a user’s EncryptedTokenAccount (ETA) back to their AssociatedTokenAccount (ATA).
ETA (on-chain encrypted balance)


  Pool custody (on-chain SPL)  ──withdraw──▶  Your ATA
Arcium MPC verifies the withdrawal authorization. The operation follows the dual-instruction pattern — the SDK waits for both the handler and the callback transactions to confirm before returning.

Usage

import { getETAIntoATAWithdrawerFunction } from "@umbra-privacy/sdk/withdrawal";

const withdraw = getETAIntoATAWithdrawerFunction({ client });

const result = await withdraw(
  destinationAddress, // where tokens go (typically client.signer.address)
  mint,               // SPL or Token-2022 mint address
  amount,             // amount in native token units (U64)
  options?,           // optional
);

Parameters

destinationAddress
Address
required
The Solana address whose ATA will receive the withdrawn tokens. This is typically client.signer.address.
mint
Address
required
The SPL or Token-2022 mint address. Must match a mint for which the user has an existing ETA.
amount
bigint
required
The amount to withdraw, in the token’s native units. Must not exceed the current ETA balance.
options.optionalData
OptionalData32
32 bytes of caller metadata stored with the withdrawal. Defaults to 32 zero bytes. Must be a pre-hashed or pre-encrypted 32-byte value — never store plaintext identifiers.
options.accountInfoCommitment
Commitment
default:"\"confirmed\""
Commitment level used for RPC account reads during withdrawal preparation.
options.computationOffset
U64
Optional Arcium computation-account PDA offset. Defaults to a fresh random U64 per call (CSPRNG).
options.mpcCallbackDataOffset
U128
Optional MpcCallbackData PDA offset (paired with the signer in seeds). Defaults to a fresh random U128 per call.
options.feeVaultOffset
U128
default:"0n"
Fee-vault PDA offset. The fee-vault path is currently unused for public-ATA withdrawals — leave at the default 0n (sentinel for “no fee-vault interaction”).
options.destinationProgram
Address
Optional observer-CPI destination program invoked after the MPC callback. Defaults to the System Program (sentinel = “no observer CPI”).
options.hooks
WithdrawalHooks
Per-phase lifecycle hooks (onValidationStart, onAccountFetchComplete, queueComputation, onComplete, onError, etc.).
There are no priorityFees, awaitCallback, skipPreflight, maxRetries, or epochInfoCommitment options on the withdrawer. Priority fees are configured at factory time. Callback waiting is always-on — the result’s callback field is populated on success.

Return Value

Returns a Promise<WithdrawResult>. The shape mirrors DepositResult:
interface WithdrawResult {
  readonly signatures: readonly TransactionSignature[];
  readonly queueSignature: TransactionSignature;
  readonly callback?: CallbackOutcome;
  readonly rentClaim?: RentClaimOutcome;
}

type CallbackOutcome =
  | { readonly status: "finalized"; readonly signature?: TransactionSignature; readonly elapsedMs: number }
  | { readonly status: "pruned";    readonly elapsedMs: number }
  | { readonly status: "timed-out"; readonly elapsedMs: number };

type RentClaimOutcome =
  | { readonly claimed: true;  readonly signature: TransactionSignature }
  | { readonly claimed: false; readonly reason: string };
  • signatures — every transaction signature submitted by this call, in submission order.
  • queueSignature — the handler (queue computation) transaction signature.
  • callback — present once the callback round-trip completes (always awaited). "finalized" means the MPC callback landed (and exposes signature); "pruned" / "timed-out" indicate the callback did not land — recover via the staged recoverer (below).
  • rentClaim — best-effort rent reclamation outcome. The withdrawal itself still succeeded even when claimed: false.

Example

import { getETAIntoATAWithdrawerFunction } from "@umbra-privacy/sdk/withdrawal";

const withdraw = getETAIntoATAWithdrawerFunction({ client });

const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";

const result = await withdraw(client.signer.address, USDC, 50_000_000n); // 50 USDC
console.log("Queue signature:", result.queueSignature);
if (result.callback?.status === "finalized") {
  console.log("Callback signature:", result.callback.signature);
}

Withdrawal destination

The tokens are sent to the ATA associated with the destinationAddress for the given mint. If that ATA does not exist, the transaction will fail with an account-not-found error. Create the ATA first using standard token tooling if needed.

Callback failure recovery

If the handler succeeds but the Arcium callback never lands (network partition, compute budget, Arcium outage), the tokens stay staged in the pool ATA. Reclaim them with the recoverer factories from @umbra-privacy/sdk/account:
import { getStagedSplRecovererFunction, getStagedSolRecovererFunction } from "@umbra-privacy/sdk/account";

const recover = getStagedSplRecovererFunction({ client });
await recover({ mint });
No MPC, no ZK proof — a synchronous recovery transaction. Use getStagedSolRecovererFunction for wSOL.

Error Handling

Withdrawal errors are typed with a stage field identifying exactly where in the pipeline the failure occurred:
import { isEncryptedWithdrawalError } from "@umbra-privacy/sdk/errors";

try {
  const result = await withdraw(client.signer.address, mint, amount);
} catch (err) {
  if (isEncryptedWithdrawalError(err)) {
    switch (err.stage) {
      case "validation":
        // Zero amount, or no ETA exists for this mint.
        console.error("Withdrawal validation failed:", err.message);
        break;

      case "mint-fetch":
        // Could not fetch the mint account — check RPC connectivity and mint address.
        break;

      case "pda-derivation":
        // Could not derive required program addresses — unexpected on-chain state.
        break;

      case "instruction-build":
        // Could not construct the instruction — protocol state mismatch.
        break;

      case "transaction-sign":
        // User rejected the transaction in their wallet.
        showNotification("Withdrawal cancelled.");
        break;

      case "transaction-send":
        // Transaction submitted but confirmation timed out.
        // The transaction may have landed — check on-chain before retrying.
        console.warn("Confirmation timeout. Check signature on-chain:", err.cause);
        break;

      default:
        console.error("Withdrawal failed at stage:", err.stage, err);
    }
  } else {
    throw err;
  }
}
See Error Handling for retry guidance and a full stage reference.

Timing

Withdrawals follow the dual-instruction pattern. The SDK submits the handler transaction, waits for Arcium to complete the MPC computation, then waits for the callback transaction to confirm. Total wall-clock time is typically a few seconds on a well-connected network.