Retirar tokens

Cómo retirar tokens del saldo disponible confidencial

Para retirar tokens del saldo disponible confidencial al saldo público:

  1. Crea dos pruebas en el lado del cliente:

    Prueba de igualdad (CiphertextCommitmentEqualityProofData): Verifica que el texto cifrado del saldo disponible restante tras el retiro coincida con su compromiso de Pedersen correspondiente, garantizando que el nuevo saldo disponible de la cuenta se calcule correctamente como remaining_balance = current_balance - withdraw_amount.

    Prueba de rango (BatchedRangeProofU64Data): Verifica que el saldo disponible restante tras el retiro sea no negativo y esté dentro de un rango especificado.

  2. Para cada prueba:

    • Invoca el programa de pruebas ZK ElGamal para verificar los datos de la prueba.
    • Almacena los metadatos específicos de la prueba en una cuenta de "estado de contexto" de la prueba para usarla en otras instrucciones.
  3. Invoca la instrucción ConfidentialTransferInstruction::Withdraw proporcionando las dos cuentas de prueba.

  4. Cierra las cuentas de prueba para recuperar el SOL utilizado para crearlas.

El siguiente diagrama muestra los pasos involucrados en el retiro de tokens del saldo disponible confidencial al saldo público:

Withdraw Tokens

Instrucciones requeridas

Para retirar tokens del saldo disponible confidencial al saldo público, debes:

  • Generar una prueba de igualdad y una prueba de rango en el lado del cliente
  • Invocar el programa de pruebas ZK ElGamal para verificar las pruebas e inicializar las cuentas de "estado de contexto"
  • Invocar la instrucción ConfidentialTransferInstruction::Withdraw proporcionando las dos cuentas de prueba.
  • Cerrar las dos cuentas de prueba para recuperar el rent.

El ejemplo en Rust que aparece a continuación genera las pruebas con el crate spl-token-confidential-transfer-proof-generation, verifica cada una en una cuenta de estado de contexto a través del programa ZK ElGamal Proof, hace referencia a ambas cuentas en la instrucción de retiro y las cierra posteriormente. El ejemplo en TypeScript utiliza el helper getConfidentialWithdrawInstructionPlan de @solana-program/token-2022/confidential, que ensambla las mismas cuentas de prueba, el retiro y el cierre como un plan de instrucciones de múltiples transacciones.

Código de ejemplo

El siguiente ejemplo retira tokens del saldo confidencial disponible de vuelta al saldo público. La cuenta ya debe estar configurada para transferencias confidenciales y tener un saldo confidencial disponible.

Las transferencias confidenciales dependen del programa ZK ElGamal Proof, que está habilitado en mainnet y devnet. Un solana-test-validator estándar no lo habilita, pero un validator local con bifurcación de mainnet como Surfpool sí lo hace. Ejecuta el ejemplo en uno de esos entornos (el código usa devnet) con un pagador con fondos, y reemplaza los marcadores de posición con tu mint y 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?

Tabla de Contenidos

Editar Página