> ## Documentation Index
> Fetch the complete documentation index at: https://www.helius.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 전송

> 개인 잔액 간에 SOL, SPL 또는 Token 2022 자산을 전송하는 가이드와 전체 코드 예제.

1. 개인 전송은 개인 지갑 간에 링 속에서 토큰을 이동시킵니다.
2. 개인 전송은 단일 Solana 거래로 Solana 지갑 주소로 송신됩니다.

#### 전송: 무엇이 비공개인지

<Tabs>
  <Tab title="Permissionless Confidential Ring">
    | 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.             |
  </Tab>

  <Tab title="Custom Rings">
    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                                                                                                      |
  </Tab>
</Tabs>

<Info>
  허가 없는 링은 금액과 자산이 암호화되어 기밀로 유지됩니다.
  사용자 정의 링은 기밀 또는 익명으로 구성할 수 있습니다 (발신자, 수신자, 자산 및 금액이 암호화됨).
</Info>

## 전송이 작동하는 방법

개인 전송은 공공 Solana 전송과 유사하게 작동합니다:

1. 사용자의 SOL 또는 SPL 잔액이 온체인에서 암호화됩니다.

2. 사용자는 개인 상태를 해독하고, 지갑은 전송을 생성하며, 소유자는 서명합니다.

   * 전용 RPC 방법으로 암호화된 상태를 가져옵니다. 사용자는 로컬에서만 잔액을 해독할 수 있습니다.
   * 지갑은 금액과 수신자를 설정한 후 ZK 증명을 요청합니다. RPC 제공자는 기본적으로 ZK 증명을 생성하여 반환합니다.

3. Solana 런타임은 서명을 검증하고 Solana 프라이버시 프로그램을 호출하여 암호화된 상태를 공개하지 않고 ZK 증명을 검증합니다.

4. 앱은 Solana 거래 해시를 통해 상태를 추적합니다.

```mermaid theme={"system"}
%%{init: {
  'theme': 'base',
  'themeVariables': {
    'lineColor':           '#FF6B35',
    'primaryTextColor':    '#737373',
    'primaryBorderColor':  '#9CA3AF',
    'actorBkg':            '#FFFFFF',
    'actorBorder':         '#9CA3AF',
    'actorTextColor':      '#737373',
    'signalColor':         '#FF6B35',
    'signalTextColor':     '#737373',
    'labelBoxBkgColor':    '#FF6B351F',
    'labelBoxBorderColor': '#FF6B35',
    'noteBkgColor':        '#F5F5F5',
    'noteTextColor':       '#737373',
    'noteBorderColor':     '#9CA3AF'
  }
}}%%
sequenceDiagram
    participant Wallet
    participant RPC as RPC Provider
    participant Solana

    Wallet->>RPC: Fetch encrypted state
    RPC-->>Wallet: Encrypted state
    Note over Wallet: Decrypt

    Note over Wallet: Set amount and recipient
    Wallet->>RPC: Request ZK proof
    Note over RPC: Generate ZK proof
    RPC-->>Wallet: ZK proof
    Note over Wallet: Build transaction, sign

    Wallet->>RPC: Submit Transaction
    RPC->>Solana: Forward transaction
    Note over Solana: Verify signatures
    Note over Solana: CPI Solana Privacy Program
    Note over Solana: Verify ZK proof
    RPC-->>Wallet: Transaction signature
```

<Accordion title="Solana 전송과 비교">
  1. 사용자의 SOL 또는 SPL 잔액은 온체인에서 공개됩니다.
  2. 지갑은 공개 상태를 읽고 전송을 생성하며, 소유자는 서명합니다.
  3. Solana 런타임은 서명을 검증하고 시스템 프로그램 또는 토큰 프로그램을 호출하여 공개 잔액을 업데이트합니다.
  4. 앱은 Solana 거래 해시를 통해 상태를 추적합니다.

  ```mermaid theme={"system"}
  %%{init: {
    'theme': 'base',
    'themeVariables': {
      'lineColor':           '#FF6B35',
      'primaryTextColor':    '#737373',
      'primaryBorderColor':  '#9CA3AF',
      'actorBkg':            '#FFFFFF',
      'actorBorder':         '#9CA3AF',
      'actorTextColor':      '#737373',
      'signalColor':         '#FF6B35',
      'signalTextColor':     '#737373',
      'labelBoxBkgColor':    '#FF6B351F',
      'labelBoxBorderColor': '#FF6B35',
      'noteBkgColor':        '#F5F5F5',
      'noteTextColor':       '#737373',
      'noteBorderColor':     '#9CA3AF'
    }
  }}%%
  sequenceDiagram
      participant Wallet
      participant RPC
      participant Solana

      Wallet->>RPC: Get public balance
      RPC-->>Wallet: Public state
      Note over Wallet: Build transfer, owner signs
      Wallet->>RPC: sendTransaction
      RPC->>Solana: Forward transaction
      Note over Solana: Verify signatures
      Note over Solana: CPI System / Token Program
      Note over Solana: Update balance
      RPC-->>Wallet: Transaction signature
  ```
