- 出金はトークンをプライベートバランスからパブリックSolanaバランスに移動させます。
- 出金は、単一のSolanaトランザクションでSolanaウォレットアドレスに送信されます。
出金: プライベートとは何か
| 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 |
パーミッションレスのリングは暗号化された金額および資産で機密性があります。
カスタムリングは、機密または匿名(送信者、受信者、資産、および金額を暗号化)として構成できます。
出金の仕組み
出金は、パブリックなSolana転送と同様に動作します:- ユーザーのSOLまたはSPLバランスはオンチェーンで暗号化されます。
-
ユーザーはプライベートステートを復号し、ウォレットが出金を構築し、所有者が署名します。
- 専用のRPCメソッドで暗号化された状態を取得します。ユーザーだけがローカルでバランスを復号できます。
- ウォレットは金額と受取人を設定し、ZK証明を要求します。RPCプロバイダーはデフォルトでZK証明を生成し、それを返します。
- Solanaランタイムは署名を検証し、Solanaプライバシープログラムを起動して、暗号化された状態を開示せずにZK証明を検証します。
- アプリはSolanaトランザクションハッシュを介してステータスを追跡します。
Solana転送と比較
Solana転送と比較
- ユーザーのSOLまたはSPLバランスはオンチェーンで公開されています。
- ウォレットはパブリックステートを読み取り、転送を構築し、所有者が署名します。
- Solanaランタイムは署名を検証し、システムプログラムまたはトークンプログラムを呼び出し、公開バランスを更新します。
- アプリはSolanaトランザクションハッシュを介してステータスを追跡します。
これは、パーミッションレスの機密リングの高レベルのトランザクションフローです。
conceptsでカスタムリングと比較してください。
始めに
- TypeScriptクライアント
- Rustクライアント
1
前提条件
The TypeScript examples require Node.js 24 or later, pnpm 11.18.0, and the Solana CLI.
pnpm add @heliuslabs/zolana @solana/kit
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: "http://zolnet-devnet-1779374825.eu-north-1.elb.amazonaws.com",
proverUrl: "http://zolnet-devnet-1779374825.eu-north-1.elb.amazonaws.com:3001",
allowInsecureHttp: true,
});
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
パブリックバランスへの出金
Solanaキット送信ヘルパー
Solanaキット送信ヘルパー
import {
appendTransactionMessageInstructions,
assertIsTransactionWithBlockhashLifetime,
createTransactionMessage,
getSignatureFromTransaction,
pipe,
sendTransactionWithoutConfirmingFactory,
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: 0 }),
(message) => setTransactionMessageFeePayerSigner(feePayer, message),
(message) =>
setTransactionMessageLifetimeUsingBlockhash(lifetime, 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 };
};
}
- SDKは命令を返します。アプリがそれを署名して送信します。
sendAndConfirmFactoryはキットトランザクションを構築し、それを提出し、署名とランディングスロットを返します。
- SOL
- SPL
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,
senderKeypair,
);
const withdrawal = new ConfidentialTransfer(
senderAddress,
[withdrawalInput],
senderSigner.address,
);
withdrawal.withdraw(
SOL_MINT,
WITHDRAW_AMOUNT,
WithdrawalTarget.sol({
recipient: senderSigner.address,
}),
);
const withdrawalProofInputs = withdrawal.sign(
senderKeypair,
assets,
);
const withdrawalData =
await client.proveTransact(
withdrawalProofInputs,
);
const withdrawalInstruction =
transactInstruction({
payer: senderSigner,
inputTree: client.tree,
outputTree: client.tree,
withdrawal: TransactWithdrawal.sol({
recipient: senderSigner.address,
}),
data: withdrawalData,
});
const withdrawalTx = await sendAndConfirm([
withdrawalInstruction,
]);
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,
senderKeypair,
);
const withdrawal = new ConfidentialTransfer(
senderAddress,
[withdrawalInput],
senderSigner.address,
);
withdrawal.withdraw(
spl.mint,
WITHDRAW_AMOUNT,
WithdrawalTarget.spl({
userTokenAccount: spl.userTokenAccount,
splTokenInterface: spl.splTokenInterface,
vaultBump: spl.vaultBump,
}),
);
const withdrawalProofInputs = withdrawal.sign(
senderKeypair,
assets,
);
const withdrawalData =
await client.proveTransact(
withdrawalProofInputs,
);
const withdrawalInstruction =
transactInstruction({
payer: senderSigner,
inputTree: client.tree,
outputTree: client.tree,
withdrawal: TransactWithdrawal.spl({
mint: spl.mint,
splTokenInterface: spl.splTokenInterface,
userTokenAccount: spl.userTokenAccount,
tokenProgram: spl.tokenProgram,
}),
data: withdrawalData,
});
const withdrawalTx = await sendAndConfirm([
withdrawalInstruction,
]);
1. 支出するプライベートトークンアカウントを選択
1. 支出するプライベートトークンアカウントを選択
import { SOL_MINT } from "@heliuslabs/zolana";
const withdrawalUtxo =
transferBalance.utxos[0]!;
- この例では、前の転送後に残ったプライベートSolanaトークンアカウントを使って支出します。出金は複数のUTXOを支出できます。
withdrawalUtxoはそのバランスから1つのプライベートSolanaトークンアカウントを選択します。
2. 証明入力の準備
2. 証明入力の準備
import { ProofInputUtxo } from "@heliuslabs/zolana/transaction";
const withdrawalInput =
ProofInputUtxo.fromKeypair(
withdrawalUtxo,
senderKeypair,
);
ProofInputUtxo.fromKeypair選択されたUTXOを送信者のプライベートウォレットキーペアで証明入力として準備します。- キーペアは、入力UTXOを支出済みとしてマークするナリファイアを導き出しますが、入力資産と金額は暗号化されたままです。
3. 出金の構築と署名
3. 出金の構築と署名
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(
senderKeypair,
assets,
);
senderAddressは送信者のシールドアドレスです。出金はこのウォレットから支出されます。[withdrawalInput]は送信者の選択されたUTXOをリストします。出金は複数のUTXOを支出できます。senderSigner.addressは手数料支払い者のSolanaアドレスです。ガススポンサーが機密出金でこの役割を果たすことができます。WithdrawalTarget.solは公開Solana受取人です。受取人は所有者または第三者である可能性があります。SOL_MINTはSOLを選択します。SPLまたはトークン2022の出金はトークンミントを渡します。WITHDRAW_AMOUNTは資産の基本単位で計上されます。SOLはラマポートを使用します。SPLおよびトークン2022資産はトークンの基本単位を使用します。withdrawal.signは状態遷移を承認し、残りのプライベートチェンジを暗号化し、ゼロ知識証明者の入力を生成します。assetsはサポートされているプライベート資産を解決するために使用する資産レジストリです。
4. ゼロ知識証明の取得
4. ゼロ知識証明の取得
import { createZolanaClient } from "@heliuslabs/zolana";
const withdrawalData =
await client.proveTransact(
withdrawalProofInputs,
);
client.proveTransactは署名された出金からゼロ知識証明を生成し、シリアル化された命令データを返します。- 証明は、送信者が入力を所有し、支出できることを示します。引き出された資産と金額は公開されます。入力金額とチェンジは暗号化されたままです。
5. 出金命令の作成
5. 出金命令の作成
import {
transactInstruction,
TransactWithdrawal,
} from "@heliuslabs/zolana/interface";
const withdrawalInstruction =
transactInstruction({
payer: senderSigner,
inputTree: client.tree,
outputTree: client.tree,
withdrawal: TransactWithdrawal.sol({
recipient: senderSigner.address,
}),
data: withdrawalData,
});
payerはSolanaトランザクションの署名と支払いを行います。ガススポンサーが機密出金でこの役割を果たせます。inputTreeとoutputTreeはclient.treeで、支出されたUTXOを含み、送信者のプライベートチェンジを受け取る状態Merkleツリーです。withdrawalはTransactWithdrawal.solで、公開Solana受取人アカウントを供給します。dataは前のステップで生成されたゼロ知識証明と暗号化されたチェンジを含んでいます。- 出金は資産をプライベートバランスから公開Solanaアカウントに移動させます。それは公開受取人アカウントを渡します。
6. 任意のSolanaトランザクションとして送信
6. 任意のSolanaトランザクションとして送信
import { sendAndConfirmFactory } from "../src/lib.js";
const withdrawalTx = await sendAndConfirm([
withdrawalInstruction,
]);
sendAndConfirmは署名し、withdrawalInstructionをSolanaトランザクションとして送信します。- 確認はインデクサ取得をゲートするのに使用されるランディングスロットをもたらします。
完全なコード例
例をクローンして実行します:git clone https://github.com/helius-labs/zolana-examples.git
cd zolana-examples/typescript-client
pnpm install
pnpm example examples/deposit_transfer_withdraw.ts
この例はローカル/Devnetでの機密Ringを使用しています ここ。
deposit_transfer_withdraw.ts
import {
SOL_MINT,
createZolanaClient,
} from "@heliuslabs/zolana";
import { atSlot } from "@heliuslabs/zolana/client";
import {
depositInstruction,
transactInstruction,
DepositAsset,
TransactWithdrawal,
} from "@heliuslabs/zolana/interface";
import { randomBlinding } from "@heliuslabs/zolana/keypair";
import {
AssetRegistry,
ConfidentialTransfer,
ProofInputUtxo,
decryptToBalances,
WithdrawalTarget,
} from "@heliuslabs/zolana/transaction";
import {
sendAndConfirmFactory,
setup,
} from "../src/lib.js";
const DEPOSIT_AMOUNT = 1_000_000_000n;
const TRANSFER_AMOUNT = 300_000_000n;
const WITHDRAW_AMOUNT = 300_000_000n;
async function main(): Promise<void> {
const {
sender: senderKeypair,
recipient: recipientKeypair,
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 senderSigner =
senderKeypair.toSolanaSigner();
const senderAddress =
senderKeypair.shieldedAddress();
const recipient =
recipientKeypair.shieldedAddress();
// 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(),
blinding: randomBlinding(),
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 view tag.
const depositResponse =
await client.getShieldedTransactionsByTags(
{ tags: [senderViewTag] },
atSlot(depositTx.slot),
);
// 4. The sender decrypts the transaction outputs locally to read the private balance.
const balancesAfterDeposit =
await decryptToBalances({
keypair: senderKeypair,
registry: assets,
transactions: depositResponse.transactions,
});
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,
senderKeypair,
);
// 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,
SOL_MINT,
TRANSFER_AMOUNT,
);
const transferProofInputs = transfer.sign(
senderKeypair,
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,
);
// 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 = 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 again, gated on the transfer's slot,
// and read the remaining private balance.
const transferResponse =
await client.getShieldedTransactionsByTags(
{ tags: [senderViewTag] },
atSlot(transferTx.slot),
);
const balancesAfterTransfer =
await decryptToBalances({
keypair: senderKeypair,
registry: assets,
transactions: transferResponse.transactions,
});
const transferBalance =
balancesAfterTransfer.balance(SOL_MINT);
if (
transferBalance.amount !==
DEPOSIT_AMOUNT - TRANSFER_AMOUNT
) {
throw new Error(
`expected remaining amount ${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,
senderKeypair,
);
// 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(
senderKeypair,
assets,
);
// 4. Fetch the ZK proof to prove the sender can spend the balance.
const withdrawalData =
await client.proveTransact(
withdrawalProofInputs,
);
// 5. Build the instruction with the state Merkle tree and Solana accounts required for the withdrawal.
const withdrawalInstruction =
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 again, gated on the withdrawal's slot,
// and read the remaining private balance.
const withdrawalResponse =
await client.getShieldedTransactionsByTags(
{ tags: [senderViewTag] },
atSlot(withdrawalTx.slot),
);
const balancesAfterWithdrawal =
await decryptToBalances({
keypair: senderKeypair,
registry: assets,
transactions:
withdrawalResponse.transactions,
});
const withdrawalBalance =
balancesAfterWithdrawal.balance(SOL_MINT);
if (
withdrawalBalance.amount !==
DEPOSIT_AMOUNT -
TRANSFER_AMOUNT -
WITHDRAW_AMOUNT
) {
throw new Error(
`expected remaining amount ${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
前提条件
The Rust examples require the latest stable Rust toolchain 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.1.0-alpha", features = ["indexer-api", "solana-rpc"] }
zolana-interface = { git = "https://github.com/helius-labs/zolana", tag = "v0.1.0-alpha", features = ["solana"] }
zolana-keypair = { git = "https://github.com/helius-labs/zolana", tag = "v0.1.0-alpha" }
zolana-transaction = { git = "https://github.com/helius-labs/zolana", tag = "v0.1.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 solana_address::Address;
use zolana_client::{SolanaRpc, ZolanaClient};
use zolana_interface::DEFAULT_TREE_ADDRESS;
let tree: Address = DEFAULT_TREE_ADDRESS.parse()?;
let client = ZolanaClient::from_urls_allowing_insecure_http(
SolanaRpc::new("https://devnet.helius-rpc.com/?api-key=YOUR_API_KEY"),
"http://zolnet-devnet-1779374825.eu-north-1.elb.amazonaws.com",
"http://zolnet-devnet-1779374825.eu-north-1.elb.amazonaws.com:3001",
tree,
);
cargo install --git https://github.com/helius-labs/zolana --tag v0.1.0-alpha zolana-cli
zolana dev start
use solana_address::Address;
use zolana_client::{SolanaRpc, ZolanaClient};
use zolana_interface::DEFAULT_TREE_ADDRESS;
let tree: Address = DEFAULT_TREE_ADDRESS.parse()?;
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
パブリックバランスへの出金
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_solana_keypair.pubkey(),
);
withdrawal.withdraw(
SOL_MINT,
WITHDRAW_AMOUNT,
SettlementTarget::Sol {
user_sol_account: sender_solana_keypair.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_solana_keypair.pubkey(),
input_tree: tree,
output_tree: tree,
owner_signers: Vec::new(),
interface_transfer_accounts: vec![TransactInterfaceTransferAccounts::Sol(
TransactSolTransferAccounts {
recipient: sender_solana_keypair.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. 支出するプライベートトークンアカウントを選択
1. 支出するプライベートトークンアカウントを選択
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();
- この例では、前の転送後に残ったプライベートSolanaトークンアカウントを使って支出します。出金は複数のUTXOを支出できます。
withdrawal_utxoはその資産の最初の支出可能なUTXOです。// SPL:コメントはget_balance(spl.mint)を示しています。
2. 証明入力の準備
2. 証明入力の準備
use zolana_transaction::instructions::types::SppProofInputUtxo;
let withdrawal_input_utxo = SppProofInputUtxo::new(withdrawal_utxo, &sender);
SppProofInputUtxo::new選択されたUTXOを送信者のプライベートウォレットキーペアで証明入力として準備します。- キーペアは、入力UTXOを支出済みとしてマークするナリファイアを導き出し、入力資産と金額は暗号化されたままです。
3. 出金の構築と署名
3. 出金の構築と署名
use zolana_transaction::{
instructions::transact::{ConfidentialTransfer, SettlementTarget},
SOL_MINT,
};
let mut withdrawal = ConfidentialTransfer::new(
sender_shielded_address,
vec![withdrawal_input_utxo],
sender_solana_keypair.pubkey(),
);
withdrawal.withdraw(
SOL_MINT,
WITHDRAW_AMOUNT,
SettlementTarget::Sol {
user_sol_account: sender_solana_keypair.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_addressは送信者のシールドアドレスです。出金はこのウォレットから支出されます。vec![withdrawal_input_utxo]は送信者の選択されたUTXOをリストします。出金は複数のUTXOを支出できます。sender_solana_keypair.pubkey()はトランザクション手数料支払い者です。ガススポンサーが機密出金でこの役割を果たせます。SettlementTarget::Solは公開Solana受取人です。受取人は所有者または第三者である場合があります。// SPL:コメントはSettlementTarget::Splを示しています。SOL_MINTはSOLを選択します。// SPL:コメントはSPLおよびトークン2022資産のトークンミントを示しています。WITHDRAW_AMOUNTは資産の基本単位で計上されます。SOLはラマポートを使用します。SPLおよびトークン2022資産はトークンの基本単位を使用します。withdrawal.signは状態遷移を承認し、残りのプライベートチェンジを暗号化し、ゼロ知識証明者の入力を生成します。assetsはサポートされているプライベート資産を解決するための資産レジストリです。
4. ゼロ知識証明の取得
4. ゼロ知識証明の取得
use zolana_client::Rpc;
let withdrawal_data = client.prove_transact(tree, proof_inputs, None)?;
client.prove_transactは署名された出金からゼロ知識証明を生成し、シリアル化された命令データを返します。treeはルートを使用して入力UTXOメンバーシップを証明する状態Merkleツリーを識別します。- 証明は、送信者が入力を所有し、支出できることを示します。引き出された資産と金額は公開されます。入力金額とチェンジは暗号化されたままです。
5. 出金命令の作成
5. 出金命令の作成
use zolana_interface::instruction::{
Transact, TransactInterfaceTransferAccounts, TransactSolTransferAccounts,
};
let withdraw_ix = Transact {
payer: sender_solana_keypair.pubkey(),
input_tree: tree,
output_tree: tree,
owner_signers: Vec::new(),
interface_transfer_accounts: vec![TransactInterfaceTransferAccounts::Sol(
TransactSolTransferAccounts {
recipient: sender_solana_keypair.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();
payerはSolanaトランザクションの署名と支払いを行います。ガススポンサーが機密出金でこの役割を果たせます。input_treeは支出されたUTXOを含む状態Merkleツリーを識別します。output_treeは送信者のプライベートチェンジへのコミットメントを受け取る状態Merkleツリーを識別します。interface_transfer_accountsはPUBLIC_SOLANA_RECIPIENT_ACCOUNT、公開Solana受取人を提供します。// SPL:コメントはSplWithdrawalを示しています。owner_signersはこの機密出金のために空です。dataは前のステップで生成されたゼロ知識証明と暗号化されたチェンジを含んでいます。
6. 任意のSolanaトランザクションとして送信
6. 任意のSolanaトランザクションとして送信
use zolana_client::Rpc;
let signature = client.create_and_send_transaction(
&[withdraw_ix],
sender_solana_keypair.pubkey(),
&[&sender_solana_keypair],
)?;
let slot = landed_slot(&client, signature)?;
create_and_send_transactionは署名し、withdraw_ixをSolanaトランザクションとして送信します。landed_slotはインデクサ取得をゲートするのに使用される確認スロットを読み取ります。sender_solana_keypairは手数料を支払い、出金を承認します。
完全なコード例
例をクローンして実行します: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
この例はローカル/Devnetでの機密Ringを使用しています ここ。
deposit_transfer_withdraw.rs
use anyhow::{anyhow, Result};
use rust_client_example::{setup, SetupContext};
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::random_blinding;
use zolana_transaction::{
decrypt_transactions,
instructions::{
transact::{ConfidentialTransfer, SettlementTarget},
types::SppProofInputUtxo,
},
AssetRegistry, SOL_MINT,
};
const DEPOSIT_AMOUNT: u64 = 1_000_000_000;
const TRANSFER_AMOUNT: u64 = 300_000_000;
const WITHDRAW_AMOUNT: u64 = 300_000_000;
fn main() -> Result<()> {
let SetupContext {
rpc_url,
indexer_url,
prover_url,
tree,
sender,
recipient_address,
} = setup()?;
// Load the funded fee payer and devnet settings, then connect.
// Photon and the prover are HTTP on this ALB, so the constructor permits that.
let client = ZolanaClient::from_urls_allowing_insecure_http(
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_solana_keypair = sender.to_solana_keypair()?;
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_solana_keypair.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()?,
blinding: random_blinding(),
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_solana_keypair.pubkey(),
&[&sender_solana_keypair],
)?;
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 view tag, the sender's public key in Confidential Rings.
let sender_tag = sender_shielded_address.confidential_view_tag()?;
let response = client.get_shielded_transactions_by_tags(
vec![sender_tag],
None,
Some(50),
Some(IndexerRpcConfig::at_slot(slot)),
)?;
// 4. The sender decrypts the transaction outputs locally to update the private balance.
let balances = decrypt_transactions(&sender, &response.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_solana_keypair.pubkey(),
);
transfer.send(&recipient_address, SOL_MINT, TRANSFER_AMOUNT)?;
// SPL: transfer.send(&recipient_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_solana_keypair.pubkey(),
input_tree: 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_solana_keypair.pubkey(),
&[&sender_solana_keypair],
)?;
let slot = landed_slot(&client, signature)?;
// 7. Sync the sender's wallet, gated on the transfer's slot, and read
// the remaining private balance.
let sender_tag = sender_shielded_address.confidential_view_tag()?;
let response = client.get_shielded_transactions_by_tags(
vec![sender_tag],
None,
Some(50),
Some(IndexerRpcConfig::at_slot(slot)),
)?;
let sender_balances = decrypt_transactions(&sender, &response.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_solana_keypair.pubkey(),
);
withdrawal.withdraw(
SOL_MINT,
WITHDRAW_AMOUNT,
SettlementTarget::Sol {
user_sol_account: sender_solana_keypair.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_solana_keypair.pubkey(),
input_tree: tree,
output_tree: tree,
owner_signers: Vec::new(),
interface_transfer_accounts: vec![TransactInterfaceTransferAccounts::Sol(
TransactSolTransferAccounts {
recipient: sender_solana_keypair.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_solana_keypair.pubkey(),
&[&sender_solana_keypair],
)?;
let slot = landed_slot(&client, signature)?;
// 7. Sync the sender's wallet, gated on the withdrawal's slot, and read
// the remaining private balance.
let sender_tag = sender_shielded_address.confidential_view_tag()?;
let response = client.get_shielded_transactions_by_tags(
vec![sender_tag],
None,
Some(50),
Some(IndexerRpcConfig::at_slot(slot)),
)?;
let sender_balances = decrypt_transactions(&sender, &response.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_solana_keypair.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"))
}