Skip to main content

Deploy & Test

This guide shows TypeScript usage examples for interacting with the Confidential SPL Token program using the Inco Solana SDK.

Setup

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import type { IncoToken } from "../target/types/inco_token";
import { 
  PublicKey, 
  Keypair, 
  SystemProgram,
  Connection,
} from "@solana/web3.js";
import { encryptValue } from "@inco/solana-sdk/encryption";
import { decrypt } from "@inco/solana-sdk/attested-decrypt";
import { hexToBuffer } from "@inco/solana-sdk/utils";

// Connection setup
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);

const program = anchor.workspace.IncoToken as Program<IncoToken>;
const wallet = provider.wallet.payer as Keypair;

Getting Handles

There are two ways to get encrypted handles for decryption:

Option 1: From Transaction Logs

When your program emits handles in logs after an operation:
async function getHandleFromTx(
  txSignature: string,
  logPrefix: string
): Promise<string> {
  // Wait for transaction to be confirmed
  await new Promise(resolve => setTimeout(resolve, 2000));

  const txDetails = await connection.getTransaction(txSignature, {
    commitment: "confirmed",
    maxSupportedTransactionVersion: 0,
  });

  const logs = txDetails?.meta?.logMessages || [];

  for (const log of logs) {
    if (log.includes(logPrefix)) {
      const match = log.match(/(\d+)/);
      if (match) return match[1];
    }
  }

  throw new Error(`Handle not found in logs for prefix: ${logPrefix}`);
}

// Usage
const handle = await getHandleFromTx(txSignature, "Balance handle:");
const result = await decrypt([handle]);

Option 2: From On-Chain Account

When the encrypted handle is stored in an account (like a token balance):
// Fetch the token account
const tokenAccount = await program.account.incoAccount.fetch(tokenAccountPubkey);

// Extract the handle from the account data
const balanceHandle = getHandleFromAccount(tokenAccount.amount);

// Decrypt the balance
const result = await decrypt([balanceHandle]);
console.log("Decrypted balance:", result.plaintexts[0]);

Handle Extraction Helper

Anchor returns BN (Big Number) objects for u128 encrypted handles. Use this helper to extract handles:
function getHandleFromAccount(anchorHandle: any): string {
  // Handle direct BN objects
  if (anchorHandle && anchorHandle._bn) {
    return anchorHandle._bn.toString(10);
  }
  
  // Handle nested BN in {"0": BN} structure
  if (typeof anchorHandle === 'object' && anchorHandle["0"]) {
    const nested = anchorHandle["0"];
    if (nested && nested._bn) {
      return nested._bn.toString(10);
    }
    if (nested && typeof nested.toString === 'function') {
      return nested.toString(10);
    }
  }
  
  // Handle array of bytes
  if (anchorHandle instanceof Uint8Array || Array.isArray(anchorHandle)) {
    const buffer = Buffer.from(anchorHandle);
    let result = BigInt(0);
    for (let i = buffer.length - 1; i >= 0; i--) {
      result = result * BigInt(256) + BigInt(buffer[i]);
    }
    return result.toString();
  }
  
  return "0";
}

Decrypt Balance Helper

Combines handle extraction and decryption:
async function decryptBalance(
  accountData: any, 
  decimals: number = 9
): Promise<number | null> {
  try {
    const handle = getHandleFromAccount(accountData.amount);
    if (handle === "0") return 0;
    
    const result = await decrypt([handle]);
    const rawAmount = parseInt(result.plaintexts[0], 10);
    return rawAmount / Math.pow(10, decimals);
  } catch (error) {
    console.error("Decryption error:", error);
    return null;
  }
}

// Usage
const tokenAccount = await program.account.incoAccount.fetch(tokenAccountPubkey);
const balance = await decryptBalance(tokenAccount);
console.log("Balance:", balance, "tokens");

Mint Operations

initialize_mint

const mintKeypair = Keypair.generate();

