- 개인 전송은 개인 지갑 간에 링 속에서 토큰을 이동시킵니다.
- 개인 전송은 단일 Solana 거래로 Solana 지갑 주소로 송신됩니다.
전송: 무엇이 비공개인지
- Permissionless Confidential Ring
- Custom Rings
| 필드 | 가시성 | 이유 |
|---|---|---|
| 자산 | 비공개 | 자산은 온체인에서 암호화됩니다 |
| 금액 | 비공개 | 전송된 금액은 온체인에서 암호화됩니다 |
| 소스 프라이빗 지갑 | 공개 | 컨피덴셜 링에서 소스 프라이빗 지갑은 온체인에서 보입니다. |
| 수신자 | 공개 | 컨피덴셜 링에서 수신자는 온체인에서 보입니다. |
링 내에서 및 링으로부터의 전송은 커스텀 링의 프로그램 ID를 드러냅니다.커스텀 링의 잔액은 출처 링의 정책이 허용하는 한 SPL 토큰 계정, 기본 링 또는 다른 링으로 이동할 수 있습니다.
기본 링은 무허가이며 정책이나 권한이 없습니다.하나의 거래는 기본 링과 하나의 커스텀 링에서 잔액을 결합할 수 있습니다. 두 개의 커스텀 링 간의 전송은 기본 링을 경유합니다.
| 프라이빗 전송 | 커스텀 컨피덴셜 링 | 커스텀 익명 링 | 커스텀 컨피덴셜과 기본 컨피덴셜 간 | 컨피덴셜(기본 또는 커스텀)에서 커스텀 익명으로 | 커스텀 익명에서 컨피덴셜(기본 또는 커스텀)로 |
|---|---|---|---|---|---|
| 금액 | 비공개 | 비공개 | 비공개 | 비공개 | 비공개 |
| 자산 | 비공개 | 비공개 | 비공개 | 비공개 | 비공개 |
| 소스 프라이빗 지갑 | 공개 | 비공개. 중계자가 거래를 제출하므로 공개 원장에 소스 프라이빗 지갑이 드러나지 않습니다. | 공개 | 공개 | 비공개. 중계자가 거래를 제출하므로 공개 원장에 소스 프라이빗 지갑이 드러나지 않습니다. |
| 수신자 | 공개 | 비공개 | 공개 | 비공개 | 공개 |
| 커스텀 링 프로그램 ID | 공개 | 공개 | 공개 | 공개 | 공개 |
허가 없는 링은 금액과 자산이 암호화되어 기밀로 유지됩니다.
사용자 정의 링은 기밀 또는 익명으로 구성할 수 있습니다 (발신자, 수신자, 자산 및 금액이 암호화됨).
전송이 작동하는 방법
개인 전송은 공공 Solana 전송과 유사하게 작동합니다:- 사용자의 SOL 또는 SPL 잔액이 온체인에서 암호화됩니다.
-
사용자는 개인 상태를 해독하고, 지갑은 전송을 생성하며, 소유자는 서명합니다.
- 전용 RPC 방법으로 암호화된 상태를 가져옵니다. 사용자는 로컬에서만 잔액을 해독할 수 있습니다.
- 지갑은 금액과 수신자를 설정한 후 ZK 증명을 요청합니다. RPC 제공자는 기본적으로 ZK 증명을 생성하여 반환합니다.
- Solana 런타임은 서명을 검증하고 Solana 프라이버시 프로그램을 호출하여 암호화된 상태를 공개하지 않고 ZK 증명을 검증합니다.
- 앱은 Solana 거래 해시를 통해 상태를 추적합니다.
Solana 전송과 비교
Solana 전송과 비교
- 사용자의 SOL 또는 SPL 잔액은 온체인에서 공개됩니다.
- 지갑은 공개 상태를 읽고 전송을 생성하며, 소유자는 서명합니다.
- Solana 런타임은 서명을 검증하고 시스템 프로그램 또는 토큰 프로그램을 호출하여 공개 잔액을 업데이트합니다.
- 앱은 Solana 거래 해시를 통해 상태를 추적합니다.
이는 허가 없는 기밀 링의 고급 거래 흐름입니다.
개념에서 사용자 정의 링과 비교하십시오.
시작하기
- TypeScript 클라이언트
- Rust 클라이언트
1
사전 준비 사항
TypeScript 예제는 Node.js 24 이상, pnpm 11.18.0, 그리고 Solana CLI가 필요합니다.
pnpm add @heliuslabs/zolana@^0.3.1-alpha @solana/kit@^8.3.0
엔드포인트 연결
엔드포인트 연결
- Devnet
- Localnet
pnpm install
cp .env.example .env
.env
API_KEY=YOUR_API_KEY
ZOLANA_PAYER_KEYPAIR=~/.config/solana/id.json
import { createZolanaClient } from "@heliuslabs/zolana";
const client = await createZolanaClient({
solanaRpcUrl: "https://devnet.helius-rpc.com/?api-key=YOUR_API_KEY",
indexerUrl: "https://d2xah7tnhdhcom.cloudfront.net",
proverUrl: "https://d21ni15goiip6l.cloudfront.net",
});
Localnet에서는 SDK가 로컬 테스트 검증자(
:8899), Photon 인덱서(:8784), 및 증명기(:3001)를 시작하고, 클라이언트는 엔드포인트 구성 없이 자동으로 이에 연결됩니다.cargo install --git https://github.com/helius-labs/zolana --tag v0.3.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,
setTransactionMessageConfig,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners,
type Instruction,
type Signature,
type TransactionSigner,
} from "@solana/kit";
import { createZolanaClient } from "@heliuslabs/zolana";
type Client = Awaited<ReturnType<typeof createZolanaClient>>;
export interface ConfirmedTransaction {
readonly signature: Signature;
readonly slot: bigint;
}
export function sendAndConfirmFactory(
client: Client,
feePayer: TransactionSigner,
): (instructions: readonly Instruction[]) => Promise<ConfirmedTransaction> {
const sendTransaction = sendTransactionWithoutConfirmingFactory({
rpc: client.solanaRpc,
});
return async function sendAndConfirm(
instructions: readonly Instruction[],
): Promise<ConfirmedTransaction> {
const { value: lifetime } = await client.solanaRpc
.getLatestBlockhash()
.send();
const signed = await signTransactionMessageWithSigners(
pipe(
createTransactionMessage({ version: 1 }),
(message) => setTransactionMessageFeePayerSigner(feePayer, message),
(message) =>
setTransactionMessageLifetimeUsingBlockhash(lifetime, message),
(message) =>
setTransactionMessageConfig(
{
computeUnitLimit: 450_000,
loadedAccountsDataSizeLimit: 64 * 1024 * 1024,
},
message,
),
(message) =>
appendTransactionMessageInstructions(instructions, message),
),
);
assertIsTransactionWithBlockhashLifetime(signed);
await sendTransaction(signed, { commitment: "confirmed" });
const signature = getSignatureFromTransaction(signed);
const slot = await client.confirmTransaction(signature);
return { signature, slot };
};
}
- SDK는 명령을 반환합니다. 앱은 이를 서명하고 전송합니다.
sendAndConfirmFactory는 Kit 거래를 생성하고, 제출하며 서명 및 도달한 슬롯을 반환합니다.
- SOL
- SPL
import { LocalKeys } from "@heliuslabs/zolana/client";
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,
sender,
);
const transfer = new ConfidentialTransfer(
senderAddress,
[transferInput],
senderSigner.address,
);
transfer.send(
recipient.shieldedAddress(),
SOL_MINT,
TRANSFER_AMOUNT,
);
const transferProofInputs = transfer.sign(
sender,
assets,
);
const senderKeys = LocalKeys.fromKeypair(sender, client.proofService);
const transferData = await client.proveTransact(
transferProofInputs,
senderKeys,
);
const transferInstruction = await transactInstruction(
{
payer: senderSigner,
inputTree: client.tree,
outputTree: client.tree,
data: transferData,
},
);
const transferTx = await sendAndConfirm([
transferInstruction,
]);
import { LocalKeys } from "@heliuslabs/zolana/client";
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,
sender,
);
const transfer = new ConfidentialTransfer(
senderAddress,
[transferInput],
senderSigner.address,
);
transfer.send(
recipient.shieldedAddress(),
spl.mint,
TRANSFER_AMOUNT,
);
const transferProofInputs = transfer.sign(
sender,
assets,
);
const senderKeys = LocalKeys.fromKeypair(sender, client.proofService);
const transferData = await client.proveTransact(
transferProofInputs,
senderKeys,
);
const transferInstruction = await 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는 해당 잔액에서 하나의 개인 솔라나 토큰 계정을 선택합니다.
2. 증명 입력 준비
2. 증명 입력 준비
import { ProofInputUtxo } from "@heliuslabs/zolana/transaction";
const transferInput =
ProofInputUtxo.fromKeypair(
transferUtxo,
sender,
);
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.shieldedAddress(),
SOL_MINT,
TRANSFER_AMOUNT,
);
const transferProofInputs = transfer.sign(
sender,
assets,
);
senderAddress는 송신자의 입니다. 전송 시 이 지갑의 자산을 사용합니다.[transferInput]에는 송신자가 선택한 UTXO가 나열됩니다. 한 번의 전송에서 여러 UTXO를 사용할 수 있습니다.senderSigner.address는 수수료 납부자의 Solana 주소입니다. 가스 스폰서가 수수료를 납부할 수 있습니다.recipient.shieldedAddress()는 수신자의 보호 주소입니다. 전송된 출력은 수신자의 조회 키로 암호화됩니다.SOL_MINT는 SOL을 선택합니다. SPL 또는 Token 2022 전송에서는 토큰 민트를 전달합니다.TRANSFER_AMOUNT의 단위는 자산의 기본 단위입니다. SOL은 lamport를 사용합니다. SPL 및 Token 2022 자산은 토큰의 기본 단위를 사용합니다.transfer.sign는 상태 전환을 승인하고 자산과 금액을 암호화하며 영지식 증명기에 사용할 입력을 생성합니다.assets는 지원되는 비공개 자산을 확인하는 데 사용되는 자산 레지스트리입니다.
4. 영지식 증명 가져오기
4. 영지식 증명 가져오기
import { LocalKeys } from "@heliuslabs/zolana/client";
const senderKeys = LocalKeys.fromKeypair(sender, client.proofService);
const transferData = await client.proveTransact(
transferProofInputs,
senderKeys,
);
senderKeys는LocalKeys.fromKeypair(sender, client.proofService)를 사용하여 송신자의 키로 증명 생성을 승인합니다.client.proveTransact는 서명된 전송에서 영지식 증명을 생성하고 직렬화된 명령 데이터를 반환합니다.- 이 증명은 암호화된 자산이나 금액을 공개하지 않고도 송신자가 입력을 소유하고 사용할 수 있음을 입증합니다.
5. 전송 명령 생성
5. 전송 명령 생성
import { transactInstruction } from "@heliuslabs/zolana/interface";
const transferInstruction = await transactInstruction(
{
payer: senderSigner,
inputTree: client.tree,
outputTree: client.tree,
data: transferData,
},
);
payer는 Solana 트랜잭션에 서명하고 수수료를 납부합니다. 가스 스폰서가 수수료를 납부할 수 있습니다.inputTree와outputTree는client.tree이며, 이는 사용된 UTXO를 포함하고 수신자 출력과 송신자 잔돈을 받는 상태 Merkle 트리입니다.await transactInstruction는 로컬에서 nullifier 계정 주소를 파생하고 명령을 반환합니다. nullifier 계정은 비공개 잔액이 이중으로 사용되지 않도록 입력 UTXO를 사용된 것으로 표시합니다.data에는 이전 단계에서 생성된 영지식 증명과 암호화된 출력이 포함됩니다.- 비공개 전송은 비공개 잔액 간에만 자산을 이동합니다. 공개 Solana 계정이나 토큰 계정을 전달하지 않습니다.
6. 일반 Solana 거래처럼 전송
6. 일반 Solana 거래처럼 전송
import { sendAndConfirmFactory } from "../src/lib.js";
const transferTx = await sendAndConfirm([
transferInstruction,
]);
sendAndConfirm는 Solana 거래로transferInstruction에 서명하고 제출합니다.- 확인 결과는 인덱서 가져오기를 제한하기 위해 사용된 도달 슬롯을 제공합니다.
전체 코드 예제
예제를 복제하고 실행하세요:git clone https://github.com/helius-labs/zolana-examples.git
cd zolana-examples
git checkout v0.3.0-alpha
cd typescript-client
pnpm install
pnpm example examples/deposit_transfer_withdraw.ts
예제에서는 여기의 로컬/devnet에 있는 기밀 Ring을 사용합니다.
deposit_transfer_withdraw.ts
import {
SOL_MINT,
ShieldedKeypair,
createZolanaClient,
} from "@heliuslabs/zolana";
import {
LocalKeys,
atSlot,
} from "@heliuslabs/zolana/client";
import {
depositInstruction,
transactInstruction,
DepositAsset,
TransactWithdrawal,
} from "@heliuslabs/zolana/interface";
import {
AssetRegistry,
ConfidentialTransfer,
ProofInputUtxo,
decryptToBalances,
WithdrawalTarget,
} from "@heliuslabs/zolana/transaction";
import {
cliKeypair,
sendAndConfirmFactory,
} from "../src/lib.js";
const DEPOSIT_AMOUNT = 10_000_000n;
const TRANSFER_AMOUNT = 3_000_000n;
const WITHDRAW_AMOUNT = 3_000_000n;
async function main(): Promise<void> {
const client = await createZolanaClient({
solanaRpcUrl: `https://devnet.helius-rpc.com/?api-key=${process.env.API_KEY}`,
});
// localnet: const client = await createZolanaClient({});
// Initialize the sender's private wallet and local authority
// to decrypt transactions and sync balances.
// The Solana signer and private wallet are derived from the same Ed25519 seed.
const sender = ShieldedKeypair.fromKeypair(
await cliKeypair(),
);
const recipient = ShieldedKeypair.generate();
const senderSigner = sender.toSolanaSigner();
const senderAddress = sender.shieldedAddress();
const senderKeys = LocalKeys.fromKeypair(
sender,
client.proofService,
);
// The SDK hands back instructions; the app owns signing and sending.
const sendAndConfirm = sendAndConfirmFactory(
client,
senderSigner,
);
// Mints that are registered with Solana Rings for privacy.
const assets = new AssetRegistry();
// Deposit SOL into the sender's private balance.
// A deposit from a public balance reveals
// sender, recipient, asset and amount.
// Alternatively, you can onramp fiat directly to a private balance.
// 1. Move public SOL into the sender's private balance.
// The view tag is the sender's Solana public key in confidential rings.
// Used by the indexer to fetch the sender's outputs.
const senderViewTag =
senderAddress.confidentialViewTag();
const depositIx = await depositInstruction({
tree: client.tree,
depositor: senderSigner,
deposits: [
{
asset: DepositAsset.sol(),
viewTag: senderViewTag,
recipientOwnerHash:
senderAddress.ownerHash(),
amount: DEPOSIT_AMOUNT,
},
],
});
// 2. Send and confirm like any Solana transaction; confirmation yields the landed slot.
const depositTx = await sendAndConfirm([
depositIx,
]);
// 3. Fetch this transaction's outputs, gated on its confirmed slot.
const depositResponse =
await client.getShieldedTransactionsBySignature(
depositTx.signature,
atSlot(depositTx.slot),
);
// 4. The sender decrypts the transaction outputs locally to read the funds deposited in this run.
const balancesAfterDeposit =
await decryptToBalances({
keypair: sender,
registry: assets,
transactions:
depositResponse.transactions.map(
({ transaction }) => transaction,
),
});
const depositBalance =
balancesAfterDeposit.balance(SOL_MINT);
if (depositBalance.amount !== DEPOSIT_AMOUNT) {
throw new Error(
`expected deposit amount ${DEPOSIT_AMOUNT}, got ${depositBalance.amount}`,
);
}
if (depositBalance.utxos.length !== 1) {
throw new Error(
`expected 1 deposit utxo, got ${depositBalance.utxos.length}`,
);
}
// Confidential SOL transfer to the recipient's private balance.
// A confidential transfer reveals only sender and recipient,
// not the asset or amount.
// 1. Select private token accounts (UTXOs) that make up the private balance for the transfer.
const transferUtxo = depositBalance.utxos[0]!;
// 2. Prepare the selected UTXOs as inputs for the zero-knowledge proof.
const transferInput =
ProofInputUtxo.fromKeypair(
transferUtxo,
sender,
);
// 3. Build and sign the confidential transfer.
// Signing encrypts the asset and amount and produces the proof inputs for the ZK prover.
const transfer = new ConfidentialTransfer(
senderAddress,
[transferInput],
senderSigner.address,
);
transfer.send(
recipient.shieldedAddress(),
SOL_MINT,
TRANSFER_AMOUNT,
);
const transferProofInputs = transfer.sign(
sender,
assets,
);
// 4. Fetch the ZK proof to prove the sender can spend the balance without revealing asset and amount.
const transferData = await client.proveTransact(
transferProofInputs,
senderKeys,
);
// 5. Build the instruction with the state Merkle tree and Solana accounts required for the transfer.
// Private transfers move balances only between private token accounts, not public token accounts.
const transferInstruction =
await transactInstruction({
payer: senderSigner,
inputTree: client.tree,
outputTree: client.tree,
data: transferData,
});
// 6. Send and confirm like any Solana transaction; confirmation yields the landed slot.
const transferTx = await sendAndConfirm([
transferInstruction,
]);
// 7. Fetch this transaction's outputs, gated on its confirmed slot.
const transferResponse =
await client.getShieldedTransactionsBySignature(
transferTx.signature,
atSlot(transferTx.slot),
);
const balancesAfterTransfer =
await decryptToBalances({
keypair: sender,
registry: assets,
transactions:
transferResponse.transactions.map(
({ transaction }) => transaction,
),
});
const transferBalance =
balancesAfterTransfer.balance(SOL_MINT);
if (
transferBalance.amount !==
DEPOSIT_AMOUNT - TRANSFER_AMOUNT
) {
throw new Error(
`expected remaining amount from this run ${DEPOSIT_AMOUNT - TRANSFER_AMOUNT}, got ${transferBalance.amount}`,
);
}
if (transferBalance.utxos.length !== 1) {
throw new Error(
`expected 1 transfer utxo, got ${transferBalance.utxos.length}`,
);
}
// Withdraw SOL from the sender's private balance to their public balance.
// A withdrawal reveals the sender, recipient, asset, and amount.
// 1. Select private token accounts (UTXOs) that make up the private balance for the withdrawal.
const withdrawalUtxo =
transferBalance.utxos[0]!;
// 2. Prepare the selected UTXOs as inputs for the zero-knowledge proof.
const withdrawalInput =
ProofInputUtxo.fromKeypair(
withdrawalUtxo,
sender,
);
// 3. Build and sign the private-to-public withdrawal.
// Signing encrypts the asset and amount of the remaining private balance
// and produces the proof inputs for the ZK prover.
const withdrawal = new ConfidentialTransfer(
senderAddress,
[withdrawalInput],
senderSigner.address,
);
withdrawal.withdraw(
SOL_MINT,
WITHDRAW_AMOUNT,
WithdrawalTarget.sol({
recipient: senderSigner.address,
}),
);
const withdrawalProofInputs = withdrawal.sign(
sender,
assets,
);
// 4. Fetch the ZK proof to prove the sender can spend the balance.
const withdrawalData =
await client.proveTransact(
withdrawalProofInputs,
senderKeys,
);
// 5. Build the instruction with the state Merkle tree and Solana accounts required for the withdrawal.
const withdrawalInstruction =
await transactInstruction({
payer: senderSigner,
inputTree: client.tree,
outputTree: client.tree,
withdrawal: TransactWithdrawal.sol({
recipient: senderSigner.address,
}),
data: withdrawalData,
});
// 6. Send and confirm like any Solana transaction; confirmation yields the landed slot.
const withdrawalTx = await sendAndConfirm([
withdrawalInstruction,
]);
// 7. Fetch this transaction's outputs, gated on its confirmed slot.
const withdrawalResponse =
await client.getShieldedTransactionsBySignature(
withdrawalTx.signature,
atSlot(withdrawalTx.slot),
);
const balancesAfterWithdrawal =
await decryptToBalances({
keypair: sender,
registry: assets,
transactions:
withdrawalResponse.transactions.map(
({ transaction }) => transaction,
),
});
const withdrawalBalance =
balancesAfterWithdrawal.balance(SOL_MINT);
if (
withdrawalBalance.amount !==
DEPOSIT_AMOUNT -
TRANSFER_AMOUNT -
WITHDRAW_AMOUNT
) {
throw new Error(
`expected remaining amount from this run ${DEPOSIT_AMOUNT - TRANSFER_AMOUNT - WITHDRAW_AMOUNT}, got ${withdrawalBalance.amount}`,
);
}
if (withdrawalBalance.utxos.length !== 1) {
throw new Error(
`expected 1 withdrawal utxo, got ${withdrawalBalance.utxos.length}`,
);
}
// 8. Read remaining private balance and the public balance.
const solanaBalance = await client.getBalance(
senderSigner.address,
);
console.log(
`withdraw private_balance=${withdrawalBalance.amount} ` +
`solana_balance=${solanaBalance} tx=${withdrawalTx.signature}`,
);
}
await main();
1
사전 준비 사항
Rust 예제를 사용하려면 Rust 1.98.1과 Solana CLI v4.0.2가 필요합니다. Solana 설치 가이드를 참조하세요.
Cargo.toml
[dependencies]
zolana-client = { git = "https://github.com/helius-labs/zolana", tag = "v0.3.0-alpha", features = ["indexer-api", "solana-rpc"] }
zolana-interface = { git = "https://github.com/helius-labs/zolana", tag = "v0.3.0-alpha" }
zolana-program = { git = "https://github.com/helius-labs/zolana", tag = "v0.3.0-alpha" }
zolana-keypair = { git = "https://github.com/helius-labs/zolana", tag = "v0.3.0-alpha" }
zolana-transaction = { git = "https://github.com/helius-labs/zolana", tag = "v0.3.0-alpha" }
zolana-wallet = { git = "https://github.com/helius-labs/zolana", tag = "v0.3.0-alpha" }
엔드포인트에 연결
엔드포인트에 연결
- Devnet
- Localnet
Helius API 키 추가하기:예제에서는 기본적으로 Solana CLI 지갑을 지불자로 사용합니다. 지불자는 devnet SOL을 보유해야 합니다. Devnet SOL을 얻는 방법을 참조하십시오.
.env
API_KEY=YOUR_API_KEY
ZOLANA_PAYER_KEYPAIR=~/.config/solana/id.json
use zolana_client::{SolanaRpc, ZolanaClient};
use zolana_interface::pda;
let tree = pda::tree(0);
let client = ZolanaClient::from_urls(
SolanaRpc::new("https://devnet.helius-rpc.com/?api-key=YOUR_API_KEY"),
"https://d2xah7tnhdhcom.cloudfront.net",
"https://d21ni15goiip6l.cloudfront.net",
)?;
cargo install --git https://github.com/helius-labs/zolana --tag v0.3.0-alpha zolana-cli
zolana dev start
use zolana_client::{SolanaRpc, ZolanaClient};
use zolana_interface::pda;
let tree = pda::tree(0);
let client = ZolanaClient::from_urls(
SolanaRpc::new("http://127.0.0.1:8899"),
"http://127.0.0.1:8784",
"http://127.0.0.1:3001",
)?;
2
개인 잔액으로 전송
use zolana_program::instruction::Transact;
use zolana_transaction::{instructions::transact::ConfidentialTransaction, 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 mut transfer = ConfidentialTransaction::new(vec![transfer_utxo], sender.pubkey())?;
transfer.transfer_sol(&recipient.shielded_address()?, TRANSFER_AMOUNT)?;
// SPL: transfer.transfer(&recipient.shielded_address()?, spl.mint, TRANSFER_AMOUNT)?;
let proof_inputs = transfer.encrypt(&sender)?;
let transfer_data = client.prove_transact(proof_inputs, None, &sender)?;
let transfer_ix = Transact {
payer: sender.pubkey(),
input_trees: vec![tree],
output_tree: tree,
owner_signers: Vec::new(),
interface_transfer_accounts: Vec::new(),
data: transfer_data,
}
.instruction();
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::transact::ConfidentialTransaction;
let mut transfer = ConfidentialTransaction::new(vec![transfer_utxo], sender.pubkey())?;
ConfidentialTransaction::new는 선택한 UTXO를 직접 가져옵니다.vec![transfer_utxo]는 입력을 나열합니다.sender.pubkey()는 트랜잭션 수수료 지불자입니다.
3. Build and encrypt the confidential transfer
3. Build and encrypt the confidential transfer
use solana_signer::Signer;
transfer.transfer_sol(&recipient.shielded_address()?, TRANSFER_AMOUNT)?;
// SPL: transfer.transfer(&recipient.shielded_address()?, spl.mint, TRANSFER_AMOUNT)?;
let proof_inputs = transfer.encrypt(&sender)?;
recipient.shielded_address()?는 수신자의 Shielded Address입니다. 전송된 출력은 수신자의 조회 키로 암호화됩니다.transfer.transfer_sol는 SOL을 선택합니다.// SPL:주석은 SPL 및 Token 2022 자산의 토큰 민트를 보여 줍니다.TRANSFER_AMOUNT는 자산의 기본 단위로 표시됩니다. SOL은 lamport를 사용합니다. SPL 및 Token 2022 자산은 토큰의 기본 단위를 사용합니다.transfer.encrypt(&sender)는 출력을 암호화하고 영지식 증명기에 필요한 입력을 생성합니다.
4. 영지식 증명 가져오기
4. 영지식 증명 가져오기
use zolana_client::Rpc;
let transfer_data = client.prove_transact(proof_inputs, None, &sender)?;
client.prove_transact는 암호화된 전송에서 영지식 증명을 생성하고 직렬화된 명령 데이터를 반환합니다.sender는 증명을 승인하는 데 사용되는 키를 제공합니다. 입력 UTXO는 해당 상태 트리를 식별합니다.- 이 증명은 암호화된 자산이나 금액을 공개하지 않고도 송신자가 입력을 소유하고 사용할 수 있음을 입증합니다.
5. 전송 명령 생성
5. 전송 명령 생성
use zolana_program::instruction::Transact;
let transfer_ix = Transact {
payer: sender.pubkey(),
input_trees: vec![tree],
output_tree: tree,
owner_signers: Vec::new(),
interface_transfer_accounts: Vec::new(),
data: transfer_data,
}
.instruction();
payer는 Solana 트랜잭션에 서명하고 수수료를 납부합니다. 가스 스폰서가 수수료를 납부할 수 있습니다.input_trees는 사용된 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.pubkey(),
&[&sender],
client.compute_budget(),
)?;
let slot = landed_slot(&client, signature)?;
create_and_send_transaction는transfer_ix에 서명하고 이를 Solana 트랜잭션으로 제출합니다.landed_slot는 인덱서 가져오기를 제한하는 데 사용되는 확인 슬롯을 읽습니다.sender는 수수료를 납부하고 전송을 승인합니다.
전체 코드 예제
예제를 복제하고 실행하세요:git clone https://github.com/helius-labs/zolana-examples.git
cd zolana-examples
git checkout v0.3.0-alpha
cd rust-client
cargo run -p rust-client-example --example deposit_transfer_withdraw
예제에서는 여기의 로컬/devnet에 있는 기밀 Ring을 사용합니다.
deposit_transfer_withdraw.rs
use anyhow::{anyhow, Result};
use rust_client_example::{cli_keypair, landed_slot, setup, SetupContext};
use solana_keypair::Keypair;
use solana_signer::Signer;
use zolana_client::{IndexerRpcConfig, Rpc, SolanaRpc, ZolanaClient};
use zolana_keypair::ShieldedKeypair;
use zolana_program::instruction::{
AssetDeposit, Deposit, DepositAsset, Transact, TransactInterfaceTransferAccounts,
TransactSolTransferAccounts,
};
use zolana_transaction::{
decrypt_spendable, instructions::transact::ConfidentialTransaction, AssetRegistry, SOL_MINT,
};
const DEPOSIT_AMOUNT: u64 = 10_000_000;
const TRANSFER_AMOUNT: u64 = 3_000_000;
const WITHDRAW_AMOUNT: u64 = 3_000_000;
fn main() -> Result<()> {
let SetupContext {
rpc_url,
indexer_url,
prover_url,
tree,
} = setup()?;
// Connect to the RPC, indexer, and prover.
let client = ZolanaClient::from_urls(SolanaRpc::new(rpc_url), &indexer_url, prover_url)?;
// Mints that are registered with Solana Rings for privacy.
let assets = AssetRegistry::default();
// SPL: assets.insert(spl.asset_id, spl.mint)?;
// Initialize the sender's private wallet and local authority
// to decrypt transactions and sync balances.
// The Solana signer and private wallet are derived from the same Ed25519 seed.
let sender = ShieldedKeypair::from_keypair(&cli_keypair()?)?;
let recipient = ShieldedKeypair::from_keypair(&Keypair::new())?;
let sender_shielded_address = sender.shielded_address()?;
// Deposit SOL into the sender's private balance.
// A deposit from a public balance reveals
// sender, recipient, asset and amount.
// Alternatively, you can onramp fiat directly to a private balance.
// 1. Move public SOL into the sender's private balance.
let sender_balances_after_deposit = {
let deposit_ix = Deposit {
tree,
depositor: sender.pubkey(),
deposits: vec![AssetDeposit {
asset: DepositAsset::Sol,
// SPL: asset: DepositAsset::Spl(zolana_program::instruction::DepositSplAccounts {
// SPL: mint: spl.mint,
// SPL: user_token: spl.user_token_account,
// SPL: token_program: spl.token_program,
// SPL: }),
view_tag: sender_shielded_address.confidential_view_tag()?,
owner: sender_shielded_address.owner_hash()?,
amount: DEPOSIT_AMOUNT,
memo: None,
}],
}
.instruction()?;
// 2. Send and confirm like any Solana transaction; the landed slot gates
// the indexer fetch below.
let signature = client.create_and_send_transaction(
&[deposit_ix],
sender.pubkey(),
&[&sender],
client.compute_budget(),
)?;
let slot = landed_slot(&client, signature)?;
// 3. Fetch transaction outputs from the indexer, gated on the deposit's slot.
// The indexer returns encrypted outputs by transaction signature.
let response = client.get_shielded_transactions_by_signature(
signature,
Some(IndexerRpcConfig::at_slot(slot)),
)?;
let transactions = response
.transactions
.into_iter()
.map(|indexed| indexed.transaction)
.collect::<Vec<_>>();
// 4. The sender decrypts the transaction outputs locally to update the private balance.
let balances = decrypt_spendable(&sender, &transactions, &assets)
.map_err(|e| anyhow!("decrypt sender transactions: {e:?}"))?
.balances;
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 mut transfer = ConfidentialTransaction::new(vec![transfer_utxo], sender.pubkey())?;
// 3. Build and encrypt the confidential transfer.
// Encryption hides the asset and amount and produces the proof inputs for the ZK prover.
transfer.transfer_sol(&recipient.shielded_address()?, TRANSFER_AMOUNT)?;
// SPL: transfer.transfer(&recipient.shielded_address()?, spl.mint, TRANSFER_AMOUNT)?;
let proof_inputs = transfer.encrypt(&sender)?;
// 4. Fetch the zk proof to prove the sender can spend the balance without revealing asset and amount.
let transfer_data = client.prove_transact(proof_inputs, None, &sender)?;
// 5. Construct the instruction.
let transfer_ix = Transact {
payer: sender.pubkey(),
input_trees: vec![tree],
output_tree: tree,
owner_signers: Vec::new(),
interface_transfer_accounts: Vec::new(),
data: transfer_data,
}
.instruction();
// 6. Send and confirm like any Solana transaction; confirmation yields the landed slot.
let signature = client.create_and_send_transaction(
&[transfer_ix],
sender.pubkey(),
&[&sender],
client.compute_budget(),
)?;
let slot = landed_slot(&client, signature)?;
// 7. Fetch the sender's UTXOs from this transaction, gated on the transfer's slot,
// and read the remaining private balance.
let response = client.get_shielded_transactions_by_signature(
signature,
Some(IndexerRpcConfig::at_slot(slot)),
)?;
let transactions = response
.transactions
.into_iter()
.map(|indexed| indexed.transaction)
.collect::<Vec<_>>();
let sender_balances = decrypt_spendable(&sender, &transactions, &assets)
.map_err(|e| anyhow!("decrypt sender transactions: {e:?}"))?
.balances;
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 mut withdrawal = ConfidentialTransaction::new(vec![withdrawal_utxo], sender.pubkey())?;
// 3. Build and encrypt the confidential withdrawal.
// Encryption hides the private change and produces the ZK prover inputs.
withdrawal.withdraw_sol(WITHDRAW_AMOUNT, sender.pubkey())?;
// SPL: withdrawal.withdraw(spl.mint, WITHDRAW_AMOUNT, spl.user_token_account)?;
let proof_inputs = withdrawal.encrypt(&sender)?;
// 4. Fetch the ZK proof to prove the sender can spend the balance.
let withdrawal_data = client.prove_transact(proof_inputs, None, &sender)?;
// 5. Combine the proof and withdrawal accounts in a single instruction.
let withdraw_ix = Transact {
payer: sender.pubkey(),
input_trees: vec![tree],
output_tree: tree,
owner_signers: Vec::new(),
interface_transfer_accounts: vec![TransactInterfaceTransferAccounts::Sol(
TransactSolTransferAccounts {
recipient: sender.pubkey(),
},
)],
// SPL: interface_transfer_accounts: vec![
// SPL: TransactInterfaceTransferAccounts::SplWithdrawal(
// SPL: zolana_program::instruction::TransactSplWithdrawalAccounts {
// SPL: mint: spl.mint,
// SPL: spl_interface: spl.vault,
// SPL: user_token_account: spl.user_token_account,
// SPL: token_program: spl.token_program,
// SPL: },
// SPL: ),
// SPL: ],
data: withdrawal_data,
}
.instruction();
// 6. Send and confirm like any Solana transaction.
let signature = client.create_and_send_transaction(
&[withdraw_ix],
sender.pubkey(),
&[&sender],
client.compute_budget(),
)?;
let slot = landed_slot(&client, signature)?;
// 7. Fetch the sender's UTXOs from this transaction, gated on the withdrawal's slot,
// and read the remaining private balance.
let response = client.get_shielded_transactions_by_signature(
signature,
Some(IndexerRpcConfig::at_slot(slot)),
)?;
let transactions = response
.transactions
.into_iter()
.map(|indexed| indexed.transaction)
.collect::<Vec<_>>();
let sender_balances = decrypt_spendable(&sender, &transactions, &assets)
.map_err(|e| anyhow!("decrypt sender transactions: {e:?}"))?
.balances;
let sender_balance = sender_balances
.get_balance(SOL_MINT)
// SPL: .get_balance(spl.mint)
.expect("failed to fetch sender's utxo");
assert_eq!(
sender_balance.amount,
DEPOSIT_AMOUNT - TRANSFER_AMOUNT - WITHDRAW_AMOUNT
);
assert_eq!(sender_balance.utxos.len(), 1);
// 8. Read remaining private balance and the public SOL balance.
let solana_balance = client.get_balance(sender.pubkey())?;
println!("withdraw solana_balance={solana_balance} tx={signature}");
// SPL: println!(
// SPL: "withdraw user_token={} tx={signature}",
// SPL: spl.user_token_account,
// SPL: );
}
Ok(())
}