- 私有转账在私有钱包之间以环的形式移动代币。
- 私有转账是在单个Solana交易中发送到Solana钱包地址的。
转账:什么是私有
- Permissionless Confidential Ring
- Custom Rings
| Field | Visibility | Why |
|---|---|---|
| Asset | Private | The asset is encrypted onchain |
| Amount | Private | The transferred amount is encrypted onchain |
| Source private wallet | Public | In a confidential Ring the source private wallet is visible onchain. |
| Recipient | Public | In a confidential Ring the recipient is visible onchain. |
Transfers from and within a ring reveal the program ID of the custom Ring.A balance in a Custom Ring can exit to an SPL token account, to the Default Ring, or to another Ring, as long as the source Ring’s policy permits it.
The default Ring is permissionless and does not have a policy or authority.One transaction can combine balances from the Default Ring and one Custom Ring. A transfer between two Custom Rings routes through the Default Ring.
| Private transfer | Custom confidential Rings | Custom anonymous Rings | Default confidential to or from Custom confidential | Confidential (Default or Custom) to Custom anonymous | Custom anonymous to confidential (Default or Custom) |
|---|---|---|---|---|---|
| Amount | Private | Private | Private | Private | Private |
| Asset | Private | Private | Private | Private | Private |
| Source private wallet | Public | Private. A relayer submits the transaction, so the public ledger does not reveal the source private wallet. | Public | Public | Private. A relayer submits the transaction, so the public ledger does not reveal the source private wallet. |
| Recipient | Public | Private | Public | Private | Public |
| Custom Ring program ID | Public | Public | Public | Public | Public |
无许可环是机密的,具有加密的金额和资产。
可以将自定义环配置为机密或匿名(加密发送者、接收者、资产和金额)。
转账如何工作
私有转账的行为类似于公共Solana转账:- 用户的SOL或SPL余额是在链上加密的。
-
用户解密私有状态,钱包构建转账,所有者签名。
- 使用专用RPC方法获取加密状态。只有用户可以在本地解密余额。
- 钱包设置金额和接收者,然后请求ZK证明。RPC提供者默认生成ZK证明并返回。
- Solana运行时验证签名并调用Solana Privacy Program,该程序在不泄露加密状态的情况下验证ZK证明。
- 应用程序通过Solana交易哈希跟踪状态。
与Solana转账比较
与Solana转账比较
- 用户的SOL或SPL余额是链上公开的。
- 钱包读取公共状态,构建转账,所有者签名。
- Solana运行时验证签名并调用系统程序或代币程序,更新公共余额。
- 应用程序通过Solana交易哈希跟踪状态。
这是无许可机密环的高层次交易流程。
与概念中的自定义环相比。
开始使用
- TypeScript Client
- Rust Client
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 Kit发送助手
Solana Kit发送助手
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构建Kit交易,提交并返回签名及落地槽位。
- SOL
- SPL
import { SOL_MINT } from "@heliuslabs/zolana";
import { transactInstruction } from "@heliuslabs/zolana/interface";
import {
ConfidentialTransfer,
ProofInputUtxo,
} from "@heliuslabs/zolana/transaction";
import { sendAndConfirmFactory } from "../src/lib.js";
const sendAndConfirm = sendAndConfirmFactory(
client,
senderSigner,
);
const transferUtxo = depositBalance.utxos[0]!;
const transferInput =
ProofInputUtxo.fromKeypair(
transferUtxo,
senderKeypair,
);
const transfer = new ConfidentialTransfer(
senderAddress,
[transferInput],
senderSigner.address,
);
transfer.send(
recipient,
SOL_MINT,
TRANSFER_AMOUNT,
);
const transferProofInputs = transfer.sign(
senderKeypair,
assets,
);
const transferData = await client.proveTransact(
transferProofInputs,
);
const transferInstruction = transactInstruction(
{
payer: senderSigner,
inputTree: client.tree,
outputTree: client.tree,
data: transferData,
},
);
const transferTx = await sendAndConfirm([
transferInstruction,
]);
import { transactInstruction } from "@heliuslabs/zolana/interface";
import {
ConfidentialTransfer,
ProofInputUtxo,
} from "@heliuslabs/zolana/transaction";
import { sendAndConfirmFactory } from "../src/lib.js";
const sendAndConfirm = sendAndConfirmFactory(
client,
senderSigner,
);
const transferUtxo = depositBalance.utxos[0]!;
const transferInput =
ProofInputUtxo.fromKeypair(
transferUtxo,
senderKeypair,
);
const transfer = new ConfidentialTransfer(
senderAddress,
[transferInput],
senderSigner.address,
);
transfer.send(
recipient,
spl.mint,
TRANSFER_AMOUNT,
);
const transferProofInputs = transfer.sign(
senderKeypair,
assets,
);
const transferData = await client.proveTransact(
transferProofInputs,
);
const transferInstruction = transactInstruction(
{
payer: senderSigner,
inputTree: client.tree,
outputTree: client.tree,
data: transferData,
},
);
const transferTx = await sendAndConfirm([
transferInstruction,
]);
1. 选择要花费的私有代币账户
1. 选择要花费的私有代币账户
import { SOL_MINT } from "@heliuslabs/zolana";
const transferUtxo = depositBalance.utxos[0]!;
- 示例花费了由先前存款创建的私有Solana代币账户。转账可以花费多个UTXO。
transferUtxo从该余额中选择一个私有Solana代币账户。
2. 准备证明输入
2. 准备证明输入
import { ProofInputUtxo } from "@heliuslabs/zolana/transaction";
const transferInput =
ProofInputUtxo.fromKeypair(
transferUtxo,
senderKeypair,
);
ProofInputUtxo.fromKeypair使用发送方的私有钱包密钥对准备选定的UTXO作为证明输入。- 密钥对派生出无效器,标记输入UTXO为已用,而资产和金额保持加密。
3. 构建和签署机密转账
3. 构建和签署机密转账
import { SOL_MINT } from "@heliuslabs/zolana";
import { ConfidentialTransfer } from "@heliuslabs/zolana/transaction";
const transfer = new ConfidentialTransfer(
senderAddress,
[transferInput],
senderSigner.address,
);
transfer.send(
recipient,
SOL_MINT,
TRANSFER_AMOUNT,
);
const transferProofInputs = transfer.sign(
senderKeypair,
assets,
);
senderAddress是发送者的。转账从此钱包中花费。[transferInput]列出发送者选择的UTXO。转账可以花费多个UTXO。senderSigner.address是费用支付者的Solana地址。燃气赞助者可以为机密转账担任此角色。recipient是接收者的屏蔽地址。转账输出被加密到接收者的查看密钥。SOL_MINT选择SOL。SPL或Token 2022转账传递代币铸造。TRANSFER_AMOUNT以资产的基本单位计价。SOL使用lamports。SPL和Token 2022资产使用代币的基本单位。transfer.sign授权状态转换,加密资产和金额,并生成零知识证明者的输入。assets是用于解析支持的私有资产的资产注册表。
4. 获取零知识证明
4. 获取零知识证明
import { createZolanaClient } from "@heliuslabs/zolana";
const transferData = await client.proveTransact(
transferProofInputs,
);
client.proveTransact从签名的转账中生成零知识证明,并返回序列化的指令数据。- 证明显示发送者拥有并可以花费输入,而不透露加密的资产或金额。
5. 构建转账指令
5. 构建转账指令
import { transactInstruction } from "@heliuslabs/zolana/interface";
const transferInstruction = transactInstruction(
{
payer: senderSigner,
inputTree: client.tree,
outputTree: client.tree,
data: transferData,
},
);
payer签名并支付Solana交易。燃气赞助者可以为机密转账担任此角色。inputTree和outputTree是client.tree,包含已用UTXO并接收接收者输出和发送者更改的状态Merkle树。data包含上一步中生成的零知识证明和加密的输出。- 私有转账仅在私有余额之间移动资产。它不传递公共Solana账户或代币账户。
6. 像任何Solana交易一样发送
6. 像任何Solana交易一样发送
import { sendAndConfirmFactory } from "../src/lib.js";
const transferTx = await sendAndConfirm([
transferInstruction,
]);
sendAndConfirm签名并提交transferInstruction作为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上的机密环在这里。
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;
use zolana_transaction::{
instructions::{
transact::ConfidentialTransfer,
types::SppProofInputUtxo,
},
SOL_MINT,
};
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();
let transfer_input_utxo = SppProofInputUtxo::new(transfer_utxo, &sender);
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)?;
let transfer_data = client.prove_transact(tree, proof_inputs, None)?;
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();
1. 选择要花费的私有代币账户
1. 选择要花费的私有代币账户
use zolana_transaction::SOL_MINT;
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();
- 示例花费了由先前存款创建的私有Solana代币账户。转账可以花费多个UTXO。
transfer_utxo是该资产的第一个可花费UTXO。// SPL:注释显示get_balance(spl.mint)。
2. 准备证明输入
2. 准备证明输入
use zolana_transaction::instructions::types::SppProofInputUtxo;
let transfer_input_utxo = SppProofInputUtxo::new(transfer_utxo, &sender);
SppProofInputUtxo::new使用发送方的私有钱包密钥对准备选定的UTXO作为证明输入。- 密钥对派生出无效器,标记输入UTXO为已用,而资产和金额保持加密。
3. 构建和签署机密转账
3. 构建和签署机密转账
use zolana_transaction::{
instructions::transact::ConfidentialTransfer,
SOL_MINT,
};
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)?;
sender_shielded_address是发送者的。转账从此钱包中花费。vec![transfer_input_utxo]列出发送者选择的UTXO。转账可以花费多个UTXO。sender_solana_keypair.pubkey()是交易费用支付者。燃气赞助者可以为机密转账担任此角色。recipient_address是接收者的屏蔽地址。转账输出被加密到接收者的查看密钥。SOL_MINT选择SOL。// SPL:注释显示SPL和Token 2022资产的代币铸造。TRANSFER_AMOUNT以资产的基本单位计价。SOL使用lamports。SPL和Token 2022资产使用代币的基本单位。transfer.sign授权状态转换,加密资产和金额,并生成零知识证明者的输入。assets是用于解析支持的私有资产的资产注册表。
4. 获取零知识证明
4. 获取零知识证明
use zolana_client::Rpc;
let transfer_data = client.prove_transact(tree, proof_inputs, None)?;
client.prove_transact从签名的转账中生成零知识证明,并返回序列化的指令数据。tree标识状态Merkle树,其根用于证明输入UTXO成员资格。- 证明显示发送者拥有并可以花费输入,而不透露加密的资产或金额。
5. 构建转账指令
5. 构建转账指令
use zolana_interface::instruction::Transact;
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();
payer签名并支付Solana交易。燃气赞助者可以为机密转账担任此角色。input_tree标识包含已用UTXO的状态Merkle树。output_tree标识接收者输出和发送者更改承诺的状态Merkle树。interface_transfer_accounts是空的,因为私有转账仅在私有余额之间移动资产,不与Solana账户或代币账户中的公共余额交互。owner_signers对此机密转账是空的。data包含上一步中生成的零知识证明和加密的输出。
6. 像任何Solana交易一样发送
6. 像任何Solana交易一样发送
use zolana_client::Rpc;
let signature = client.create_and_send_transaction(
&[transfer_ix],
sender_solana_keypair.pubkey(),
&[&sender_solana_keypair],
)?;
let slot = landed_slot(&client, signature)?;
create_and_send_transaction签名并提交transfer_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上的机密环在这里。
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"))
}