Retirer des tokens

Comment retirer des tokens depuis le solde disponible confidentiel

Pour retirer des tokens du solde disponible confidentiel vers le solde public :

  1. Créez deux preuves côté client :

    Preuve d'égalité (CiphertextCommitmentEqualityProofData): Vérifie que le texte chiffré du solde disponible restant après le retrait correspond à son engagement de Pedersen associé, garantissant que le nouveau solde disponible du compte est correctement calculé comme remaining_balance = current_balance - withdraw_amount.

    Preuve de plage (BatchedRangeProofU64Data): Vérifie que le solde disponible restant après le retrait est non négatif et dans une plage spécifiée.

  2. Pour chaque preuve :

    • Invoquez le programme de preuve ZK ElGamal pour vérifier les données de preuve.
    • Stockez les métadonnées spécifiques à la preuve dans un compte « état de contexte » de preuve à utiliser dans d'autres instructions.
  3. Invoquez l'instruction ConfidentialTransferInstruction::Withdraw en fournissant les deux comptes de preuve.

  4. Fermez les comptes de preuve pour récupérer le SOL utilisé pour les créer.

Le diagramme suivant illustre les étapes du retrait de tokens depuis le solde disponible confidentiel vers le solde public :

Withdraw Tokens

Instructions requises

Pour retirer des tokens du solde disponible confidentiel vers le solde public, vous devez :

  • Générer une preuve d'égalité et une preuve de plage côté client
  • Invoquer le programme de preuve Zk ElGamal pour vérifier les preuves et initialiser les comptes « état de contexte »
  • Invoquer l'instruction ConfidentialTransferInstruction::Withdraw en fournissant les deux comptes de preuve.
  • Fermer les deux comptes de preuve pour récupérer le rent.

L'exemple Rust ci-dessous génère les preuves avec la crate spl-token-confidential-transfer-proof-generation, vérifie chacune dans un compte d'état de contexte via le programme ZK ElGamal Proof, référence les deux comptes dans l'instruction de retrait et les ferme ensuite. L'exemple TypeScript utilise l'assistant getConfidentialWithdrawInstructionPlan de @solana-program/token-2022/confidential, qui assemble les mêmes comptes de preuve, le retrait et les fermetures sous forme de plan d'instructions multi-transactions.

Exemple de code

L'exemple suivant retire des tokens du solde disponible confidentiel pour les reverser dans le solde public. Le compte doit déjà être configuré pour les transferts confidentiels et détenir un solde confidentiel disponible.

Les transferts confidentiels dépendent du programme ZK ElGamal Proof, qui est activé sur le mainnet et le devnet. Un solana-test-validator standard ne l'active pas, mais un validator local avec fork du mainnet tel que Surfpool le fait. Exécutez l'exemple sur l'un d'eux (le code utilise le devnet) avec un payeur financé, et remplacez les espaces réservés par votre mint et votre token account.

Rust

