保留残高の適用

保留中の残高を利用可能な残高に適用する方法

トークンを機密で転送するには、まず公開トークン残高を機密残高に変換する必要があります。この変換は2段階で行われます:

  1. 機密保留残高:最初に、トークンは公開残高から「保留中」の機密残高に「デポジット」されます。
  2. 機密利用可能残高:次に、保留中の残高が利用可能な残高に「適用」され、機密転送にトークンが使用できるようになります。

このセクションでは、第2段階である保留中の残高を利用可能な残高に適用する手順を説明します。

トークンが公開残高から「デポジット」される場合、またはトークンがある token account から別の token account へ機密転送される場合、トークンは最初に機密保留残高に追加されます。機密転送にトークンを使用するには、保留中の残高を利用可能な残高に「適用」する必要があります。

Apply Pending Balance

次の図は、保留中の残高を利用可能な残高に適用する手順を示しています:

Apply Pending Balance

必要な instructions

保留中の残高を利用可能な残高に変換するには、 ConfidentialTransferInstruction::ApplyPendingBalance instruction を呼び出してください。

spl_token_client クレートは、以下の例に示すように、ApplyPendingBalance instruction を含むトランザクションを構築して送信する confidential_transfer_apply_pending_balance メソッドを提供しています。

サンプルコード

次の例では、機密保留残高を機密利用可能残高に適用する方法を示しています。

機密転送は ZK ElGamal Proof プログラムに依存しており、このプログラムはメインネットおよびデブネットで有効になっています。標準的な solana-test-validator ではこれが有効になっていませんが、Surfpool のようなメインネットフォークのローカル validator では有効です。資金が入ったペイヤーを用意し、デブネット(コードではデブネットを使用)またはそれらのいずれかに対してサンプルを実行し、プレースホルダーのミントアドレスとアカウントアドレスを自身のものに置き換えてください。

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(),
);
let owner = load_keypair()?;
let amount: u64 = 100;
let decimals: u8 = 2;
// Setup: create a confidential account with a pending balance.
let (_mint, token_account) = setup_pending_balance_account(&rpc_client, &owner, amount, decimals)?;
// Derive the owner's keys to decrypt the current balances.
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 pending balance is split into low (16 bits) and high parts, each small
// enough to recover with ElGamal's bounded decrypt_u32.
let pending_lo: ElGamalCiphertext = ct_extension
.pending_balance_lo
.try_into()
.map_err(|e| anyhow::anyhow!("pending_balance_lo: {e:?}"))?;
let pending_hi: ElGamalCiphertext = ct_extension
.pending_balance_hi
.try_into()
.map_err(|e| anyhow::anyhow!("pending_balance_hi: {e:?}"))?;
let pending_lo_amount = pending_lo
.decrypt_u32(elgamal_keypair.secret())
.context("decrypt pending_balance_lo")? as u64;
let pending_hi_amount = pending_hi
.decrypt_u32(elgamal_keypair.secret())
.context("decrypt pending_balance_hi")? as u64;
let pending_total = pending_lo_amount + (pending_hi_amount << 16);
// Read the current available 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!("decryptable_available_balance: {e:?}"))?;
let current_available = decryptable_balance
.decrypt(&aes_key)
.context("decrypt available balance")?;
let new_available = current_available + pending_total;
// Re-encrypt the new available balance with the AES key for fast reads.
let new_decryptable: PodAeCiphertext = aes_key.encrypt(new_available).into();
// The expected counter guards against pending credits that arrive between
// building and processing this instruction.
let expected_counter: u64 = ct_extension.pending_balance_credit_counter.into();
let apply_ix = apply_pending_balance(
&spl_token_2022::id(),
&token_account,
expected_counter,
&new_decryptable,
&owner.pubkey(),
&[&owner.pubkey()],
)?;
let blockhash = rpc_client.get_latest_blockhash()?;
let transaction =
Transaction::new_signed_with_payer(&[apply_ix], Some(&owner.pubkey()), &[&owner], blockhash);
let signature = rpc_client.send_and_confirm_transaction(&transaction)?;
println!("Applied pending balance. New available: {new_available}. Tx: {signature}");
Ok(())
}

Typescript

const client = await createClient()
.use(signerFromFile(join(homedir(), ".config/solana/id.json")))
.use(
solanaRpc({
rpcUrl: "https://api.devnet.solana.com"
})
);
// The Solana CLI default keypair, used as fee payer, mint authority, and
// token account owner.
const owner = client.payer;
const amount = 100n;
const decimals = 2;
// Setup: create a confidential account with a pending balance.
const mint = await createConfidentialMint(client, owner, decimals);
const token = await createConfidentialTokenAccount(client, owner, mint);
await mintPublicTokens(client, owner, mint, token, amount);
await depositTokens(client, owner, mint, token, amount, decimals);
// Derive the owner's keys to decrypt the current balances.
const { elgamalSecretKey, aesKey } = await deriveConfidentialKeys(owner, mint);
// The helper decrypts the pending and available balances, re-encrypts the new
// available balance, and builds the ApplyPendingBalance instruction. No proof
// is required.
const tokenAccount = await fetchToken(client.rpc, token);
const applyInstruction = getApplyConfidentialPendingBalanceInstructionFromToken(
{
token,
tokenAccount: tokenAccount.data,
authority: owner,
elgamalSecretKey,
aesKey
}
);
const result = await client.sendTransaction([applyInstruction]);
console.log(
`Applied pending balance. New available: ${amount}. Tx: ${result.context.signature}`
);

Is this page helpful?

目次

ページを編集
© 2026 Solana Foundation. 無断転載を禁じます。