await program.methods
  .initializeMint(
    9,                    // decimals
    wallet.publicKey,     // mint authority
    wallet.publicKey      // freeze authority
  )
  .accounts({
    mint: mintKeypair.publicKey,
    payer: wallet.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .signers([mintKeypair])
  .rpc();

// Verify
const mintAccount = await program.account.incoMint.fetch(mintKeypair.publicKey);
console.log("Decimals:", mintAccount.decimals);
console.log("Initialized:", mintAccount.isInitialized);

mint_to

const mintAmount = BigInt(1_000_000_000); // 1 token with 9 decimals
const encryptedAmount = await encryptValue(mintAmount);

await program.methods
  .mintTo(hexToBuffer(encryptedAmount), 0)
  .accounts({
    mint: mintKeypair.publicKey,
    account: tokenAccountKeypair.publicKey,
    mintAuthority: wallet.publicKey,
  })
  .rpc();

// Wait for handle storage, then decrypt balance
await new Promise(resolve => setTimeout(resolve, 3000));

const tokenAccount = await program.account.incoAccount.fetch(tokenAccountKeypair.publicKey);
const balance = await decryptBalance(tokenAccount);
console.log("Balance after mint:", balance, "tokens");

mint_to_with_handle

// Use an existing handle (e.g., from another account's balance)
const sourceAccount = await program.account.incoAccount.fetch(sourceAccountPubkey);
const amountHandle = sourceAccount.amount;

await program.methods
  .mintToWithHandle(amountHandle)
  .accounts({
    mint: mintKeypair.publicKey,
    account: tokenAccountKeypair.publicKey,
    mintAuthority: wallet.publicKey,
  })
  .rpc();

Account Operations

initialize_account

const tokenAccountKeypair = Keypair.generate();

await program.methods
  .initializeAccount()
  .accounts({
    account: tokenAccountKeypair.publicKey,
    mint: mintKeypair.publicKey,
    owner: wallet.publicKey,
    payer: wallet.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .signers([tokenAccountKeypair])
  .rpc();

// Verify
const tokenAccount = await program.account.incoAccount.fetch(tokenAccountKeypair.publicKey);
console.log("Owner:", tokenAccount.owner.toBase58());
console.log("State:", tokenAccount.state);

close_account

await program.methods
  .closeAccount()
  .accounts({
    account: tokenAccountKeypair.publicKey,
    destination: wallet.publicKey,  // Receives remaining lamports
    authority: wallet.publicKey,
  })
  .rpc();

Transfer Operations

transfer

const transferAmount = BigInt(250_000_000); // 0.25 tokens
const encryptedAmount = await encryptValue(transferAmount);

await program.methods
  .transfer(hexToBuffer(encryptedAmount), 0)
  .accounts({
    source: sourceAccountPubkey,
    destination: destinationAccountPubkey,
    authority: wallet.publicKey,
  })
  .rpc();

// Verify balances
await new Promise(resolve => setTimeout(resolve, 3000));

const sourceAccount = await program.account.incoAccount.fetch(sourceAccountPubkey);
const destAccount = await program.account.incoAccount.fetch(destinationAccountPubkey);

console.log("Source balance:", await decryptBalance(sourceAccount));
console.log("Destination balance:", await decryptBalance(destAccount));

transfer_with_handle

Transfer using an existing encrypted handle (e.g., transfer entire balance):
const sourceAccount = await program.account.incoAccount.fetch(sourceAccountPubkey);
const amountHandle = sourceAccount.amount;

await program.methods
  .transferWithHandle(amountHandle)
  .accounts({
    source: sourceAccountPubkey,
    destination: destinationAccountPubkey,
    authority: wallet.publicKey,
  })
  .rpc();

Delegation Operations

approve

const approveAmount = BigInt(100_000_000); // 0.1 tokens
const encryptedAmount = await encryptValue(approveAmount);

await program.methods
  .approve(hexToBuffer(encryptedAmount), 0)
  .accounts({
    source: tokenAccountPubkey,
    delegate: delegatePubkey,
    owner: wallet.publicKey,
  })
  .rpc();

// Verify
const tokenAccount = await program.account.incoAccount.fetch(tokenAccountPubkey);
console.log("Delegate:", tokenAccount.delegate);

revoke

await program.methods
  .revoke()
  .accounts({
    source: tokenAccountPubkey,
    owner: wallet.publicKey,
  })
  .rpc();

// Verify
const tokenAccount = await program.account.incoAccount.fetch(tokenAccountPubkey);
console.log("Delegate after revoke:", tokenAccount.delegate); // { none: {} }

Burn Operations

burn

const burnAmount = BigInt(100_000_000); // 0.1 tokens
const encryptedAmount = await encryptValue(burnAmount);

await program.methods
  .burn(hexToBuffer(encryptedAmount), 0)
  .accounts({
    account: tokenAccountPubkey,
    mint: mintPubkey,
    authority: wallet.publicKey,
  })
  .rpc();

// Verify
await new Promise(resolve => setTimeout(resolve, 3000));

const tokenAccount = await program.account.incoAccount.fetch(tokenAccountPubkey);
const balance = await decryptBalance(tokenAccount);
console.log("Balance after burn:", balance, "tokens");

Freeze/Thaw Operations

freeze_account

await program.methods
  .freezeAccount()
  .accounts({
    account: tokenAccountPubkey,
    mint: mintPubkey,
    freezeAuthority: wallet.publicKey,
  })
  .rpc();

// Verify
const tokenAccount = await program.account.incoAccount.fetch(tokenAccountPubkey);
console.log("State:", tokenAccount.state); // { frozen: {} }

thaw_account

await program.methods
  .thawAccount()
  .accounts({
    account: tokenAccountPubkey,
    mint: mintPubkey,
    freezeAuthority: wallet.publicKey,
  })
  .rpc();

// Verify
const tokenAccount = await program.account.incoAccount.fetch(tokenAccountPubkey);
console.log("State:", tokenAccount.state); // { initialized: {} }

Authority Management

set_mint_authority

await program.methods
  .setMintAuthority(newAuthorityPubkey)
  .accounts({
    mint: mintPubkey,
    currentAuthority: wallet.publicKey,
  })
  .rpc();

// Disable minting permanently (set to null)
await program.methods
  .setMintAuthority(null)
  .accounts({
    mint: mintPubkey,
    currentAuthority: wallet.publicKey,
  })
  .rpc();

set_freeze_authority

await program.methods
  .setFreezeAuthority(newAuthorityPubkey)
  .accounts({
    mint: mintPubkey,
    currentAuthority: wallet.publicKey,
  })
  .rpc();

set_account_owner

await program.methods
  .setAccountOwner(newOwnerPubkey)
  .accounts({
    account: tokenAccountPubkey,
    currentOwner: wallet.publicKey,
  })
  .rpc();

set_close_authority

await program.methods
  .setCloseAuthority(newAuthorityPubkey)
  .accounts({
    account: tokenAccountPubkey,
    owner: wallet.publicKey,
  })
  .rpc();

Running Tests

# Build the program
anchor build

# Run tests on devnet
anchor test --provider.cluster devnet
Add await new Promise(resolve => setTimeout(resolve, 3000)) after operations that modify encrypted state to ensure handles are stored before decryption.