- Eine Einzahlung verschiebt Tokens vom öffentlichen Solana-Guthaben des Benutzers in sein eigenes oder das eines anderen privaten Guthabens.
- Einzahlungen werden in einer einzigen Solana-Transaktion an eine Solana-Wallet-Adresse gesendet.
Einzahlung: Was ist privat
| Field | Visibility | Why |
|---|---|---|
| Source public wallet | Public | The source wallet address is visible onchain |
| Asset | Public | The asset is visible onchain |
| Amount | Public | The deposited amount is visible onchain |
| Destination wallet address | Public | The destination wallet address is visible onchain |
| Resulting private balance | Private | The resulting private balance is encrypted onchain |
Der erlaubnisfreie Ring ist vertraulich mit verschlüsseltem Betrag und Vermögenswert.
Ein benutzerdefinierter Ring kann als vertraulich oder anonym konfiguriert werden (verschlüsselter Absender, Empfänger, Vermögenswert und Betrag).
Wie eine Einzahlung funktioniert
Eine Einzahlung ist eine öffentliche Solana-Übertragung mit einem resultierenden privaten Guthaben.- Das SOL- oder SPL-Guthaben des Benutzers ist öffentlich on-chain.
- Die Wallet setzt Betrag und Empfänger fest, erstellt die Einzahlung und der Eigentümer signiert. Eine Einzahlung erfordert keinen ZK-Beweis.
- Die Solana-Laufzeit prüft die Signaturen und ruft das Solana-Privacy-Programm auf.
- Die App verfolgt den Status über den Solana-Transaktions-Hash.
Vergleich mit Solana-Übertragung
Vergleich mit Solana-Übertragung
- Das SOL- oder SPL-Guthaben des Benutzers ist öffentlich on-chain.
- Die Wallet liest den öffentlichen Status, erstellt eine Übertragung und der Eigentümer signiert.
- Die Solana-Laufzeit prüft die Signaturen und ruft das System-Programm oder Token-Programm auf, das das öffentliche Guthaben aktualisiert.
- Die App verfolgt den Status über den Solana-Transaktions-Hash.
Dies ist der Transaktionsablauf auf hoher Ebene für den erlaubnisfreien vertraulichen Ring.
Vergleichen Sie dies mit benutzerdefinierten Ringen in Konzepten.
Loslegen
- TypeScript Client
- Rust Client
1
Voraussetzungen
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
Einzahlung in ein privates Guthaben
Solana Kit send helper
Solana Kit send helper
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 };
};
}
- Das SDK gibt Anweisungen zurück. Die App signiert und sendet diese.
sendAndConfirmFactoryerstellt eine Kit-Transaktion, übermittelt sie und gibt die Signatur plus den gelandeten Slot zurück.
- SOL
- SPL
import {
depositInstruction,
DepositAsset,
} from "@heliuslabs/zolana/interface";
import { randomBlinding } from "@heliuslabs/zolana/keypair";
import { sendAndConfirmFactory } from "../src/lib.js";
const sendAndConfirm = sendAndConfirmFactory(
client,
senderSigner,
);
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,
},
],
});
const depositTx = await sendAndConfirm([
depositIx,
]);
import {
depositInstruction,
DepositAsset,
} from "@heliuslabs/zolana/interface";
import { randomBlinding } from "@heliuslabs/zolana/keypair";
import { sendAndConfirmFactory } from "../src/lib.js";
const sendAndConfirm = sendAndConfirmFactory(
client,
senderSigner,
);
const senderViewTag =
senderAddress.confidentialViewTag();
const depositIx = await depositInstruction({
tree: client.tree,
depositor: senderSigner,
deposits: [
{
asset: DepositAsset.spl({
mint: spl.mint,
sourceTokenAccount: spl.sourceTokenAccount,
tokenProgram: spl.tokenProgram,
}),
viewTag: senderViewTag,
recipientOwnerHash:
senderAddress.ownerHash(),
blinding: randomBlinding(),
amount: DEPOSIT_AMOUNT,
},
],
});
const depositTx = await sendAndConfirm([
depositIx,
]);
1. Verschiebe öffentliche Tokens in das private Guthaben des Absenders
1. Verschiebe öffentliche Tokens in das private Guthaben des Absenders
import {
depositInstruction,
DepositAsset,
} from "@heliuslabs/zolana/interface";
import { randomBlinding } from "@heliuslabs/zolana/keypair";
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,
},
],
});
senderAddressist die des Empfängers. Dieses Beispiel zahlt an den Absender ein. Der Empfänger kann der Absender oder eine Drittanbieter-Wallet sein.senderViewTagistsenderAddress.confidentialViewTag(), der öffentliche Solana-Schlüssel des Absenders in vertraulichen Ringen. Der Indexer nutzt diesen, um übereinstimmende verschlüsselte Ausgaben zurückzugeben.client.treeist der Status-Merkle-Baum, der das neue Ausgabeverpflichtungswert erhält.depositor/senderSignerfinanziert und signiert die Einzahlung von öffentlich auf privat.DepositAsset.sol()wählt SOL.DepositAsset.splnimmt das Mint, das Quell-Token-Konto und das Token-Programm für SPL- und Token-2022-Assets.recipientOwnerHashistsenderAddress.ownerHash(). Dieser Hash der Signier- und Nullifier-Public-Keys des Empfängers ist das Inhaberfeld, das im neuen privaten Token-Konto festgeschrieben wird.randomBlinding()fügt dem privaten Token-Konto Zufälligkeit hinzu.DEPOSIT_AMOUNTverwendet Lamports für SOL oder die Basiseinheiten des Tokens für SPL und Token 2022 Assets.
2. Senden wie jede Solana-Transaktion
2. Senden wie jede Solana-Transaktion
import { sendAndConfirmFactory } from "../src/lib.js";
const depositTx = await sendAndConfirm([
depositIx,
]);
sendAndConfirmsigniert und übermitteltdepositIxals Solana-Transaktion.- Die Bestätigung ergibt den gelandeten Slot, der verwendet wird, um die Indexer-Abfrage zu steuern.
Vollständiges Codebeispiel
Klonen und das Beispiel ausführen:git clone https://github.com/helius-labs/zolana-examples.git
cd zolana-examples/typescript-client
pnpm install
pnpm example examples/deposit_transfer_withdraw.ts
Die Beispiele verwenden einen vertraulichen Ring auf lokal/devnet hier.
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
Voraussetzungen
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
Einzahlung in ein privates Guthaben
use zolana_interface::instruction::{AssetDeposit, Deposit, DepositAsset};
use zolana_keypair::random_blinding;
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()?;
1. Öffentliches SOL in das private Guthaben des Absenders verschieben
1. Öffentliches SOL in das private Guthaben des Absenders verschieben
use zolana_interface::instruction::{AssetDeposit, Deposit, DepositAsset};
use zolana_keypair::random_blinding;
// 1. Move public SOL into the sender's private balance.
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()?;
sender_shielded_addressist die des Empfängers. Dieses Beispiel zahlt an den Absender ein. Der Empfänger kann der Absender oder eine Drittanbieter-Wallet sein.view_tagistsender_shielded_address.confidential_view_tag(), der öffentliche Solana-Schlüssel des Absenders in vertraulichen Ringen. Der Indexer nutzt diesen, um übereinstimmende verschlüsselte Ausgaben zurückzugeben.treeist der Status-Merkle-Baum, der das neue Ausgabeverpflichtungswert erhält.depositor/sender_solana_keypairfinanziert und signiert die Einzahlung von öffentlich auf privat.DepositAsset::Solwählt SOL. Die// SPL:-Kommentare zeigenDepositAsset::Splmit Mint, Quell-Token-Konto und Token-Programm für SPL- und Token-2022-Assets.owneristsender_shielded_address.owner_hash(). Dieser Hash der Signier- und Nullifier-Public-Keys des Empfängers ist das Inhaberfeld, das im neuen privaten Token-Konto festgeschrieben wird.random_blinding()fügt dem privaten Token-Konto Zufälligkeit hinzu.DEPOSIT_AMOUNTverwendet Lamports für SOL oder die Basiseinheiten des Tokens für SPL und Token 2022 Assets.utxo_dataundmemosindNone.
2. Senden wie jede Solana-Transaktion
2. Senden wie jede Solana-Transaktion
use zolana_client::Rpc;
let signature = client.create_and_send_transaction(
&[deposit_ix],
sender_solana_keypair.pubkey(),
&[&sender_solana_keypair],
)?;
let slot = landed_slot(&client, signature)?;
create_and_send_transactionsigniert und übermitteltdeposit_ixals Solana-Transaktion.landed_slotliest den Bestätigungs-Slot, der verwendet wird, um die Indexer-Abfrage zu steuern.sender_solana_keypairzahlt die Gebühr und autorisiert die Einzahlung von öffentlich auf privat.
Vollständiges Codebeispiel
Klonen und das Beispiel ausführen: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
Die Beispiele verwenden einen vertraulichen Ring auf lokal/devnet hier.
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"))
}