- Un retiro mueve tokens de un saldo privado a un saldo público de Solana.
- Los retiros se envían en una sola transacción de Solana a la dirección de una billetera de Solana.
Retiro: qué es privado
| Field | Visibility | Why |
|---|---|---|
| Source private wallet | Public | In a confidential Ring the source private wallet is visible onchain. |
| Asset | Public | The asset is visible onchain |
| Amount | Public | The withdrawn amount is visible onchain |
| Destination public wallet | Public | The destination wallet address is visible onchain |
| Resulting public balance | Public | The resulting public balance is visible onchain |
| Remaining private balance | Private | The remaining balance is encrypted onchain |
El Ring sin permisos es confidencial y cifra el monto y el activo.
Puedes configurar un Ring personalizado como confidencial o anónimo (remitente, destinatario, activo y monto cifrados).
Cómo funciona un retiro
Un retiro funciona de forma similar a una transferencia pública de Solana:- El saldo SOL o SPL del usuario se cifra en la cadena.
-
El usuario descifra el estado privado, la billetera crea un retiro y el propietario firma.
- Obtén el estado cifrado con métodos RPC específicos. Solo el usuario puede descifrar los saldos localmente.
- La billetera establece el monto y el destinatario y luego solicita una prueba ZK. De forma predeterminada, el proveedor de RPC genera la prueba ZK y la devuelve.
- El entorno de ejecución de Solana verifica las firmas e invoca el programa de privacidad de Solana, que verifica la prueba ZK sin revelar el estado cifrado.
- La aplicación consulta el estado mediante el hash de la transacción de Solana.
Compare to Solana Transfer
Compare to Solana Transfer
- El saldo SOL o SPL del usuario es público en la cadena.
- La billetera lee el estado público, crea una transferencia y el propietario firma.
- El entorno de ejecución de Solana verifica las firmas e invoca el programa del sistema o el programa de tokens, que actualiza el saldo público.
- La aplicación consulta el estado mediante el hash de la transacción de Solana.
Este es el flujo de transacción general del Ring confidencial sin permisos.
Compáralo con los Rings personalizados en conceptos.
Comienza
- TypeScript Client
- Rust Client
1
Requisitos previos
The TypeScript examples require Node.js 24 or later, pnpm 11.18.0, and the Solana CLI.
pnpm add @heliuslabs/zolana@0.2.0-alpha @solana/kit@^8.3.0
Connect to Endpoints
Connect to Endpoints
- Devnet
- Localnet
pnpm install
cp .env.example .env
.env
API_KEY=YOUR_API_KEY
ZOLANA_PAYER_KEYPAIR=~/.config/solana/id.json
import { createZolanaClient } from "@heliuslabs/zolana";
const client = await createZolanaClient({
solanaRpcUrl: "https://devnet.helius-rpc.com/?api-key=YOUR_API_KEY",
indexerUrl: "https://d2xah7tnhdhcom.cloudfront.net",
proverUrl: "https://d21ni15goiip6l.cloudfront.net",
});
On localnet the SDK starts the local test validator (
:8899), Photon indexer (:8784), and prover (:3001), and the
client connects to them automatically without needing endpoint configuration.cargo install --git https://github.com/helius-labs/zolana --tag v0.1.0-alpha zolana-cli
zolana dev start
import { createZolanaClient } from "@heliuslabs/zolana";
const client = await createZolanaClient({});
2
Retirar a un saldo público
Solana Kit send helper
Solana Kit send helper
import {
appendTransactionMessageInstructions,
assertIsTransactionWithBlockhashLifetime,
createTransactionMessage,
getSignatureFromTransaction,
pipe,
sendTransactionWithoutConfirmingFactory,
setTransactionMessageConfig,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners,
type Instruction,
type Signature,
type TransactionSigner,
} from "@solana/kit";
import { createZolanaClient } from "@heliuslabs/zolana";
type Client = Awaited<ReturnType<typeof createZolanaClient>>;
export interface ConfirmedTransaction {
readonly signature: Signature;
readonly slot: bigint;
}
export function sendAndConfirmFactory(
client: Client,
feePayer: TransactionSigner,
): (instructions: readonly Instruction[]) => Promise<ConfirmedTransaction> {
const sendTransaction = sendTransactionWithoutConfirmingFactory({
rpc: client.solanaRpc,
});
return async function sendAndConfirm(
instructions: readonly Instruction[],
): Promise<ConfirmedTransaction> {
const { value: lifetime } = await client.solanaRpc
.getLatestBlockhash()
.send();
const signed = await signTransactionMessageWithSigners(
pipe(
createTransactionMessage({ version: 1 }),
(message) => setTransactionMessageFeePayerSigner(feePayer, message),
(message) =>
setTransactionMessageLifetimeUsingBlockhash(lifetime, message),
(message) =>
setTransactionMessageConfig(
{
computeUnitLimit: 450_000,
loadedAccountsDataSizeLimit: 64 * 1024 * 1024,
},
message,
),
(message) =>
appendTransactionMessageInstructions(instructions, message),
),
);
assertIsTransactionWithBlockhashLifetime(signed);
await sendTransaction(signed, { commitment: "confirmed" });
const signature = getSignatureFromTransaction(signed);
const slot = await client.confirmTransaction(signature);
return { signature, slot };
};
}
- El SDK devuelve instrucciones. La aplicación las firma y las envía.
sendAndConfirmFactorycrea una transacción de Kit, la envía y devuelve la firma junto con el slot en el que se procesó.
- SOL
- SPL
import { LocalKeys } from "@heliuslabs/zolana/client";
import { SOL_MINT } from "@heliuslabs/zolana";
import {
transactInstruction,
TransactWithdrawal,
} from "@heliuslabs/zolana/interface";
import {
ConfidentialTransfer,
ProofInputUtxo,
WithdrawalTarget,
} from "@heliuslabs/zolana/transaction";
const withdrawalUtxo =
transferBalance.utxos[0]!;
const withdrawalInput =
ProofInputUtxo.fromKeypair(
withdrawalUtxo,
sender,
);
const withdrawal = new ConfidentialTransfer(
senderAddress,
[withdrawalInput],
senderSigner.address,
);
withdrawal.withdraw(
SOL_MINT,
WITHDRAW_AMOUNT,
WithdrawalTarget.sol({
recipient: senderSigner.address,
}),
);
const withdrawalProofInputs = withdrawal.sign(
sender,
assets,
);
const senderKeys = LocalKeys.fromKeypair(sender, client.proofService);
const withdrawalData =
await client.proveTransact(
withdrawalProofInputs,
senderKeys,
);
const withdrawalInstruction =
await transactInstruction({
payer: senderSigner,
inputTree: client.tree,
outputTree: client.tree,
withdrawal: TransactWithdrawal.sol({
recipient: senderSigner.address,
}),
data: withdrawalData,
});
const withdrawalTx = await sendAndConfirm([
withdrawalInstruction,
]);
import { LocalKeys } from "@heliuslabs/zolana/client";
import {
transactInstruction,
TransactWithdrawal,
} from "@heliuslabs/zolana/interface";
import {
ConfidentialTransfer,
ProofInputUtxo,
WithdrawalTarget,
} from "@heliuslabs/zolana/transaction";
const withdrawalUtxo =
transferBalance.utxos[0]!;
const withdrawalInput =
ProofInputUtxo.fromKeypair(
withdrawalUtxo,
sender,
);
const withdrawal = new ConfidentialTransfer(
senderAddress,
[withdrawalInput],
senderSigner.address,
);
withdrawal.withdraw(
spl.mint,
WITHDRAW_AMOUNT,
WithdrawalTarget.spl({
recipientTokenAccount: spl.userTokenAccount,
splTokenInterface: spl.splTokenInterface,
splInterfaceBump: spl.splInterfaceBump,
}),
);
const withdrawalProofInputs = withdrawal.sign(
sender,
assets,
);
const senderKeys = LocalKeys.fromKeypair(sender, client.proofService);
const withdrawalData =
await client.proveTransact(
withdrawalProofInputs,
senderKeys,
);
const withdrawalInstruction =
await transactInstruction({
payer: senderSigner,
inputTree: client.tree,
outputTree: client.tree,
withdrawal: TransactWithdrawal.spl({
mint: spl.mint,
splTokenInterface: spl.splTokenInterface,
recipientTokenAccount: spl.userTokenAccount,
tokenProgram: spl.tokenProgram,
}),
data: withdrawalData,
});
const withdrawalTx = await sendAndConfirm([
withdrawalInstruction,
]);
1. Select private token accounts to spend
1. Select private token accounts to spend
import { SOL_MINT } from "@heliuslabs/zolana";
const withdrawalUtxo =
transferBalance.utxos[0]!;
- El ejemplo gasta la cuenta de tokens privada de Solana restante después de la transferencia anterior. Un retiro puede gastar varios UTXO.
withdrawalUtxoselecciona una cuenta de tokens privada de Solana de ese saldo.
2. Prepare proof inputs
2. Prepare proof inputs
import { ProofInputUtxo } from "@heliuslabs/zolana/transaction";
const withdrawalInput =
ProofInputUtxo.fromKeypair(
withdrawalUtxo,
sender,
);
ProofInputUtxo.fromKeypairprepara el UTXO seleccionado como entrada de la prueba con el par de claves de la billetera privada del remitente.- El par de claves deriva el anulador que marca el UTXO de entrada como gastado, mientras que el activo y el monto de entrada permanecen cifrados.
3. Build and sign the withdrawal
3. Build and sign the withdrawal
import { SOL_MINT } from "@heliuslabs/zolana";
import {
ConfidentialTransfer,
WithdrawalTarget,
} from "@heliuslabs/zolana/transaction";
const withdrawal = new ConfidentialTransfer(
senderAddress,
[withdrawalInput],
senderSigner.address,
);
withdrawal.withdraw(
SOL_MINT,
WITHDRAW_AMOUNT,
WithdrawalTarget.sol({
recipient: senderSigner.address,
}),
);
const withdrawalProofInputs = withdrawal.sign(
sender,
assets,
);
senderAddresses la del remitente. El retiro gasta fondos de esta billetera.[withdrawalInput]enumera los UTXO seleccionados del remitente. Un retiro puede gastar varios UTXO.senderSigner.addresses la dirección de Solana del pagador de la comisión. Un patrocinador de gas puede pagar la comisión.WithdrawalTarget.soles el destinatario público de Solana. El destinatario puede ser el propietario o un tercero.SOL_MINTselecciona SOL. Para retirar un activo SPL o Token 2022, pasa la dirección de acuñación del token.WITHDRAW_AMOUNTse expresa en las unidades base del activo. SOL usa lamports. Los activos SPL y Token 2022 usan las unidades base del token.withdrawal.signautoriza la transición de estado, cifra el cambio privado restante y genera las entradas para el probador de conocimiento cero.assetses el registro de activos utilizado para resolver los activos privados compatibles.
4. Fetch the zero-knowledge proof
4. Fetch the zero-knowledge proof
import { LocalKeys } from "@heliuslabs/zolana/client";
const senderKeys = LocalKeys.fromKeypair(sender, client.proofService);
const withdrawalData =
await client.proveTransact(
withdrawalProofInputs,
senderKeys,
);
senderKeysusaLocalKeys.fromKeypair(sender, client.proofService)para autorizar la generación de la prueba con las claves del remitente.client.proveTransactgenera la prueba de conocimiento cero a partir del retiro firmado y devuelve los datos serializados de la instrucción.- La prueba demuestra que el remitente posee las entradas y puede gastarlas. El activo y el monto retirados son públicos. Los montos de entrada y el cambio permanecen cifrados.
5. Build the withdrawal instruction
5. Build the withdrawal instruction
import {
transactInstruction,
TransactWithdrawal,
} from "@heliuslabs/zolana/interface";
const withdrawalInstruction =
await transactInstruction({
payer: senderSigner,
inputTree: client.tree,
outputTree: client.tree,
withdrawal: TransactWithdrawal.sol({
recipient: senderSigner.address,
}),
data: withdrawalData,
});
payerfirma y paga la transacción de Solana. Un patrocinador de gas puede pagar la comisión.inputTreeeoutputTreesonclient.tree, el árbol de Merkle de estado que contiene los UTXO gastados y recibe el cambio privado del remitente.withdrawalesTransactWithdrawal.sol, la cuenta pública de Solana del destinatario.await transactInstructionderiva localmente las direcciones de las cuentas de anuladores y devuelve la instrucción. Las cuentas de anuladores marcan los UTXO de entrada como gastados para impedir que un saldo privado se gaste dos veces.datacontiene la prueba de conocimiento cero y el cambio cifrado generados en el paso anterior.- Un retiro mueve el activo de un saldo privado a una cuenta pública de Solana. Pasa la cuenta pública del destinatario.
6. Send like any Solana transaction
6. Send like any Solana transaction
import { sendAndConfirmFactory } from "../src/lib.js";
const withdrawalTx = await sendAndConfirm([
withdrawalInstruction,
]);
sendAndConfirmfirma y envíawithdrawalInstructioncomo una transacción de Solana.- La confirmación devuelve el slot en el que se procesó, utilizado para controlar la consulta al indexador.
Ejemplo de código completo
Clona y ejecuta el ejemplo:git clone https://github.com/helius-labs/zolana-examples.git
cd zolana-examples/typescript-client
pnpm install
pnpm example examples/deposit_transfer_withdraw.ts
Los ejemplos usan un Ring confidencial en local/devnet aquí.
deposit_transfer_withdraw.ts
import {
SOL_MINT,
ShieldedKeypair,
createZolanaClient,
} from "@heliuslabs/zolana";
import {
LocalKeys,
atSlot,
} from "@heliuslabs/zolana/client";
import {
depositInstruction,
transactInstruction,
DepositAsset,
TransactWithdrawal,
} from "@heliuslabs/zolana/interface";
import {
AssetRegistry,
ConfidentialTransfer,
ProofInputUtxo,
decryptToBalances,
WithdrawalTarget,
} from "@heliuslabs/zolana/transaction";
import {
cliKeypair,
sendAndConfirmFactory,
setup,
} from "../src/lib.js";
const DEPOSIT_AMOUNT = 10_000_000n;
const TRANSFER_AMOUNT = 3_000_000n;
const WITHDRAW_AMOUNT = 3_000_000n;
async function main(): Promise<void> {
const { clientConfig } = await setup();
// Connect to Helius devnet RPC plus the Photon indexer and prover.
const client =
await createZolanaClient(clientConfig);
// Initialize the sender's private wallet and local authority
// to decrypt transactions and sync balances.
// The Solana signer and private wallet are derived from the same Ed25519 seed.
const sender = ShieldedKeypair.fromKeypair(
await cliKeypair(),
);
const recipient = ShieldedKeypair.generate();
const senderSigner = sender.toSolanaSigner();
const senderAddress = sender.shieldedAddress();
const senderKeys = LocalKeys.fromKeypair(
sender,
client.proofService,
);
// The SDK hands back instructions; the app owns signing and sending.
const sendAndConfirm = sendAndConfirmFactory(
client,
senderSigner,
);
// Mints that are registered with Solana Rings for privacy.
const assets = new AssetRegistry();
// Deposit SOL into the sender's private balance.
// A deposit from a public balance reveals
// sender, recipient, asset and amount.
// Alternatively, you can onramp fiat directly to a private balance.
// 1. Move public SOL into the sender's private balance.
// The view tag is the sender's Solana public key in confidential rings.
// Used by the indexer to fetch the sender's UTXOs.
const senderViewTag =
senderAddress.confidentialViewTag();
const depositIx = await depositInstruction({
tree: client.tree,
depositor: senderSigner,
deposits: [
{
asset: DepositAsset.sol(),
viewTag: senderViewTag,
recipientOwnerHash:
senderAddress.ownerHash(),
amount: DEPOSIT_AMOUNT,
},
],
});
// 2. Send and confirm like any Solana transaction; confirmation yields the landed slot.
const depositTx = await sendAndConfirm([
depositIx,
]);
// 3. Fetch transaction outputs from the indexer, gated on the deposit's slot.
// The indexer returns encrypted outputs by transaction signature.
const depositResponse =
await client.getShieldedTransactionsBySignature(
depositTx.signature,
atSlot(depositTx.slot),
);
// 4. The sender decrypts the transaction outputs locally to read the private balance.
const balancesAfterDeposit =
await decryptToBalances({
keypair: sender,
registry: assets,
transactions:
depositResponse.transactions.map(
({ transaction }) => transaction,
),
});
const depositBalance =
balancesAfterDeposit.balance(SOL_MINT);
if (depositBalance.amount !== DEPOSIT_AMOUNT) {
throw new Error(
`expected deposit amount ${DEPOSIT_AMOUNT}, got ${depositBalance.amount}`,
);
}
if (depositBalance.utxos.length !== 1) {
throw new Error(
`expected 1 deposit utxo, got ${depositBalance.utxos.length}`,
);
}
// Confidential SOL transfer to the recipient's private balance.
// A confidential transfer reveals only sender and recipient,
// not the asset or amount.
// 1. Select private token accounts (UTXOs) that make up the private balance for the transfer.
const transferUtxo = depositBalance.utxos[0]!;
// 2. Prepare the selected UTXOs as inputs for the zero-knowledge proof.
const transferInput =
ProofInputUtxo.fromKeypair(
transferUtxo,
sender,
);
// 3. Build and sign the confidential transfer.
// Signing encrypts the asset and amount and produces the proof inputs for the ZK prover.
const transfer = new ConfidentialTransfer(
senderAddress,
[transferInput],
senderSigner.address,
);
transfer.send(
recipient.shieldedAddress(),
SOL_MINT,
TRANSFER_AMOUNT,
);
const transferProofInputs = transfer.sign(
sender,
assets,
);
// 4. Fetch the ZK proof to prove the sender can spend the balance without revealing asset and amount.
const transferData = await client.proveTransact(
transferProofInputs,
senderKeys,
);
// 5. Build the instruction with the state Merkle tree and Solana accounts required for the transfer.
// Private transfers move balances only between private token accounts, not public token accounts.
const transferInstruction =
await transactInstruction({
payer: senderSigner,
inputTree: client.tree,
outputTree: client.tree,
data: transferData,
});
// 6. Send and confirm like any Solana transaction; confirmation yields the landed slot.
const transferTx = await sendAndConfirm([
transferInstruction,
]);
// 7. Fetch the sender's UTXOs from this transaction, gated on the transfer's slot,
// and read the remaining private balance.
const transferResponse =
await client.getShieldedTransactionsBySignature(
transferTx.signature,
atSlot(transferTx.slot),
);
const balancesAfterTransfer =
await decryptToBalances({
keypair: sender,
registry: assets,
transactions:
transferResponse.transactions.map(
({ transaction }) => transaction,
),
});
const transferBalance =
balancesAfterTransfer.balance(SOL_MINT);
if (
transferBalance.amount !==
DEPOSIT_AMOUNT - TRANSFER_AMOUNT
) {
throw new Error(
`expected remaining amount from this run ${DEPOSIT_AMOUNT - TRANSFER_AMOUNT}, got ${transferBalance.amount}`,
);
}
if (transferBalance.utxos.length !== 1) {
throw new Error(
`expected 1 transfer utxo, got ${transferBalance.utxos.length}`,
);
}
// Withdraw SOL from the sender's private balance to their public balance.
// A withdrawal reveals the sender, recipient, asset, and amount.
// 1. Select private token accounts (UTXOs) that make up the private balance for the withdrawal.
const withdrawalUtxo =
transferBalance.utxos[0]!;
// 2. Prepare the selected UTXOs as inputs for the zero-knowledge proof.
const withdrawalInput =
ProofInputUtxo.fromKeypair(
withdrawalUtxo,
sender,
);
// 3. Build and sign the private-to-public withdrawal.
// Signing encrypts the asset and amount of the remaining private balance
// and produces the proof inputs for the ZK prover.
const withdrawal = new ConfidentialTransfer(
senderAddress,
[withdrawalInput],
senderSigner.address,
);
withdrawal.withdraw(
SOL_MINT,
WITHDRAW_AMOUNT,
WithdrawalTarget.sol({
recipient: senderSigner.address,
}),
);
const withdrawalProofInputs = withdrawal.sign(
sender,
assets,
);
// 4. Fetch the ZK proof to prove the sender can spend the balance.
const withdrawalData =
await client.proveTransact(
withdrawalProofInputs,
senderKeys,
);
// 5. Build the instruction with the state Merkle tree and Solana accounts required for the withdrawal.
const withdrawalInstruction =
await transactInstruction({
payer: senderSigner,
inputTree: client.tree,
outputTree: client.tree,
withdrawal: TransactWithdrawal.sol({
recipient: senderSigner.address,
}),
data: withdrawalData,
});
// 6. Send and confirm like any Solana transaction; confirmation yields the landed slot.
const withdrawalTx = await sendAndConfirm([
withdrawalInstruction,
]);
// 7. Fetch the sender's UTXOs from this transaction, gated on the withdrawal's slot,
// and read the remaining private balance.
const withdrawalResponse =
await client.getShieldedTransactionsBySignature(
withdrawalTx.signature,
atSlot(withdrawalTx.slot),
);
const balancesAfterWithdrawal =
await decryptToBalances({
keypair: sender,
registry: assets,
transactions:
withdrawalResponse.transactions.map(
({ transaction }) => transaction,
),
});
const withdrawalBalance =
balancesAfterWithdrawal.balance(SOL_MINT);
if (
withdrawalBalance.amount !==
DEPOSIT_AMOUNT -
TRANSFER_AMOUNT -
WITHDRAW_AMOUNT
) {
throw new Error(
`expected remaining amount from this run ${DEPOSIT_AMOUNT - TRANSFER_AMOUNT - WITHDRAW_AMOUNT}, got ${withdrawalBalance.amount}`,
);
}
if (withdrawalBalance.utxos.length !== 1) {
throw new Error(
`expected 1 withdrawal utxo, got ${withdrawalBalance.utxos.length}`,
);
}
// 8. Read remaining private balance and the public balance.
const solanaBalance = await client.getBalance(
senderSigner.address,
);
console.log(
`withdraw private_balance=${withdrawalBalance.amount} ` +
`solana_balance=${solanaBalance} tx=${withdrawalTx.signature}`,
);
}
await main();
1
Requisitos previos
The Rust examples require Rust 1.98.1 and the Solana CLI v4.0.2. See the Solana installation guide.
Cargo.toml
[dependencies]
zolana-client = { git = "https://github.com/helius-labs/zolana", tag = "v0.2.0-alpha", features = ["indexer-api", "solana-rpc"] }
zolana-interface = { git = "https://github.com/helius-labs/zolana", tag = "v0.2.0-alpha", features = ["solana"] }
zolana-keypair = { git = "https://github.com/helius-labs/zolana", tag = "v0.2.0-alpha" }
zolana-transaction = { git = "https://github.com/helius-labs/zolana", tag = "v0.2.0-alpha" }
zolana-wallet = { git = "https://github.com/helius-labs/zolana", tag = "v0.2.0-alpha" }
Connect to Endpoints
Connect to Endpoints
- Devnet
- Localnet
Add a Helius API key:The examples use the Solana CLI wallet as the payer by default. The payer must hold devnet SOL. See How to Get Devnet SOL.
.env
API_KEY=YOUR_API_KEY
ZOLANA_PAYER_KEYPAIR=~/.config/solana/id.json
use zolana_client::{SolanaRpc, ZolanaClient};
use zolana_interface::pda;
let tree = pda::tree(0);
let client = ZolanaClient::from_urls(
SolanaRpc::new("https://devnet.helius-rpc.com/?api-key=YOUR_API_KEY"),
"https://d2xah7tnhdhcom.cloudfront.net",
"https://d21ni15goiip6l.cloudfront.net",
tree,
)?;
cargo install --git https://github.com/helius-labs/zolana --tag v0.1.0-alpha zolana-cli
zolana dev start
use zolana_client::{SolanaRpc, ZolanaClient};
use zolana_interface::pda;
let tree = pda::tree(0);
let client = ZolanaClient::from_urls(
SolanaRpc::new("http://127.0.0.1:8899"),
"http://127.0.0.1:8784",
"http://127.0.0.1:3001",
tree,
)?;
2
Retirar a un saldo público
use zolana_interface::instruction::{
Transact, TransactInterfaceTransferAccounts, TransactSolTransferAccounts,
};
use zolana_transaction::{
instructions::{
transact::{ConfidentialTransfer, SettlementTarget},
types::SppProofInputUtxo,
},
SOL_MINT,
};
let withdrawal_utxo = sender_balances_after_transfer
.get_balance(SOL_MINT)
// SPL: .get_balance(spl.mint)
.and_then(|balance| balance.utxos.first())
.expect("failed to fetch sender's utxo")
.clone();
let withdrawal_input_utxo = SppProofInputUtxo::new(withdrawal_utxo, &sender);
let mut withdrawal = ConfidentialTransfer::new(
sender_shielded_address,
vec![withdrawal_input_utxo],
sender.pubkey(),
);
withdrawal.withdraw(
SOL_MINT,
WITHDRAW_AMOUNT,
SettlementTarget::Sol {
user_sol_account: sender.pubkey(),
},
)?;
// SPL: withdrawal.withdraw(
// SPL: spl.mint,
// SPL: WITHDRAW_AMOUNT,
// SPL: SettlementTarget::Spl {
// SPL: user_spl_token: spl.user_token_account,
// SPL: spl_token_interface: spl.vault,
// SPL: },
// SPL: )?;
let proof_inputs = withdrawal.sign(&sender, &assets)?;
let withdrawal_data = client.prove_transact(tree, proof_inputs, None)?;
let withdraw_ix = Transact {
payer: sender.pubkey(),
input_trees: vec![tree],
output_tree: tree,
owner_signers: Vec::new(),
interface_transfer_accounts: vec![TransactInterfaceTransferAccounts::Sol(
TransactSolTransferAccounts {
recipient: sender.pubkey(),
},
)],
// SPL: interface_transfer_accounts: vec![
// SPL: TransactInterfaceTransferAccounts::SplWithdrawal(
// SPL: zolana_interface::instruction::TransactSplWithdrawalAccounts {
// SPL: mint: spl.mint,
// SPL: vault: spl.vault,
// SPL: user_token_account: spl.user_token_account,
// SPL: token_program: spl.token_program,
// SPL: },
// SPL: ),
// SPL: ],
data: withdrawal_data,
}
.instruction();
1. Select private token accounts to spend
1. Select private token accounts to spend
use zolana_transaction::SOL_MINT;
let withdrawal_utxo = sender_balances_after_transfer
.get_balance(SOL_MINT)
// SPL: .get_balance(spl.mint)
.and_then(|balance| balance.utxos.first())
.expect("failed to fetch sender's utxo")
.clone();
- El ejemplo gasta la cuenta de tokens privada de Solana restante después de la transferencia anterior. Un retiro puede gastar varios UTXO.
withdrawal_utxoes el primer UTXO disponible para gastar de ese activo. El comentario// SPL:muestraget_balance(spl.mint).
2. Prepare proof inputs
2. Prepare proof inputs
use zolana_transaction::instructions::types::SppProofInputUtxo;
let withdrawal_input_utxo = SppProofInputUtxo::new(withdrawal_utxo, &sender);
SppProofInputUtxo::newprepara el UTXO seleccionado como entrada de la prueba con el par de claves de la billetera privada del remitente.- El par de claves deriva el anulador que marca el UTXO de entrada como gastado, mientras que el activo y el monto de entrada permanecen cifrados.
3. Build and sign the withdrawal
3. Build and sign the withdrawal
use zolana_transaction::{
instructions::transact::{ConfidentialTransfer, SettlementTarget},
SOL_MINT,
};
let mut withdrawal = ConfidentialTransfer::new(
sender_shielded_address,
vec![withdrawal_input_utxo],
sender.pubkey(),
);
withdrawal.withdraw(
SOL_MINT,
WITHDRAW_AMOUNT,
SettlementTarget::Sol {
user_sol_account: sender.pubkey(),
},
)?;
// SPL: withdrawal.withdraw(
// SPL: spl.mint,
// SPL: WITHDRAW_AMOUNT,
// SPL: SettlementTarget::Spl {
// SPL: user_spl_token: spl.user_token_account,
// SPL: spl_token_interface: spl.vault,
// SPL: },
// SPL: )?;
let proof_inputs = withdrawal.sign(&sender, &assets)?;
sender_shielded_addresses la del remitente. El retiro gasta fondos de esta billetera.vec![withdrawal_input_utxo]enumera los UTXO seleccionados del remitente. Un retiro puede gastar varios UTXO.sender.pubkey()es el pagador de la comisión de la transacción. Un patrocinador de gas puede pagar la comisión.SettlementTarget::Soles el destinatario público de Solana. El destinatario puede ser el propietario o un tercero. El comentario// SPL:muestraSettlementTarget::Spl.SOL_MINTselecciona SOL. El comentario// SPL:muestra la dirección de acuñación del token para activos SPL y Token 2022.WITHDRAW_AMOUNTse expresa en las unidades base del activo. SOL usa lamports. Los activos SPL y Token 2022 usan las unidades base del token.withdrawal.signautoriza la transición de estado, cifra el cambio privado restante y genera las entradas para el probador de conocimiento cero.assetses el registro de activos utilizado para resolver los activos privados compatibles.
4. Fetch the zero-knowledge proof
4. Fetch the zero-knowledge proof
use zolana_client::Rpc;
let withdrawal_data = client.prove_transact(tree, proof_inputs, None)?;
client.prove_transactgenera la prueba de conocimiento cero a partir del retiro firmado y devuelve los datos serializados de la instrucción.treeidentifica el árbol de Merkle de estado cuya raíz se utiliza para demostrar la pertenencia del UTXO de entrada.- La prueba demuestra que el remitente posee las entradas y puede gastarlas. El activo y el monto retirados son públicos. Los montos de entrada y el cambio permanecen cifrados.
5. Build the withdrawal instruction
5. Build the withdrawal instruction
use zolana_interface::instruction::{
Transact, TransactInterfaceTransferAccounts, TransactSolTransferAccounts,
};
let withdraw_ix = Transact {
payer: sender.pubkey(),
input_trees: vec![tree],
output_tree: tree,
owner_signers: Vec::new(),
interface_transfer_accounts: vec![TransactInterfaceTransferAccounts::Sol(
TransactSolTransferAccounts {
recipient: sender.pubkey(),
},
)],
// SPL: interface_transfer_accounts: vec![
// SPL: TransactInterfaceTransferAccounts::SplWithdrawal(
// SPL: zolana_interface::instruction::TransactSplWithdrawalAccounts {
// SPL: mint: spl.mint,
// SPL: vault: spl.vault,
// SPL: user_token_account: spl.user_token_account,
// SPL: token_program: spl.token_program,
// SPL: },
// SPL: ),
// SPL: ],
data: withdrawal_data,
}
.instruction();
payerfirma y paga la transacción de Solana. Un patrocinador de gas puede pagar la comisión.input_treesidentifica el árbol de Merkle de estado que contiene los UTXO gastados.output_treeidentifica el árbol de Merkle de estado que recibe el compromiso del cambio privado del remitente.interface_transfer_accountsproporcionaTransactInterfaceTransferAccounts::Sol, el destinatario público de Solana. El comentario// SPL:muestraSplWithdrawal.owner_signersestá vacío para este retiro confidencial.datacontiene la prueba de conocimiento cero y el cambio cifrado generados en el paso anterior.
6. Send like any Solana transaction
6. Send like any Solana transaction
use zolana_client::Rpc;
let signature = client.create_and_send_transaction(
&[withdraw_ix],
sender.pubkey(),
&[&sender],
client.compute_budget(),
)?;
let slot = landed_slot(&client, signature)?;
create_and_send_transactionfirma y envíawithdraw_ixcomo una transacción de Solana.landed_slotlee el slot de confirmación utilizado para controlar la consulta al indexador.senderpaga la comisión y autoriza el retiro.
Ejemplo de código completo
Clona y ejecuta el ejemplo:git clone https://github.com/helius-labs/zolana-examples.git
cd zolana-examples/rust-client
cargo run -p rust-client-example --example deposit_transfer_withdraw
Los ejemplos usan un Ring confidencial en local/devnet aquí.
deposit_transfer_withdraw.rs
use anyhow::{anyhow, Result};
use rust_client_example::{cli_keypair, setup, SetupContext};
use solana_keypair::Keypair;
use solana_signature::Signature;
use solana_signer::Signer;
use zolana_client::{IndexerRpcConfig, Rpc, SolanaRpc, ZolanaClient};
use zolana_interface::instruction::{
AssetDeposit, Deposit, DepositAsset, Transact, TransactInterfaceTransferAccounts,
TransactSolTransferAccounts,
};
use zolana_keypair::ShieldedKeypair;
use zolana_transaction::{
decrypt_transactions,
instructions::{
transact::{ConfidentialTransfer, SettlementTarget},
types::SppProofInputUtxo,
},
AssetRegistry, SOL_MINT,
};
const DEPOSIT_AMOUNT: u64 = 10_000_000;
const TRANSFER_AMOUNT: u64 = 3_000_000;
const WITHDRAW_AMOUNT: u64 = 3_000_000;
fn main() -> Result<()> {
let SetupContext {
rpc_url,
indexer_url,
prover_url,
tree,
} = setup()?;
// Connect to the RPC, indexer, and prover.
let client = ZolanaClient::from_urls(SolanaRpc::new(rpc_url), &indexer_url, prover_url, tree)?;
// Mints that are registered with Solana Rings for privacy.
let assets = AssetRegistry::default();
// SPL: assets.insert(spl.asset_id, spl.mint)?;
// Initialize the sender's private wallet and local authority
// to decrypt transactions and sync balances.
// The Solana signer and private wallet are derived from the same Ed25519 seed.
let sender = ShieldedKeypair::from_keypair(&cli_keypair()?)?;
let recipient = ShieldedKeypair::from_keypair(&Keypair::new())?;
let sender_shielded_address = sender.shielded_address()?;
// Deposit SOL into the sender's private balance.
// A deposit from a public balance reveals
// sender, recipient, asset and amount.
// Alternatively, you can onramp fiat directly to a private balance.
// 1. Move public SOL into the sender's private balance.
let sender_balances_after_deposit = {
let deposit_ix = Deposit {
tree,
depositor: sender.pubkey(),
deposits: vec![AssetDeposit {
asset: DepositAsset::Sol,
// SPL: asset: DepositAsset::Spl(zolana_interface::instruction::DepositSplAccounts {
// SPL: mint: spl.mint,
// SPL: user_token: spl.user_token_account,
// SPL: token_program: spl.token_program,
// SPL: }),
view_tag: sender_shielded_address.confidential_view_tag()?,
owner: sender_shielded_address.owner_hash()?,
amount: DEPOSIT_AMOUNT,
utxo_data: None,
memo: None,
}],
}
.instruction()?;
// 2. Send and confirm like any Solana transaction; the landed slot gates
// the indexer fetch below.
let signature = client.create_and_send_transaction(
&[deposit_ix],
sender.pubkey(),
&[&sender],
client.compute_budget(),
)?;
let slot = landed_slot(&client, signature)?;
// 3. Fetch transaction outputs from the indexer, gated on the deposit's slot.
// The indexer returns encrypted outputs by transaction signature.
let response = client.get_shielded_transactions_by_signature(
signature,
Some(IndexerRpcConfig::at_slot(slot)),
)?;
let transactions = response
.transactions
.into_iter()
.map(|indexed| indexed.transaction)
.collect::<Vec<_>>();
// 4. The sender decrypts the transaction outputs locally to update the private balance.
let balances = decrypt_transactions(&sender, &transactions, &assets)
.map_err(|e| anyhow!("decrypt sender transactions: {e:?}"))?;
let sender_balance = balances
.get_balance(SOL_MINT)
// SPL: .get_balance(spl.mint)
.expect("failed to fetch sender's utxo");
assert_eq!(sender_balance.amount, DEPOSIT_AMOUNT);
assert_eq!(sender_balance.utxos.len(), 1);
balances
};
// Confidential SOL transfer to the recipient's private balance.
// A confidential transfer reveals only sender and recipient,
// not the asset or amount.
let sender_balances_after_transfer = {
// 1. Select UTXOs that make up the private balance for the transfer.
let transfer_utxo = sender_balances_after_deposit
.get_balance(SOL_MINT)
// SPL: .get_balance(spl.mint)
.and_then(|balance| balance.utxos.first())
.expect("failed to fetch deposited utxo")
.clone();
// 2. Prepare the selected UTXOs as inputs for the zero-knowledge proof.
let transfer_input_utxo = SppProofInputUtxo::new(transfer_utxo, &sender);
// 3. Build and sign the confidential transfer.
// Signing encrypts the asset and amount and produces the proof inputs for the ZK prover.
let mut transfer = ConfidentialTransfer::new(
sender_shielded_address,
vec![transfer_input_utxo],
sender.pubkey(),
);
transfer.send(&recipient.shielded_address()?, SOL_MINT, TRANSFER_AMOUNT)?;
// SPL: transfer.send(&recipient.shielded_address()?, spl.mint, TRANSFER_AMOUNT)?;
let proof_inputs = transfer.sign(&sender, &assets)?;
// 4. Fetch the zk proof to prove the sender can spend the balance without revealing asset and amount.
let transfer_data = client.prove_transact(tree, proof_inputs, None)?;
// 5. Construct the instruction.
let transfer_ix = Transact {
payer: sender.pubkey(),
input_trees: vec![tree],
output_tree: tree,
owner_signers: Vec::new(),
interface_transfer_accounts: Vec::new(),
data: transfer_data,
}
.instruction();
// 6. Send and confirm like any Solana transaction; confirmation yields the landed slot.
let signature = client.create_and_send_transaction(
&[transfer_ix],
sender.pubkey(),
&[&sender],
client.compute_budget(),
)?;
let slot = landed_slot(&client, signature)?;
// 7. Fetch the sender's UTXOs from this transaction, gated on the transfer's slot,
// and read the remaining private balance.
let response = client.get_shielded_transactions_by_signature(
signature,
Some(IndexerRpcConfig::at_slot(slot)),
)?;
let transactions = response
.transactions
.into_iter()
.map(|indexed| indexed.transaction)
.collect::<Vec<_>>();
let sender_balances = decrypt_transactions(&sender, &transactions, &assets)
.map_err(|e| anyhow!("decrypt sender transactions: {e:?}"))?;
let sender_balance = sender_balances
.get_balance(SOL_MINT)
// SPL: .get_balance(spl.mint)
.expect("failed to fetch sender's utxo");
assert_eq!(sender_balance.amount, DEPOSIT_AMOUNT - TRANSFER_AMOUNT);
assert_eq!(sender_balance.utxos.len(), 1);
sender_balances
};
// Withdraw SOL back to the sender's public balance.
// A withdrawal from a confidential balance reveals
// sender, recipient, asset and amount.
{
// 1. Select UTXOs that make up the private balance for the withdrawal.
let withdrawal_utxo = sender_balances_after_transfer
.get_balance(SOL_MINT)
// SPL: .get_balance(spl.mint)
.and_then(|balance| balance.utxos.first())
.expect("failed to fetch sender's utxo")
.clone();
// 2. Prepare the selected UTXOs as inputs for the zero-knowledge proof.
let withdrawal_input_utxo = SppProofInputUtxo::new(withdrawal_utxo, &sender);
// 3. Build and sign the confidential withdrawal.
// Signing encrypts the private change and produces the ZK prover inputs.
let mut withdrawal = ConfidentialTransfer::new(
sender_shielded_address,
vec![withdrawal_input_utxo],
sender.pubkey(),
);
withdrawal.withdraw(
SOL_MINT,
WITHDRAW_AMOUNT,
SettlementTarget::Sol {
user_sol_account: sender.pubkey(),
},
)?;
// SPL: withdrawal.withdraw(
// SPL: spl.mint,
// SPL: WITHDRAW_AMOUNT,
// SPL: SettlementTarget::Spl {
// SPL: user_spl_token: spl.user_token_account,
// SPL: spl_token_interface: spl.vault,
// SPL: },
// SPL: )?;
let proof_inputs = withdrawal.sign(&sender, &assets)?;
// 4. Fetch the ZK proof to prove the sender can spend the balance.
let withdrawal_data = client.prove_transact(tree, proof_inputs, None)?;
// 5. Combine the proof and withdrawal accounts in a single instruction.
let withdraw_ix = Transact {
payer: sender.pubkey(),
input_trees: vec![tree],
output_tree: tree,
owner_signers: Vec::new(),
interface_transfer_accounts: vec![TransactInterfaceTransferAccounts::Sol(
TransactSolTransferAccounts {
recipient: sender.pubkey(),
},
)],
// SPL: interface_transfer_accounts: vec![
// SPL: TransactInterfaceTransferAccounts::SplWithdrawal(
// SPL: zolana_interface::instruction::TransactSplWithdrawalAccounts {
// SPL: mint: spl.mint,
// SPL: vault: spl.vault,
// SPL: user_token_account: spl.user_token_account,
// SPL: token_program: spl.token_program,
// SPL: },
// SPL: ),
// SPL: ],
data: withdrawal_data,
}
.instruction();
// 6. Send and confirm like any Solana transaction.
let signature = client.create_and_send_transaction(
&[withdraw_ix],
sender.pubkey(),
&[&sender],
client.compute_budget(),
)?;
let slot = landed_slot(&client, signature)?;
// 7. Fetch the sender's UTXOs from this transaction, gated on the withdrawal's slot,
// and read the remaining private balance.
let response = client.get_shielded_transactions_by_signature(
signature,
Some(IndexerRpcConfig::at_slot(slot)),
)?;
let transactions = response
.transactions
.into_iter()
.map(|indexed| indexed.transaction)
.collect::<Vec<_>>();
let sender_balances = decrypt_transactions(&sender, &transactions, &assets)
.map_err(|e| anyhow!("decrypt sender transactions: {e:?}"))?;
let sender_balance = sender_balances
.get_balance(SOL_MINT)
// SPL: .get_balance(spl.mint)
.expect("failed to fetch sender's utxo");
assert_eq!(
sender_balance.amount,
DEPOSIT_AMOUNT - TRANSFER_AMOUNT - WITHDRAW_AMOUNT
);
assert_eq!(sender_balance.utxos.len(), 1);
// 8. Read remaining private balance and the public SOL balance.
let solana_balance = client.get_balance(sender.pubkey())?;
println!("withdraw solana_balance={solana_balance} tx={signature}");
// SPL: println!(
// SPL: "withdraw user_token={} tx={signature}",
// SPL: spl.user_token_account,
// SPL: );
}
Ok(())
}
/// Slot the confirmed transaction landed in, which drives the indexer
/// freshness gate on the fetches that read the transaction back.
fn landed_slot(client: &ZolanaClient<SolanaRpc>, signature: Signature) -> Result<u64> {
client
.get_signature_statuses(vec![signature])?
.first()
.and_then(|status| status.as_ref())
.map(|status| status.slot)
.ok_or_else(|| anyhow!("transaction status missing after confirmation"))
}