</Accordion>

<Info>
  이는 허가 없는 기밀 링의 고급 거래 흐름입니다.
  [개념](/docs/ko/privacy/concepts#고급-거래-흐름)에서 사용자 정의 링과 비교하십시오.
</Info>

# 시작하기

<Tabs>
  <Tab title="TypeScript 클라이언트">
    <Steps>
      <Step>
        ### 사전 준비 사항

        <Info>
          The TypeScript examples require Node.js 24 or later, pnpm 11.18.0, and the Solana CLI.
        </Info>

        ```bash theme={"system"}
        pnpm add @heliuslabs/zolana @solana/kit
        ```

        Source: [sdk-libs/ts](https://github.com/helius-labs/zolana/tree/main/sdk-libs/ts)

        <Accordion title="Connect to Endpoints">
          <Tabs>
            <Tab title="Devnet">
              ```bash theme={"system"}
              pnpm install
              cp .env.example .env
              ```

              Add a [Helius API key](https://dashboard.helius.dev/):

              ```bash .env theme={"system"}
              API_KEY=YOUR_API_KEY
              ZOLANA_PAYER_KEYPAIR=~/.config/solana/id.json
              ```

              ```ts theme={"system"}
              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,
              });
              ```

              The examples use the Solana CLI wallet as the payer by default. The payer must hold devnet SOL. See [How to Get Devnet SOL](/docs/rpc/devnet-sol).
            </Tab>

            <Tab title="Localnet">
              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.

              ```bash theme={"system"}
              cargo install --git https://github.com/helius-labs/zolana --tag v0.1.0-alpha zolana-cli
              zolana dev start
              ```

              ```ts theme={"system"}
              import { createZolanaClient } from "@heliuslabs/zolana";

              const client = await createZolanaClient({});
              ```
            </Tab>
          </Tabs>
        </Accordion>
      </Step>

      <Step>
        ### 개인 잔액으로 전송

        <Accordion title="Solana Kit 전송 도우미">
          ```typescript theme={"system"}
          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 거래를 생성하고, 제출하며 서명 및 도달한 슬롯을 반환합니다.
        </Accordion>

        <Tabs>
          <Tab title="SOL">
            ```typescript theme={"system"}
            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,
              ]);
            ```
          </Tab>

          <Tab title="SPL">
            ```typescript theme={"system"}
            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,
              ]);
            ```
          </Tab>
        </Tabs>

        <AccordionGroup>
          <Accordion title="1. 사용할 개인 토큰 계정 선택">
            ```typescript theme={"system"}
            import { SOL_MINT } from "@heliuslabs/zolana";

            const transferUtxo = depositBalance.utxos[0]!;
            ```

            * 예제는 이전 입금에 의해 생성된 개인 솔라나 토큰 계정을 사용합니다. 전송은 여러 UTXO를 사용할 수 있습니다.
            * `transferUtxo`는 해당 잔액에서 하나의 개인 솔라나 토큰 계정을 선택합니다.
          </Accordion>

          <Accordion title="2. 증명 입력 준비">
            ```typescript theme={"system"}
            import { ProofInputUtxo } from "@heliuslabs/zolana/transaction";

            const transferInput =
              ProofInputUtxo.fromKeypair(
                transferUtxo,
                senderKeypair,
              );
            ```

            * `ProofInputUtxo.fromKeypair`는 발신자의 개인 지갑 키페어로 선택된 UTXO를 증명 입력으로 준비합니다.
            * 키페어는 입력 UTXO를 소비한 것으로 표시하는 무효화를 파생하며, 자산과 금액은 암호화된 상태로 유지됩니다.
          </Accordion>

          <Accordion title="3. 기밀 전송 생성 및 서명">
            ```typescript theme={"system"}
            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`는 발신자의 <Tooltip tip="사용자 레지스트리 기록에 게시된 공개키 번들 (서명, 무효화 및 보기 키). 온체인 주소는 아님.">보호된 주소</Tooltip>입니다. 전송은 이 지갑에서 사용됩니다.
            * `[transferInput]`는 발신자의 선택된 UTXO 목록입니다. 전송은 여러 UTXO를 사용할 수 있습니다.
            * `senderSigner.address`는 수수료를 지불하는 솔라나 주소입니다. 기밀 전송을 위해 가스 후원자가 이 역할을 수행할 수 있습니다.
            * `recipient`는 수신자의 보호된 주소입니다. 전송된 출력은 수신자의 보기 키에 암호화됩니다.
            * `SOL_MINT`는 SOL을 선택합니다. SPL 또는 Token 2022 전송은 토큰 민트를 전달합니다.
            * `TRANSFER_AMOUNT`는 자산의 기본 단위로 표시됩니다. SOL은 램포트를 사용합니다. SPL과 Token 2022 자산은 토큰의 기본 단위를 사용합니다.
            * `transfer.sign`는 상태 전환을 승인하고 자산과 금액을 암호화하며, 제로 지식 증명기를 위한 입력을 생성합니다.
            * `assets`는 지원되는 개인 자산을 해결하는 데 사용되는 자산 레지스트리입니다.
          </Accordion>

          <Accordion title="4. 영지식 증명 가져오기">
            ```typescript theme={"system"}
            import { createZolanaClient } from "@heliuslabs/zolana";

            const transferData = await client.proveTransact(
              transferProofInputs,
            );
            ```

            * `client.proveTransact`는 서명된 전송에서 영지식 증명을 생성하고 직렬화된 명령어 데이터를 반환합니다.
            * 증명은 발신자가 자산이나 금액을 드러내지 않고 입력을 소모할 수 있음을 보여줍니다.
          </Accordion>

          <Accordion title="5. 전송 명령 생성">
            ```typescript theme={"system"}
            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와 수신자 출력 및 발신자 변경을 받는 상태 머클 트리를 포함합니다.
            * `data`는 이전 단계에서 생성된 영지식 증명과 암호화된 출력을 포함합니다.
            * 개인 전송은 자산을 개인 잔액 간에만 이동하며, 공개 솔라나 계정이나 토큰 계정을 통과하지 않습니다.
          </Accordion>

          <Accordion title="6. 일반 Solana 거래처럼 전송">
            ```typescript theme={"system"}
            import { sendAndConfirmFactory } from "../src/lib.js";

            const transferTx = await sendAndConfirm([
              transferInstruction,
            ]);
            ```

            * `sendAndConfirm`는 Solana 거래로 `transferInstruction`에 서명하고 제출합니다.
            * 확인 결과는 인덱서 가져오기를 제한하기 위해 사용된 도달 슬롯을 제공합니다.
          </Accordion>
        </AccordionGroup>
      </Step>
    </Steps>

    ### 전체 코드 예제

    예제를 복제하고 실행하세요:

    ```bash theme={"system"}
    git clone https://github.com/helius-labs/zolana-examples.git
    cd zolana-examples/typescript-client
    pnpm install
    pnpm example examples/deposit_transfer_withdraw.ts
    ```

    <Info>
      예제는 [여기](https://github.com/helius-labs/zolana-examples/blob/main/typescript-client/examples/deposit_transfer_withdraw.ts)에서 로컬/devnet에서 기밀 링을 사용합니다.
    </Info>

    ```typescript deposit_transfer_withdraw.ts expandable theme={"system"}
    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();
    ```
  </Tab>

  <Tab title="Rust 클라이언트">
    <Steps>
      <Step>
        ### 사전 준비 사항

        <Info>
          The Rust examples require the latest stable Rust toolchain and the Solana CLI v4.0.2. See the [Solana installation guide](https://solana.com/docs/intro/installation).
        </Info>

        ```toml Cargo.toml theme={"system"}
        [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" }
        ```

        Source: [sdk-libs/client](https://github.com/helius-labs/zolana/tree/v0.1.0-alpha/sdk-libs/client)

        <Accordion title="Connect to Endpoints">
          <Tabs>
            <Tab title="Devnet">
              Add a [Helius API key](https://dashboard.helius.dev/):

              ```bash .env theme={"system"}
              API_KEY=YOUR_API_KEY
              ZOLANA_PAYER_KEYPAIR=~/.config/solana/id.json
              ```

              ```rust theme={"system"}
              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,
              );
              ```

              The examples use the Solana CLI wallet as the payer by default. The payer must hold devnet SOL. See [How to Get Devnet SOL](/docs/rpc/devnet-sol).
            </Tab>

            <Tab title="Localnet">
              ```bash theme={"system"}
              cargo install --git https://github.com/helius-labs/zolana --tag v0.1.0-alpha zolana-cli
              zolana dev start
              ```

              ```rust theme={"system"}
              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,
              )?;
              ```
            </Tab>
          </Tabs>
        </Accordion>
      </Step>

      <Step>
        ### 개인 잔액으로 전송

        ```rust theme={"system"}
        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();
        ```

        <AccordionGroup>
          <Accordion title="1. 사용할 개인 토큰 계정 선택">
            ```rust theme={"system"}
            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)`를 보여줍니다.
          </Accordion>

          <Accordion title="2. 증명 입력 준비">
            ```rust theme={"system"}
            use zolana_transaction::instructions::types::SppProofInputUtxo;

            let transfer_input_utxo = SppProofInputUtxo::new(transfer_utxo, &sender);
            ```

            * `SppProofInputUtxo::new`는 발신자의 개인 지갑 키페어로 선택된 UTXO를 증명 입력으로 준비합니다.
            * 키페어는 입력 UTXO를 소비한 것으로 표시하는 무효화를 파생하며, 자산과 금액은 암호화된 상태로 유지됩니다.
          </Accordion>

          <Accordion title="3. 기밀 전송 생성 및 서명">
            ```rust theme={"system"}
            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`는 발신자의 <Tooltip tip="사용자 레지스트리 기록에 게시된 공개키 번들 (서명, 무효화 및 보기 키). 온체인 주소는 아님.">보호된 주소</Tooltip>입니다. 전송은 이 지갑에서 사용됩니다.
            * `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`는 지원되는 개인 자산을 해결하는 데 사용되는 자산 레지스트리입니다.
          </Accordion>

          <Accordion title="4. 영지식 증명 가져오기">
            ```rust theme={"system"}
            use zolana_client::Rpc;

            let transfer_data = client.prove_transact(tree, proof_inputs, None)?;
            ```

            * `client.prove_transact`는 서명된 전송에서 영지식 증명을 생성하고 직렬화된 명령어 데이터를 반환합니다.
            * `tree`는 입력 UTXO 멤버십을 증명하는 데 사용되는 루트를 가진 상태 머클 트리를 식별합니다.
            * 증명은 발신자가 자산이나 금액을 드러내지 않고 입력을 소모할 수 있음을 보여줍니다.
          </Accordion>

          <Accordion title="5. 전송 명령 생성">
            ```rust theme={"system"}
            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를 포함하는 상태 머클 트리를 식별합니다.
            * `output_tree`는 수신자 출력 및 발신자 변화를 받는 커밋을 포함하는 상태 머클 트리를 식별합니다.
            * `interface_transfer_accounts`는 개인 전송이 자산을 개인 잔액 간에만 이동시키고 Solana 계정이나 토큰 계정의 공공 잔액과 상호작용하지 않기 때문에 비어 있습니다.
            * `owner_signers`는 이 기밀 전송에 대해 비어 있습니다.
            * `data`는 이전 단계에서 생성된 영지식 증명과 암호화된 출력을 포함합니다.
          </Accordion>

          <Accordion title="6. 일반 Solana 거래처럼 전송">
            ```rust theme={"system"}
            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`는 Solana 거래로 `transfer_ix`에 서명하고 제출합니다.
            * `landed_slot`는 인덱서 fetch에 사용된 확인 슬롯을 읽습니다.
            * `sender_solana_keypair`는 수수료를 지불하고 전송을 승인합니다.
          </Accordion>
        </AccordionGroup>
      </Step>
    </Steps>

    ## 전체 코드 예제

    예제를 복제하고 실행하세요:

    ```bash theme={"system"}
    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
    ```

    <Info>
      예제는 [여기](https://github.com/helius-labs/zolana-examples/blob/main/rust-client/examples/deposit_transfer_withdraw.rs)에서 로컬/devnet에서 기밀 링을 사용합니다.
    </Info>

    ```rust deposit_transfer_withdraw.rs expandable theme={"system"}
    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"))
    }
    ```
  </Tab>
</Tabs>

## 관련 가이드

<CardGroup cols={2}>
  <Card title="입금" icon="arrow-down-to-bracket" href="/docs/ko/privacy/guides/deposit" horizontal />

  <Card title="출금" icon="arrow-up-from-bracket" href="/docs/ko/privacy/guides/withdraw" horizontal />

  <Card title="개인 잔액 읽기" icon="wallet" href="/docs/ko/privacy/guides/read-balance" horizontal />

  <Card title="개인 기록 읽기" icon="clock-rotate-left" href="/docs/ko/privacy/guides/read-history" horizontal />
</CardGroup>

## Didn't find what you were looking for?

<Callout type="info">
  Reach out! [Telegram](https://t.me/tilo_light) | [E-Mail](mailto:sales@helius.xyz) | [Contact](https://www.helius.dev/contact)
</Callout>
