Deploy & Test
This guide shows TypeScript usage examples for interacting with the Confidential SPL Token program using the Inco Solana SDK.Setup
Copy
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:Copy
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):Copy
// 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:Copy
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:Copy
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
Copy
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
Copy
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
Copy
// 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
Copy
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
Copy
await program.methods
.closeAccount()
.accounts({
account: tokenAccountKeypair.publicKey,
destination: wallet.publicKey, // Receives remaining lamports
authority: wallet.publicKey,
})
.rpc();
Transfer Operations
transfer
Copy
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):Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
await program.methods
.setFreezeAuthority(newAuthorityPubkey)
.accounts({
mint: mintPubkey,
currentAuthority: wallet.publicKey,
})
.rpc();
set_account_owner
Copy
await program.methods
.setAccountOwner(newOwnerPubkey)
.accounts({
account: tokenAccountPubkey,
currentOwner: wallet.publicKey,
})
.rpc();
set_close_authority
Copy
await program.methods
.setCloseAuthority(newAuthorityPubkey)
.accounts({
account: tokenAccountPubkey,
owner: wallet.publicKey,
})
.rpc();
Running Tests
Copy
# 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.