const ZK_PROOF_PROGRAM_ID: Pubkey =
solana_pubkey::pubkey!("ZkE1Gama1Proof11111111111111111111111111111");
fn main() -> Result<()> {
let rpc_client = RpcClient::new_with_commitment(
String::from("https://api.devnet.solana.com"),
CommitmentConfig::confirmed(),
);
// Owner = fee payer = token account owner. The account must already be
// configured for confidential transfers with an available confidential
// balance to withdraw from.
let owner = load_keypair()?;
let amount: u64 = 100;
let decimals: u8 = 2;
// Setup: create an available confidential balance to withdraw.
let (mint, token_account) = setup_withdrawable_account(&rpc_client, &owner, amount, decimals)?;
// Derive the owner's keys and read the current confidential available balance.
let (elgamal_keypair, aes_key) = derive_confidential_keys(&owner, &token_account.to_bytes())
.map_err(|e| anyhow::anyhow!("derive confidential keys: {e}"))?;
let account_data = rpc_client.get_account(&token_account)?;
let account = StateWithExtensions::<TokenAccount>::unpack(&account_data.data)?;
let ct_extension = account.get_extension::<ConfidentialTransferAccount>()?;
// The ElGamal available-balance ciphertext is required to build the proof.
let available_balance: ElGamalCiphertext = ct_extension
.available_balance
.try_into()
.map_err(|e| anyhow::anyhow!("decode available balance: {e:?}"))?;
// Read the plaintext balance from the AES-encrypted decryptable balance.
// ElGamal's decrypt_u32 only recovers values up to 2^32 raw units, so it
// fails for realistic balances; the AES field has no such limit.
let decryptable_balance: AeCiphertext = ct_extension
.decryptable_available_balance
.try_into()
.map_err(|e| anyhow::anyhow!("decode decryptable balance: {e:?}"))?;
let current_available = decryptable_balance
.decrypt(&aes_key)
.context("decrypt available balance")?;
// Generate the equality and range proofs for the withdrawal.
let proof_data = withdraw_proof_data(&available_balance, current_available, amount, &elgamal_keypair)
.map_err(|e| anyhow::anyhow!("withdraw_proof_data: {e}"))?;
let new_available = current_available
.checked_sub(amount)
.context("insufficient confidential balance")?;
let new_decryptable: PodAeCiphertext = aes_key.encrypt(new_available).into();
// The owner is the context-state authority for both proof accounts.
let authority: Address = owner.pubkey().to_bytes().into();
// Equality proof context state account.
let equality_account = Keypair::new();
let equality_size = size_of::<ProofContextState<CiphertextCommitmentEqualityProofContext>>();
let equality_create_ix = system_instruction::create_account(
&owner.pubkey(),
&equality_account.pubkey(),
rpc_client.get_minimum_balance_for_rent_exemption(equality_size)?,
equality_size as u64,
&ZK_PROOF_PROGRAM_ID,
);
let equality_verify_ix = ProofInstruction::VerifyCiphertextCommitmentEquality
.encode_verify_proof(
Some(ContextStateInfo {
context_state_account: &Address::from(equality_account.pubkey().to_bytes()),
context_state_authority: &authority,
}),
&proof_data.equality_proof_data,
);
send_tx(&rpc_client, &[equality_create_ix], &[&owner, &equality_account])?;
send_tx(&rpc_client, &[equality_verify_ix], &[&owner])?;
// Range proof context state account.
let range_account = Keypair::new();
let range_size = size_of::<ProofContextState<BatchedRangeProofContext>>();
let range_create_ix = system_instruction::create_account(
&owner.pubkey(),
&range_account.pubkey(),
rpc_client.get_minimum_balance_for_rent_exemption(range_size)?,
range_size as u64,
&ZK_PROOF_PROGRAM_ID,
);
let range_verify_ix = ProofInstruction::VerifyBatchedRangeProofU64.encode_verify_proof(
Some(ContextStateInfo {
context_state_account: &Address::from(range_account.pubkey().to_bytes()),
context_state_authority: &authority,
}),
&proof_data.range_proof_data,
);
send_tx(&rpc_client, &[range_create_ix], &[&owner, &range_account])?;
send_tx(&rpc_client, &[range_verify_ix], &[&owner])?;
// Withdraw, referencing both pre-verified proof accounts.
let withdraw_ixs = withdraw(
&spl_token_2022::id(),
&token_account,
&mint,
amount,
decimals,
&new_decryptable,
&owner.pubkey(),
&[&owner.pubkey()],
ProofLocation::ContextStateAccount(&equality_account.pubkey()),
ProofLocation::ContextStateAccount(&range_account.pubkey()),
)?;
let blockhash = rpc_client.get_latest_blockhash()?;
let transaction =
Transaction::new_signed_with_payer(&withdraw_ixs, Some(&owner.pubkey()), &[&owner], blockhash);
let signature = rpc_client.send_and_confirm_transaction(&transaction)?;
// Close both proof accounts to reclaim their rent.
for account in [&equality_account, &range_account] {
let close_ix = close_context_state(
ContextStateInfo {
context_state_account: &Address::from(account.pubkey().to_bytes()),
context_state_authority: &authority,
},
&authority,
);
send_tx(&rpc_client, &[close_ix], &[&owner])?;
}
println!("Withdrew {amount} tokens to the public balance: {signature}");
Ok(())
}

Typescript

const client = await createClient()
.use(signerFromFile(join(homedir(), ".config/solana/id.json")))
.use(
solanaRpc({
rpcUrl: "https://api.devnet.solana.com",
maxConcurrency: 1
})
);
// The Solana CLI default keypair, used as fee payer, mint authority, and
// token account owner.
const owner = client.payer;
const depositAmount = 100n;
const amount = 25n;
const decimals = 2;
// Setup: create a confidential account with an available confidential balance.
const mint = await createConfidentialMint(client, owner, decimals);
const token = await createConfidentialTokenAccount(client, owner, mint);
await mintPublicTokens(client, owner, mint, token, depositAmount);
await depositTokens(client, owner, mint, token, depositAmount, decimals);
await applyPendingBalance(client, owner, mint, token);
// Derive the owner's recoverable ElGamal and AES keys, bound to (owner, mint).
const { elgamalKeypair, aesKey } = await deriveConfidentialKeys(owner, mint);
const tokenAccount = (await fetchToken(client.rpc, token)).data;
// Builds the equality + range proof accounts, the withdraw, and the closes as a
// multi-transaction instruction plan.
const plan = await getConfidentialWithdrawInstructionPlan({
rpc: client.rpc,
payer: owner,
authority: owner,
token,
mint,
tokenAccount,
amount,
decimals,
elgamalKeypair,
aesKey
});
const result = await client.sendTransactions(plan);
const summary = summarizeTransactionPlanResult(result);
const signature =
summary.successfulTransactions[summary.successfulTransactions.length - 1]
.context.signature;
console.log(`Withdrew ${amount} tokens to the public balance: ${signature}`);

Is this page helpful?

Table des matières

Modifier la page
© 2026 Fondation Solana. Tous droits réservés.