- プライベート転送は、プライベートウォレット間でトークンをリング内に移動します。
- プライベート転送は、1 つの 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ランタイムは署名を検証し、System ProgramまたはToken Programを呼び出して公開バランスを更新します。
- アプリは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 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]!;
- この例では、先行するデポジットで作成されたプライベートソラナトークンアカウントを支出しています。転送は複数のUTXOを使用できます。
transferUtxoがそのバランスから1つのプライベートソラナトークンアカウントを選択します。
2. 証明入力を準備
2. 証明入力を準備
import { ProofInputUtxo } from "@heliuslabs/zolana/transaction";
const transferInput =
ProofInputUtxo.fromKeypair(
transferUtxo,
senderKeypair,
);
ProofInputUtxo.fromKeypairが選択されたUTXOを送信者のプライベートウォレットキーペアを使って証明入力として準備します。- キーペアは、資産と額が暗号化されたままで、使用済みとして入力UTXOをマークするnullifierを導出します。
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は手数料支払者のソラナアドレスです。ガススポンサーが機密転送においてこの役割を果たすことができます。recipientは受信者のシールドアドレスです。送信された出力は受信者の表示キーに暗号化されます。SOL_MINTがSOLを選択します。SPLまたはToken 2022転送はトークンミントを渡します。TRANSFER_AMOUNTは資産の基本単位で示されます。SOLはラメポーツを使用します。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がソラナトランザクションで署名し料金を支払います。ガススポンサーが機密転送においてこの役割を果たすことができます。inputTreeおよびoutputTreeはclient.tree、消費されたUTXOを含み、受取人の出力と送信者の釣銭を受け取る状態Merkle木です。dataは前のステップで生成されたゼロ知識証明と暗号化された出力を含みます。- プライベート転送は資産をプライベートバランス間でのみ移動します。公開ソラナアカウントまたはトークンアカウントを通過しません。
6. ソラナトランザクションのように送信
6. ソラナトランザクションのように送信
import { sendAndConfirmFactory } from "../src/lib.js";
const transferTx = await sendAndConfirm([
transferInstruction,
]);
sendAndConfirmが署名し、transferInstructionをソラナトランザクションとして送信します。- 確認は、インデクサーの取得をゲートするために使用されるスロットをもたらします。
完全なコード例
例をクローンして実行します。git clone https://github.com/helius-labs/zolana-examples.git
cd zolana-examples/typescript-client
pnpm install
pnpm example examples/deposit_transfer_withdraw.ts
例はこちらでローカル/デブネット上の機密リングを使用しています。
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();
- この例では、先行するデポジットで作成されたプライベートソラナトークンアカウントを支出しています。転送は複数の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をマークするnullifierを導出します。
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はラメポーツを使用します。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は、ルートを使用して入力UTXOメンバーシップを証明する状態Merkle木を識別します。- 証明は、送信者が暗号化された資産や額を明らかにせずに入力を所持し支出できることを示します。
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がソラナトランザクションで署名し料金を支払います。ガススポンサーが機密転送においてこの役割を果たすことができます。input_treeは消費されたUTXOを含む状態Merkle木を識別します。output_treeは受取人の出力と送信者の釣銭を受け取る状態Merkle木を識別します。interface_transfer_accountsは、プライベート転送が資産をプライベートバランス間でのみ移動し、ソラナアカウントやトークンアカウントの公開バランスと対話しないために空です。owner_signersは、この機密転送のために空です。dataは前のステップで生成されたゼロ知識証明と暗号化された出力を含みます。
6. ソラナトランザクションのように送信
6. ソラナトランザクションのように送信
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をソラナトランザクションとして送信します。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
例はこちらでローカル/デブネット上の機密リングを使用しています。
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"))
}