Typescript
Build a Typescript client to create or interact with compressed accounts. Includes a step-by-step implementation guide and full code examples.
The TypeScript Client SDK provides two test environments:
For local testing, use
TestRpc.TestRpcis an in-memory indexer that parses events and builds Merkle trees on-demand to generate proofs instantly without persisting state.Requires a running test validator with Light System Programs and Merkle tree accounts.
For test-validator, devnet and mainnet use
Rpc.Rpcis a thin wrapper extending Solana's web3.jsConnectionclass with compression-related endpoints. Find a full list of JSON RPC methods here.Connects to Photon indexer to query compressed accounts and prover service to generate validity proofs.
RpcandTestRpcimplement the sameCompressionApiInterface. Seamlessly switch betweenTestRpc, local test validator, and public Solana networks.
Find full code examples at the end for Anchor.
Implementation Guide
This guide covers the components of a Typescript client. Here is the complete flow:










Dependencies
npm install --save \
@lightprotocol/[email protected] \
@lightprotocol/[email protected] \
@solana/web3.jsyarn add \
@lightprotocol/[email protected] \
@lightprotocol/[email protected] \
@solana/web3.jspnpm add \
@lightprotocol/[email protected] \
@lightprotocol/[email protected] \
@solana/web3.jsEnvironment
Connect to local, devnet or mainnet with Rpc.
import { createRpc } from '@lightprotocol/stateless.js';
const rpc = createRpc('https://mainnet.helius-rpc.com/?api-key=YOUR_API_KEY');For Helius mainnet: The endpoint serves RPC, Photon indexer, and prover API.
import { createRpc } from '@lightprotocol/stateless.js';
const rpc = createRpc('https://devnet.helius-rpc.com/?api-key=YOUR_API_KEY');For Helius devnet: The endpoint serves RPC, Photon indexer, and prover API.
Start a local test-validator with the below command. It will start a single-node Solana cluster, an RPC node, and a prover node at ports 8899, 8784, and 3001.
light test-validatorThen connect to it:
import { createRpc } from '@lightprotocol/stateless.js';
const rpc = createRpc();Set up test environment with TestRpc.
import { getTestRpc } from '@lightprotocol/stateless.js';
import { LightWasm, WasmFactory } from '@lightprotocol/hasher.rs';
const lightWasm: LightWasm = await WasmFactory.getInstance();
const testRpc = await getTestRpc(lightWasm);Tree Configuration
Before creating a compressed account, your client must fetch metadata of two Merkle trees:
an address tree to derive and store the account address and
a state tree to store the compressed account hash.
The protocol maintains Merkle trees. You don't need to initialize custom trees. Find the addresses for Merkle trees here.
import { getDefaultAddressTreeInfo } from '@lightprotocol/stateless.js';
const addressTree = getDefaultAddressTreeInfo();
const stateTreeInfos = await rpc.getStateTreeInfos();
const outputStateTree = selectStateTreeInfo(stateTreeInfos);V1:
getDefaultAddressTreeInfo()returnsTreeInfowith the public key and other metadata for the address tree.
const addressTree = await rpc.getAddressTreeInfoV2();
const stateTreeInfos = await rpc.getStateTreeInfos();
const outputStateTree = selectStateTreeInfo(stateTreeInfos);V2:
Address Trees: getDefaultAddressTreeInfo() / rpc.getAddressTreeInfoV2() returns TreeInfo with the public key and other metadata for the address tree.
TreeInfois usedto derive addresses and
for
getValidityProofV0()to prove the address does not exist yet.
State Trees:
getStateTreeInfos()returnsTreeInfo[]with pubkeys and metadata for all active state trees.selectStateTreeInfo()selects a random state tree to store the compressed account hash.Selecting a random state tree prevents write-lock contention on state trees and increases throughput.
Account hashes can move to different state trees after each state transition.
Best practice is to minimize different trees per transaction. Still, since trees fill up over time, programs must handle accounts from different state trees within the same transaction.
Derive Address
Derive a persistent address as a unique identifier for your compressed account.
Use the derivation method that matches your address tree type from the previous step.
const seed = deriveAddressSeed(
[Buffer.from('my-seed')],
programId
);
const address = deriveAddress(seed, addressTree.tree);Derive the seed:
Pass arbitrary byte slices in the array to uniquely identify the account
Specify
programIdto combine with your seeds
Then, derive the address:
Pass the derived 32-byte
seedfrom the first stepSpecify
addressTree.treepubkey
const seed = deriveAddressSeedV2(
[Buffer.from('my-seed')]
);
const address = deriveAddressV2(seed, addressTree.tree, programId);Derive the seed:
Pass arbitrary byte slices in the array to uniquely identify the account
Then, derive the address:
Pass the derived 32-byte
seedfrom the first stepSpecify
addressTree.treepubkeySpecify
programIdin the address derivation. V2 includes it here instead of in the seed.
Validity Proof
Fetch a validity proof from your RPC provider that supports ZK Compression (Helius, Triton, ...). The proof type depends on the operation:
To create a compressed account, you must prove the address doesn't already exist in the address tree.
To update or close a compressed account, you must prove its account hash exists in a state tree.
You can combine multiple addresses and hashes in one proof to optimize compute cost and instruction data.
const proof = await rpc.getValidityProofV0(
[],
[{ address: bn(address.toBytes()), tree: addressTree.tree, queue: addressTree.queue }]
);Pass these parameters:
Leave (
[]) empty to create compressed accounts, since no compressed account exists yet.Specify the new address with its tree and queue pubkeys in
[{ address: bn(address.toBytes()), tree, queue }]and convert it to the required format.
The RPC returns proof result with
compressedProof: The proof that the address does not exist in the address tree, passed to the program in your instruction data.rootIndices: An array with root index from the validity proof for the address tree.Empty
leafIndicesarray, since no compressed account exists yet.
const hash = compressedAccount.hash;
const tree = compressedAccount.treeInfo.tree;
const queue = compressedAccount.treeInfo.queue;
const proof = await rpc.getValidityProofV0(
[{ hash, tree, queue }],
[]
);Pass these parameters:
Specify the account hash with its tree and queue pubkeys in
[{ hash, tree, queue }].Get
treeandqueuefromcompressedAccount.treeInfo.treeandcompressedAccount.treeInfo.queue.(
[]) remains empty, since the proof verifies the account hash exists in a state tree, not that the address doesn't exist in an address tree.
The RPC returns proof result with
compressedProof: The proof that the account hash exists in the state tree, passed to the program in your instruction data.rootIndicesandleafIndicesarrays with proof metadata to pack accounts in the next step.
const hash = compressedAccount.hash;
const tree = compressedAccount.treeInfo.tree;
const queue = compressedAccount.treeInfo.queue;
const proof = await rpc.getValidityProofV0(
[{ hash, tree, queue }],
[{ address: bn(address.toBytes()), tree: addressTree.tree, queue: addressTree.queue }]
);Pass these parameters:
Specify one or more existing account hashes with their tree and queue pubkeys in
[{ hash, tree, queue }].Specify one or more new addresses with their tree and queue pubkeys in
[{ address: bn(address.toBytes()), tree, queue }].
The RPC returns proof result with
compressedProof: A single combined proof that verifies both the account hash exists in the state tree and the address does not exist in the address tree, passed to the program in your instruction data.rootIndicesandleafIndicesarrays with proof metadata to buildPackedAddressTreeInfoandPackedStateTreeInfoin the next step.
Supported Combinations and Maximums
The specific combinations and maximums depend on the circuit version (v1 or v2) and the proof type.
Combine multiple hashes or multiple addresses in a single proof, or
multiple hashes and addresses in a single combined proof.
V1 circuits can prove in a single proof
1, 2, 3, 4, or 8 hashes,
1, 2, 3, 4, or 8 addresses, or
multiple hashes or addresses in any combination of the below.
Single Combined Proofs
Any combination of
Hashes
1, 2, 3, 4, 8
Addresses
1, 2, 4, 8
V2 circuits can prove in a single proof
1 to 20 hashes,
1 to 32 addresses, or
multiple hashes or addresses in any combination of the below.
Single Combined Proofs
Any combination of
Hashes
1 to 4
Addresses
1 to 4
Pack Accounts
To optimize instruction data we pack accounts into an array:
Every packed account is assigned to an u8 index.
Indices are included in instruction data, instead of 32 byte pubkeys.
The indices point to the
remainingAccountsin Anchor.
1. Initialize PackedAccounts
import { PackedAccounts, SystemAccountMetaConfig } from '@lightprotocol/stateless.js';
const packedAccounts = new PackedAccounts();PackedAccounts creates the accounts array that you'll pass to .remainingAccounts(). It automatically:
Assigns pubkeys sequential u8 indices, and
deduplicates pubkeys to make sure each unique pubkey appears only once in the array.
For example, if the input state tree is the same as the output state tree, both reference the same pubkey and return the same index.
2. Add Light System Accounts
Add the Light System accounts your program needs to create and interact with compressed accounts.
const systemAccountConfig = new SystemAccountMetaConfig(programId);
packedAccounts.addSystemAccounts(systemAccountConfig);Pass your program ID in
new SystemAccountMetaConfig(programId)to derive the CPI signer PDACall
addSystemAccounts(systemAccountConfig)- the SDK will add 8 Light System accounts in the sequence below
3. Pack Tree Accounts from Validity Proof
getValidityProofV0() returns pubkeys and other metadata of Merkle trees. You will convert the pubkeys to u8 indices that reference accounts in the remainingAccounts array to optimize your instruction data.
const addressTreeIndex = packedAccounts.insertOrGet(addressTree.tree);
const addressQueueIndex = packedAccounts.insertOrGet(addressTree.queue);
const packedAddressTreeInfo = {
addressMerkleTreePubkeyIndex: addressTreeIndex,
addressQueuePubkeyIndex: addressQueueIndex,
rootIndex: proof.rootIndices[0]
};Call
insertOrGet()with each tree and queue pubkey from the validity proofCreate
PackedAddressTreeInfowith three fields:
addressMerkleTreePubkeyIndex: Points to the address tree account inremainingAccountsThe address tree is used to derive addresses and verify the address does not already exist
addressQueuePubkeyIndex: Points to the address queue account inremainingAccountsThe queue buffers new addresses before they are inserted into the address tree
rootIndex: The Merkle root index fromproof.rootIndices[0](Validity Proof step)Specifies the root to verify the address does not exist in the tree
const merkleTreePubkeyIndex = packedAccounts.insertOrGet(compressedAccount.treeInfo.tree);
const queuePubkeyIndex = packedAccounts.insertOrGet(compressedAccount.treeInfo.queue);
const outputStateTreeIndex = packedAccounts.insertOrGet(outputStateTree.tree);
const compressedAccountMeta = {
treeInfo: {
merkleTreePubkeyIndex,
queuePubkeyIndex,
leafIndex: compressedAccount.leafIndex,
rootIndex: proof.rootIndices[0],
proveByIndex: false
},
address: compressedAccount.address,
outputStateTreeIndex
};Call
insertOrGet()with the output state treeCreate
compressedAccountMetawith three top-level fields:treeInfo: State tree metadata with u8 indices pointing to accounts inremainingAccountsaddress: The compressed account's addressoutputStateTreeIndex: Points to the output state tree
treeInfo fields (PackedStateTreeInfo):
merkleTreePubkeyIndex: Points to the state tree account inremainingAccountsThe state tree stores the existing account hash that Light System Program verifies
queuePubkeyIndex: Points to the nullifier queue account inremainingAccountsThe queue tracks nullified (spent) account hashes to prevent double-spending
leafIndex: The leaf position in the Merkle tree fromcompressedAccount.leafIndexSpecifies which leaf contains your account hash to verify it exists in the tree
rootIndex: The Merkle root index fromproof.rootIndices[0]Specifies the root to verify the account hash against
proveByIndex: The proof verification mode.falsefor v1 state treestruefor v2 state trees
4. Add Output State Tree
Specify the state tree to store the new account hash.
const outputTreeIndex = packedAccounts.insertOrGet(outputStateTree.tree);Use
outputStateTreevariable from Tree Configuration (Step 3) with theTreeInfocontaining pubkey and metadata for the randomly selected state treeCall
insertOrGet(outputStateTree.tree)to add the tree and get its index for instruction data
5. Finalize Accounts
const { remainingAccounts } = packedAccounts.toAccountMetas();Call toAccountMetas() to build the complete accounts structure for .remainingAccounts().
Packed struct indices reference accounts by their position in this array.
The method returns an object with a
remainingAccountsproperty containing theAccountMeta[]array.
The method returns accounts in two sections:
[systemAccounts] [packedAccounts]
↑ ↑
Light System Merkle tree &
accounts queue accounts
System accounts as
read-only(indices 0-7):System accounts like the noop program log state changes but don't modify their own state.
Light System Program expects these accounts at these exact positions.
Tree and queue accounts as
writable(indices 8+):All tree and queue accounts with writable flags in sequential order.
Light System Program writes new hashes and addresses to these accounts.
6. Summary
You built the remainingAccounts array to merge accounts into an array:
Light System accounts to create and interact with compressed accounts via the Light System Program
Tree accounts from the validity proof to prove address non-existence (create) or existence of the account hash (update/close/reinit/burn)
The output state tree to store the new account hash
The accounts receive a sequential u8 index. Instruction data references accounts via these indices in this order.
PackedAddressTreeInfo and PackedStateTreeInfo structs contain indices instead of 32-byte pubkeys, to reduce instruction data size to 1 byte per index.
Instruction Data
Build your instruction data with the validity proof, tree account indices, and complete account data.
const proof = {
0: proofRpcResult.compressedProof,
};
const instructionData = {
proof,
addressTreeInfo: packedAddressTreeInfo,
outputStateTreeIndex: outputTreeIndex,
};Validity Proof
Add and wrap the
compressedProofyou fetched to prove that the address does not exist yet in the specified address tree.
Specify Merkle trees to store address and account hash
Include the Merkle tree metadata from the Pack Accounts section:
PackedAddressTreeInfospecifies the index to the address tree account used to derive the address. The index points to the address tree account in the accounts array.outputStateTreeIndexpoints to the state tree account in the accounts array that will store the compressed account hash.
Pass initial account data
Add custom fields to your instruction struct for any initial data your program requires.
This example passes a
messagefield to define the initial state of the account.
const proof = {
0: proofRpcResult.compressedProof,
};
const instructionData = {
proof,
accountMeta: {
treeInfo: packedStateTreeInfo,
address: compressedAccount.address,
outputStateTreeIndex: outputTreeIndex
},
currentMessage: currentAccount.message,
newMessage,
};Validity Proof
Add and wrap the
compressedProofyou fetched to prove the account hash exists in the state tree.
Specify input hash and output state tree
Include the Merkle tree metadata from the Pack Accounts section:
accountMetapoints to the input hash and specifies the output state tree with these fields:treeInfo: PackedStateTreeInfopoints to the existing account hash that will be nullified by the Light System Programaddressspecifies the account's derived addressoutputStateTreeIndexpoints to the state tree that will store the updated compressed account hash
Pass current account data
Pass the complete current account data. The program reconstructs the existing account hash from this data to verify it matches the hash in the state tree.
In this example, we pass
currentMessagefrom the fetched account andnewMessagefor the update.
const proof = {
0: proofRpcResult.compressedProof,
};
const instructionData = {
proof,
accountMeta: {
treeInfo: packedStateTreeInfo,
address: compressedAccount.address,
outputStateTreeIndex: outputTreeIndex
},
currentMessage: currentAccount.message,
};Validity Proof
Add and wrap the
compressedProofyou fetched to prove the account hash exists in the state tree.
Specify input hash and output state tree
Include the Merkle tree metadata from the Pack Accounts section:
accountMetapoints to the input hash and specifies the output state tree:treeInfo: PackedStateTreeInfopoints to the existing account hash that will be nullified by the Light System Programaddressspecifies the account's derived addressoutputStateTreeIndexpoints to the state tree that will store the output hash with zero values
Pass current account data
Pass the complete current account data. The program reconstructs the existing account hash from this data to verify it matches the hash in the state tree.
In this example, we pass
currentMessagefrom the fetched account before closing.
const proof = {
0: proofRpcResult.compressedProof,
};
const instructionData = {
proof,
accountMeta: {
treeInfo: packedStateTreeInfo,
address: compressedAccount.address,
outputStateTreeIndex: outputTreeIndex
},
};Validity Proof
Add and wrap the
compressedProofyou fetched to prove the account hash exists in the state tree.
Specify input hash and output state tree
Include the Merkle tree metadata from the Pack Accounts section:
accountMetapoints to the input hash and specifies the output state tree:treeInfo: PackedStateTreeInfopoints to the existing account hash that will be nullified by the Light System Programaddressspecifies the account's derived addressoutputStateTreeIndexpoints to the state tree that will store the reinitialized account hash
Account data initialization
Reinitialize creates an account with default-initialized values (e.g.,
Pubkeyas all zeros, numbers as0, strings as empty).To set custom values, update the account in the same or a separate transaction.
const proof = {
0: proofRpcResult.compressedProof,
};
const instructionData = {
proof,
accountMeta: {
treeInfo: packedStateTreeInfo,
address: compressedAccount.address,
},
currentMessage: currentAccount.message,
};Validity Proof
Add and wrap the
compressedProofyou fetched to prove the account hash exists in the state tree.
Specify input hash
Include the Merkle tree metadata from the Pack Accounts section:
accountMetapoints to the input hash:treeInfo: PackedStateTreeInfopoints to the existing account hash that will be nullified by the Light System Programaddressspecifies the account's derived addressNo
outputStateTreeIndex, since burn does not create output state.
Pass current account data
Pass the complete current account data. The program reconstructs the existing account hash from this data to verify it matches the hash in the state tree.
In this example, we pass
currentMessagefrom the fetched account before burning.
Instruction
Build the instruction with your program_id, accounts, and data from Step 7. Pass the accounts array you built in Step 6.
const instruction = await program.methods
.createAccount(proof, packedAddressTreeInfo, outputStateTreeIndex, message)
.accounts({
signer: payer.publicKey
})
.remainingAccounts(remainingAccounts.toAccountMetas().remainingAccounts)
.instruction();Pass the proof, packed address tree info, output state tree index, and initial account data (e.g., message) as separate parameters to .createAccount().
const instruction = await program.methods
.updateAccount(proof, currentAccount, compressedAccountMeta, newMessage)
.accounts({
signer: payer.publicKey
})
.remainingAccounts(remainingAccounts.toAccountMetas().remainingAccounts)
.instruction();Pass the proof, current account data, compressed account metadata, and new account data as separate parameters to .updateAccount().
const instruction = await program.methods
.closeAccount(proof, compressedAccountMeta, message)
.accounts({
signer: payer.publicKey
})
.remainingAccounts(remainingAccounts.toAccountMetas().remainingAccounts)
.instruction();Pass the proof, compressed account metadata, and current account data as separate parameters to .closeAccount().
const instruction = await program.methods
.reinitAccount(proof, compressedAccountMeta)
.accounts({
signer: payer.publicKey
})
.remainingAccounts(remainingAccounts.toAccountMetas().remainingAccounts)
.instruction();Pass the proof and compressed account metadata as separate parameters to .reinitAccount(). No account data is passed since reinit creates default-initialized zero values.
const instruction = await program.methods
.burnAccount(proof, compressedAccountMeta, currentMessage)
.accounts({
signer: payer.publicKey
})
.remainingAccounts(remainingAccounts.toAccountMetas().remainingAccounts)
.instruction();Pass the proof, compressed account metadata (without outputStateTreeIndex), and current account data as separate parameters to .burnAccount().
What to include in accounts:
Pass program-specific accounts as defined by your program's IDL (signer, feepayer).
Add all remaining accounts with
.remainingAccounts():Light System accounts, added via
PackedAccounts.addSystemAccounts().Merkle tree and queue accounts, added via
PackedAccounts.insertOrGet().
Build the instruction:
Anchor converts
.accounts({ signer })toAccountMeta[]using the program's IDL..remainingAccounts()appends the complete packed accounts array.Returns
TransactionInstructionwithprogramId, mergedkeys, and serialized instructiondata.
Final account array:
[0-N]
Program-specific accounts from .accounts()
(e.g., signer)
[N+1-N+8]
Light System accounts from .remainingAccounts()
[N+9+]
Merkle trees and queues from .remainingAccounts()Full Code Examples
Full TypeScript test examples using local test validator with createRpc().
Install the Light CLI first to download program binaries:
npm -g i @lightprotocol/[email protected]Start local test validator:
light test-validatorThen run tests in a separate terminal:
anchor test --skip-local-validatorFor help with debugging, see the Error Cheatsheet.
// Create Compressed Account Example
import * as anchor from "@coral-xyz/anchor";
import { Program, web3 } from "@coral-xyz/anchor";
import { Create } from "../target/types/create";
import idl from "../target/idl/create.json";
import {
bn,
CompressedAccountWithMerkleContext,
confirmTx,
createRpc,
defaultStaticAccountsStruct,
defaultTestStateTreeAccounts,
deriveAddress,
deriveAddressSeed,
LightSystemProgram,
PackedAccounts,
Rpc,
sleep,
SystemAccountMetaConfig,
} from "@lightprotocol/stateless.js";
import * as assert from "assert";
const path = require("path");
const os = require("os");
require("dotenv").config();
const anchorWalletPath = path.join(os.homedir(), ".config/solana/id.json");
process.env.ANCHOR_WALLET = anchorWalletPath;
describe("test-anchor", () => {
const program = anchor.workspace.Create as Program<Create>;
const coder = new anchor.BorshCoder(idl as anchor.Idl);
it("create compressed account", async () => {
let signer = new web3.Keypair();
let rpc = createRpc(
"http://127.0.0.1:8899",
"http://127.0.0.1:8784",
"http://127.0.0.1:3001",
{
commitment: "confirmed",
},
);
let lamports = web3.LAMPORTS_PER_SOL;
await rpc.requestAirdrop(signer.publicKey, lamports);
await sleep(2000);
const outputStateTree = defaultTestStateTreeAccounts().merkleTree;
const addressTree = defaultTestStateTreeAccounts().addressTree;
const addressQueue = defaultTestStateTreeAccounts().addressQueue;
const messageSeed = new TextEncoder().encode("message");
const seed = deriveAddressSeed(
[messageSeed, signer.publicKey.toBytes()],
new web3.PublicKey(program.idl.address),
);
const address = deriveAddress(seed, addressTree);
// Create compressed account with message
const txId = await createCompressedAccount(
rpc,
addressTree,
addressQueue,
address,
program,
outputStateTree,
signer,
"Hello, compressed world!",
);
console.log("Transaction ID:", txId);
// Wait for indexer to process the transaction
const slot = await rpc.getSlot();
await rpc.confirmTransactionIndexed(slot);
let compressedAccount = await rpc.getCompressedAccount(bn(address.toBytes()));
let myAccount = coder.types.decode(
"MyCompressedAccount",
compressedAccount.data.data,
);
console.log("Decoded data owner:", myAccount.owner.toBase58());
console.log("Decoded data message:", myAccount.message);
// Verify account data
assert.ok(
myAccount.owner.equals(signer.publicKey),
"Owner should match signer public key"
);
assert.strictEqual(
myAccount.message,
"Hello, compressed world!",
"Message should match the created message"
);
});
});
async function createCompressedAccount(
rpc: Rpc,
addressTree: anchor.web3.PublicKey,
addressQueue: anchor.web3.PublicKey,
address: anchor.web3.PublicKey,
program: anchor.Program<Create>,
outputStateTree: anchor.web3.PublicKey,
signer: anchor.web3.Keypair,
message: string,
) {
const proofRpcResult = await rpc.getValidityProofV0(
[],
[
{
tree: addressTree,
queue: addressQueue,
address: bn(address.toBytes()),
},
],
);
const systemAccountConfig = new SystemAccountMetaConfig(program.programId);
let remainingAccounts = new PackedAccounts();
remainingAccounts.addSystemAccounts(systemAccountConfig);
const addressMerkleTreePubkeyIndex =
remainingAccounts.insertOrGet(addressTree);
const addressQueuePubkeyIndex = remainingAccounts.insertOrGet(addressQueue);
const packedAddressTreeInfo = {
rootIndex: proofRpcResult.rootIndices[0],
addressMerkleTreePubkeyIndex,
addressQueuePubkeyIndex,
};
const outputStateTreeIndex =
remainingAccounts.insertOrGet(outputStateTree);
let proof = {
0: proofRpcResult.compressedProof,
};
const computeBudgetIx = web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 1000000,
});
let tx = await program.methods
.createAccount(proof, packedAddressTreeInfo, outputStateTreeIndex, message)
.accounts({
signer: signer.publicKey,
})
.preInstructions([computeBudgetIx])
.remainingAccounts(remainingAccounts.toAccountMetas().remainingAccounts)
.signers([signer])
.transaction();
tx.recentBlockhash = (await rpc.getRecentBlockhash()).blockhash;
tx.sign(signer);
const sig = await rpc.sendTransaction(tx, [signer]);
await confirmTx(rpc, sig);
return sig;
}// Update Compressed Account Example
import * as anchor from "@coral-xyz/anchor";
import { Program, web3 } from "@coral-xyz/anchor";
import { Update } from "../target/types/update";
import updateIdl from "../target/idl/update.json";
import {
bn,
CompressedAccountWithMerkleContext,
confirmTx,
createRpc,
defaultStaticAccountsStruct,
defaultTestStateTreeAccounts,
deriveAddress,
deriveAddressSeed,
LightSystemProgram,
PackedAccounts,
Rpc,
sleep,
SystemAccountMetaConfig,
} from "@lightprotocol/stateless.js";
import * as assert from "assert";
const path = require("path");
const os = require("os");
require("dotenv").config();
const anchorWalletPath = path.join(os.homedir(), ".config/solana/id.json");
process.env.ANCHOR_WALLET = anchorWalletPath;
describe("test-anchor-update", () => {
const updateProgram = anchor.workspace.Update as Program<Update>;
const updateCoder = new anchor.BorshCoder(updateIdl as anchor.Idl);
it("update compressed account message", async () => {
let signer = new web3.Keypair();
let rpc = createRpc(
"http://127.0.0.1:8899",
"http://127.0.0.1:8784",
"http://127.0.0.1:3001",
{
commitment: "confirmed",
},
);
let lamports = web3.LAMPORTS_PER_SOL;
await rpc.requestAirdrop(signer.publicKey, lamports);
await sleep(2000);
const outputStateTree = defaultTestStateTreeAccounts().merkleTree;
const addressTree = defaultTestStateTreeAccounts().addressTree;
const addressQueue = defaultTestStateTreeAccounts().addressQueue;
const messageSeed = new TextEncoder().encode("message");
const seed = deriveAddressSeed(
[messageSeed, signer.publicKey.toBytes()],
new web3.PublicKey(updateProgram.idl.address),
);
const address = deriveAddress(seed, addressTree);
// Step 1: Create compressed account with initial message using update program's create_account
const createTxId = await createCompressedAccount(
rpc,
addressTree,
addressQueue,
address,
updateProgram,
outputStateTree,
signer,
"Hello, compressed world!",
);
console.log("Create Transaction ID:", createTxId);
// Wait for indexer to process the create transaction
let slot = await rpc.getSlot();
await rpc.confirmTransactionIndexed(slot);
// Step 2: Get the created account
let compressedAccount = await rpc.getCompressedAccount(bn(address.toBytes()));
let myAccount = updateCoder.types.decode(
"MyCompressedAccount",
compressedAccount.data.data,
);
assert.strictEqual(myAccount.message, "Hello, compressed world!");
assert.ok(myAccount.owner.equals(signer.publicKey), "Owner should match signer public key");
console.log("Created message:", myAccount.message);
// Step 3: Update the account with new message
const updateTxId = await updateCompressedAccount(
rpc,
compressedAccount,
updateProgram,
outputStateTree,
signer,
"Hello again, compressed World!",
);
console.log("Update Transaction ID:", updateTxId);
// Wait for indexer to process the update transaction
slot = await rpc.getSlot();
await rpc.confirmTransactionIndexed(slot);
// Step 4: Verify the update
compressedAccount = await rpc.getCompressedAccount(bn(address.toBytes()));
myAccount = updateCoder.types.decode(
"MyCompressedAccount",
compressedAccount.data.data,
);
console.log("Updated message:", myAccount.message);
assert.ok(myAccount.owner.equals(signer.publicKey), "Owner should match signer public key");
assert.strictEqual(myAccount.message, "Hello again, compressed World!", "Message should be updated");
});
});
async function createCompressedAccount(
rpc: Rpc,
addressTree: anchor.web3.PublicKey,
addressQueue: anchor.web3.PublicKey,
address: anchor.web3.PublicKey,
program: anchor.Program<Update>,
outputStateTree: anchor.web3.PublicKey,
signer: anchor.web3.Keypair,
message: string,
) {
const proofRpcResult = await rpc.getValidityProofV0(
[],
[
{
tree: addressTree,
queue: addressQueue,
address: bn(address.toBytes()),
},
],
);
const systemAccountConfig = new SystemAccountMetaConfig(program.programId);
let remainingAccounts = new PackedAccounts();
remainingAccounts.addSystemAccounts(systemAccountConfig);
const addressMerkleTreePubkeyIndex =
remainingAccounts.insertOrGet(addressTree);
const addressQueuePubkeyIndex = remainingAccounts.insertOrGet(addressQueue);
const packedAddressTreeInfo = {
rootIndex: proofRpcResult.rootIndices[0],
addressMerkleTreePubkeyIndex,
addressQueuePubkeyIndex,
};
const outputStateTreeIndex =
remainingAccounts.insertOrGet(outputStateTree);
let proof = {
0: proofRpcResult.compressedProof,
};
const computeBudgetIx = web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 1000000,
});
let tx = await program.methods
.createAccount(proof, packedAddressTreeInfo, outputStateTreeIndex, message)
.accounts({
signer: signer.publicKey,
})
.preInstructions([computeBudgetIx])
.remainingAccounts(remainingAccounts.toAccountMetas().remainingAccounts)
.signers([signer])
.transaction();
tx.recentBlockhash = (await rpc.getRecentBlockhash()).blockhash;
tx.sign(signer);
const sig = await rpc.sendTransaction(tx, [signer]);
await confirmTx(rpc, sig);
return sig;
}
async function updateCompressedAccount(
rpc: Rpc,
compressedAccount: CompressedAccountWithMerkleContext,
program: anchor.Program<Update>,
outputStateTree: anchor.web3.PublicKey,
signer: anchor.web3.Keypair,
newMessage: string,
) {
const proofRpcResult = await rpc.getValidityProofV0(
[
{
hash: compressedAccount.hash,
tree: compressedAccount.treeInfo.tree,
queue: compressedAccount.treeInfo.queue,
},
],
[],
);
const systemAccountConfig = new SystemAccountMetaConfig(program.programId);
let remainingAccounts = new PackedAccounts();
remainingAccounts.addSystemAccounts(systemAccountConfig);
const merkleTreePubkeyIndex = remainingAccounts.insertOrGet(
compressedAccount.treeInfo.tree,
);
const queuePubkeyIndex = remainingAccounts.insertOrGet(
compressedAccount.treeInfo.queue,
);
const outputStateTreeIndex =
remainingAccounts.insertOrGet(outputStateTree);
// Deserialize current account using update program's coder
const coder = new anchor.BorshCoder(updateIdl as anchor.Idl);
const currentAccount = coder.types.decode(
"MyCompressedAccount",
compressedAccount.data.data,
);
const compressedAccountMeta = {
treeInfo: {
merkleTreePubkeyIndex,
queuePubkeyIndex,
leafIndex: compressedAccount.leafIndex,
proveByIndex: false,
rootIndex: proofRpcResult.rootIndices[0],
},
outputStateTreeIndex,
address: compressedAccount.address,
};
let proof = {
0: proofRpcResult.compressedProof,
};
const computeBudgetIx = web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 1000000,
});
let tx = await program.methods
.updateAccount(proof, currentAccount, compressedAccountMeta, newMessage)
.accounts({
signer: signer.publicKey,
})
.preInstructions([computeBudgetIx])
.remainingAccounts(remainingAccounts.toAccountMetas().remainingAccounts)
.signers([signer])
.transaction();
tx.recentBlockhash = (await rpc.getRecentBlockhash()).blockhash;
tx.sign(signer);
const sig = await rpc.sendTransaction(tx, [signer]);
await confirmTx(rpc, sig);
return sig;
}// Close Compressed Account Example
import * as anchor from "@coral-xyz/anchor";
import { Program, web3 } from "@coral-xyz/anchor";
import { Close } from "../target/types/close";
import closeIdl from "../target/idl/close.json";
import {
bn,
CompressedAccountWithMerkleContext,
confirmTx,
createRpc,
defaultStaticAccountsStruct,
defaultTestStateTreeAccounts,
deriveAddress,
deriveAddressSeed,
LightSystemProgram,
PackedAccounts,
Rpc,
sleep,
SystemAccountMetaConfig,
} from "@lightprotocol/stateless.js";
import * as assert from "assert";
const path = require("path");
const os = require("os");
require("dotenv").config();
const anchorWalletPath = path.join(os.homedir(), ".config/solana/id.json");
process.env.ANCHOR_WALLET = anchorWalletPath;
describe("test-anchor-close", () => {
const closeProgram = anchor.workspace.Close as Program<Close>;
const closeCoder = new anchor.BorshCoder(closeIdl as anchor.Idl);
it("close compressed account", async () => {
let signer = new web3.Keypair();
let rpc = createRpc(
"http://127.0.0.1:8899",
"http://127.0.0.1:8784",
"http://127.0.0.1:3001",
{
commitment: "confirmed",
},
);
let lamports = web3.LAMPORTS_PER_SOL;
await rpc.requestAirdrop(signer.publicKey, lamports);
await sleep(2000);
const outputStateTree = defaultTestStateTreeAccounts().merkleTree;
const addressTree = defaultTestStateTreeAccounts().addressTree;
const addressQueue = defaultTestStateTreeAccounts().addressQueue;
const messageSeed = new TextEncoder().encode("message");
const seed = deriveAddressSeed(
[messageSeed, signer.publicKey.toBytes()],
new web3.PublicKey(closeProgram.idl.address),
);
const address = deriveAddress(seed, addressTree);
const createTxId = await createCompressedAccount(
rpc,
addressTree,
addressQueue,
address,
closeProgram,
outputStateTree,
signer,
"Hello, compressed world!",
);
console.log("Create Transaction ID:", createTxId);
// Wait for indexer to process the create transaction
let slot = await rpc.getSlot();
await rpc.confirmTransactionIndexed(slot);
let compressedAccount = await rpc.getCompressedAccount(bn(address.toBytes()));
let myAccount = closeCoder.types.decode(
"MyCompressedAccount",
compressedAccount.data.data,
);
assert.strictEqual(myAccount.message, "Hello, compressed world!");
assert.ok(myAccount.owner.equals(signer.publicKey), "Owner should match signer public key");
console.log("Created message:", myAccount.message);
const closeTxId = await closeCompressedAccount(
rpc,
compressedAccount,
closeProgram,
outputStateTree,
signer,
"Hello, compressed world!",
);
console.log("Close Transaction ID:", closeTxId);
// Wait for indexer to process the close transaction
slot = await rpc.getSlot();
await rpc.confirmTransactionIndexed(slot);
// After closing, the account exists with zero data.
// Verify the account was closed by checking that data.data is empty.
const closedAccount = await rpc.getCompressedAccount(bn(address.toBytes()));
assert.ok(
closedAccount.data.data === null ||
(Buffer.isBuffer(closedAccount.data.data) && closedAccount.data.data.length === 0),
"Closed account should have null or empty data.data"
);
console.log("Verified account was closed (data.data is empty as expected)");
});
});
async function createCompressedAccount(
rpc: Rpc,
addressTree: anchor.web3.PublicKey,
addressQueue: anchor.web3.PublicKey,
address: anchor.web3.PublicKey,
program: anchor.Program<Close>,
outputStateTree: anchor.web3.PublicKey,
signer: anchor.web3.Keypair,
message: string,
) {
const proofRpcResult = await rpc.getValidityProofV0(
[],
[
{
tree: addressTree,
queue: addressQueue,
address: bn(address.toBytes()),
},
],
);
const systemAccountConfig = new SystemAccountMetaConfig(program.programId);
let remainingAccounts = new PackedAccounts();
remainingAccounts.addSystemAccounts(systemAccountConfig);
const addressMerkleTreePubkeyIndex =
remainingAccounts.insertOrGet(addressTree);
const addressQueuePubkeyIndex = remainingAccounts.insertOrGet(addressQueue);
const packedAddressTreeInfo = {
rootIndex: proofRpcResult.rootIndices[0],
addressMerkleTreePubkeyIndex,
addressQueuePubkeyIndex,
};
const outputStateTreeIndex =
remainingAccounts.insertOrGet(outputStateTree);
let proof = {
0: proofRpcResult.compressedProof,
};
const computeBudgetIx = web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 1000000,
});
let tx = await program.methods
.createAccount(proof, packedAddressTreeInfo, outputStateTreeIndex, message)
.accounts({
signer: signer.publicKey,
})
.preInstructions([computeBudgetIx])
.remainingAccounts(remainingAccounts.toAccountMetas().remainingAccounts)
.signers([signer])
.transaction();
tx.recentBlockhash = (await rpc.getRecentBlockhash()).blockhash;
tx.sign(signer);
const sig = await rpc.sendTransaction(tx, [signer]);
await confirmTx(rpc, sig);
return sig;
}
async function closeCompressedAccount(
rpc: Rpc,
compressedAccount: CompressedAccountWithMerkleContext,
program: anchor.Program<Close>,
outputStateTree: anchor.web3.PublicKey,
signer: anchor.web3.Keypair,
message: string,
) {
const systemAccountConfig = new SystemAccountMetaConfig(program.programId);
let remainingAccounts = new PackedAccounts();
remainingAccounts.addSystemAccounts(systemAccountConfig);
const proofRpcResult = await rpc.getValidityProofV0(
[
{
hash: compressedAccount.hash,
tree: compressedAccount.treeInfo.tree,
queue: compressedAccount.treeInfo.queue,
},
],
[],
);
const merkleTreePubkeyIndex = remainingAccounts.insertOrGet(
compressedAccount.treeInfo.tree,
);
const queuePubkeyIndex = remainingAccounts.insertOrGet(
compressedAccount.treeInfo.queue,
);
const outputStateTreeIndex =
remainingAccounts.insertOrGet(outputStateTree);
const coder = new anchor.BorshCoder(closeIdl as anchor.Idl);
const currentAccount = coder.types.decode(
"MyCompressedAccount",
compressedAccount.data.data,
);
const compressedAccountMeta = {
treeInfo: {
merkleTreePubkeyIndex,
queuePubkeyIndex,
leafIndex: compressedAccount.leafIndex,
proveByIndex: false,
rootIndex: proofRpcResult.rootIndices[0],
},
address: compressedAccount.address,
outputStateTreeIndex,
};
let proof = {
0: proofRpcResult.compressedProof,
};
const computeBudgetIx = web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 1000000,
});
let tx = await program.methods
.closeAccount(proof, compressedAccountMeta, message)
.accounts({
signer: signer.publicKey,
})
.preInstructions([computeBudgetIx])
.remainingAccounts(remainingAccounts.toAccountMetas().remainingAccounts)
.signers([signer])
.transaction();
tx.recentBlockhash = (await rpc.getRecentBlockhash()).blockhash;
tx.sign(signer);
const sig = await rpc.sendTransaction(tx, [signer]);
await confirmTx(rpc, sig);
return sig;
}// Reinitialize Compressed Account Example
import * as anchor from "@coral-xyz/anchor";
import { Program, web3 } from "@coral-xyz/anchor";
import { Reinit } from "../target/types/reinit";
import reinitIdl from "../target/idl/reinit.json";
import {
bn,
CompressedAccountWithMerkleContext,
confirmTx,
createRpc,
defaultStaticAccountsStruct,
defaultTestStateTreeAccounts,
deriveAddress,
deriveAddressSeed,
LightSystemProgram,
PackedAccounts,
Rpc,
sleep,
SystemAccountMetaConfig,
} from "@lightprotocol/stateless.js";
import * as assert from "assert";
const path = require("path");
const os = require("os");
require("dotenv").config();
const anchorWalletPath = path.join(os.homedir(), ".config/solana/id.json");
process.env.ANCHOR_WALLET = anchorWalletPath;
describe("test-anchor-reinit", () => {
const reinitProgram = anchor.workspace.Reinit as Program<Reinit>;
const reinitCoder = new anchor.BorshCoder(reinitIdl as anchor.Idl);
it("reinitialize compressed account", async () => {
let signer = new web3.Keypair();
let rpc = createRpc(
"http://127.0.0.1:8899",
"http://127.0.0.1:8784",
"http://127.0.0.1:3001",
{
commitment: "confirmed",
},
);
let lamports = web3.LAMPORTS_PER_SOL;
await rpc.requestAirdrop(signer.publicKey, lamports);
await sleep(2000);
const outputStateTree = defaultTestStateTreeAccounts().merkleTree;
const addressTree = defaultTestStateTreeAccounts().addressTree;
const addressQueue = defaultTestStateTreeAccounts().addressQueue;
const messageSeed = new TextEncoder().encode("message");
const seed = deriveAddressSeed(
[messageSeed, signer.publicKey.toBytes()],
new web3.PublicKey(reinitProgram.idl.address),
);
const address = deriveAddress(seed, addressTree);
const createTxId = await createCompressedAccount(
rpc,
addressTree,
addressQueue,
address,
reinitProgram,
outputStateTree,
signer,
"Hello, compressed world!",
);
console.log("Create Transaction ID:", createTxId);
// Wait for indexer to process the transaction
let slot = await rpc.getSlot();
await rpc.confirmTransactionIndexed(slot);
let compressedAccount = await rpc.getCompressedAccount(bn(address.toBytes()));
let myAccount = reinitCoder.types.decode(
"MyCompressedAccount",
compressedAccount.data.data,
);
assert.strictEqual(myAccount.message, "Hello, compressed world!");
assert.ok(myAccount.owner.equals(signer.publicKey), "Owner should match signer public key");
console.log("Created message:", myAccount.message);
const closeTxId = await closeCompressedAccount(
rpc,
compressedAccount,
reinitProgram,
outputStateTree,
signer,
"Hello, compressed world!",
);
console.log("Close Transaction ID:", closeTxId);
// Wait for indexer to process the close transaction
slot = await rpc.getSlot();
await rpc.confirmTransactionIndexed(slot);
let closedCompressedAccount = await rpc.getCompressedAccount(bn(address.toBytes()));
// The getValidityProofV0 call will fetch the current closed account state.
const reinitTxId = await reinitCompressedAccount(
rpc,
closedCompressedAccount,
reinitProgram,
outputStateTree,
signer,
);
console.log("Reinit Transaction ID:", reinitTxId);
// Wait for indexer to process the reinit transaction
slot = await rpc.getSlot();
await rpc.confirmTransactionIndexed(slot);
// Verify the account was reinitialized with default values
let reinitializedAccount = await rpc.getCompressedAccount(bn(address.toBytes()));
let reinitMyAccount = reinitCoder.types.decode(
"MyCompressedAccount",
reinitializedAccount.data.data,
);
assert.strictEqual(reinitMyAccount.message, "", "Message should be empty (default)");
assert.ok(
reinitMyAccount.owner.equals(web3.PublicKey.default),
"Owner should be default PublicKey"
);
console.log("Compressed account was reinitialized with default values");
});
});
async function createCompressedAccount(
rpc: Rpc,
addressTree: anchor.web3.PublicKey,
addressQueue: anchor.web3.PublicKey,
address: anchor.web3.PublicKey,
program: anchor.Program<Reinit>,
outputStateTree: anchor.web3.PublicKey,
signer: anchor.web3.Keypair,
message: string,
) {
const proofRpcResult = await rpc.getValidityProofV0(
[],
[
{
tree: addressTree,
queue: addressQueue,
address: bn(address.toBytes()),
},
],
);
const systemAccountConfig = new SystemAccountMetaConfig(program.programId);
let remainingAccounts = new PackedAccounts();
remainingAccounts.addSystemAccounts(systemAccountConfig);
const addressMerkleTreePubkeyIndex =
remainingAccounts.insertOrGet(addressTree);
const addressQueuePubkeyIndex = remainingAccounts.insertOrGet(addressQueue);
const packedAddressTreeInfo = {
rootIndex: proofRpcResult.rootIndices[0],
addressMerkleTreePubkeyIndex,
addressQueuePubkeyIndex,
};
const outputStateTreeIndex =
remainingAccounts.insertOrGet(outputStateTree);
let proof = {
0: proofRpcResult.compressedProof,
};
const computeBudgetIx = web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 1000000,
});
let tx = await program.methods
.createAccount(proof, packedAddressTreeInfo, outputStateTreeIndex, message)
.accounts({
signer: signer.publicKey,
})
.preInstructions([computeBudgetIx])
.remainingAccounts(remainingAccounts.toAccountMetas().remainingAccounts)
.signers([signer])
.transaction();
tx.recentBlockhash = (await rpc.getRecentBlockhash()).blockhash;
tx.sign(signer);
const sig = await rpc.sendTransaction(tx, [signer]);
await confirmTx(rpc, sig);
return sig;
}
async function closeCompressedAccount(
rpc: Rpc,
compressedAccount: CompressedAccountWithMerkleContext,
program: anchor.Program<Reinit>,
outputStateTree: anchor.web3.PublicKey,
signer: anchor.web3.Keypair,
message: string,
) {
const systemAccountConfig = new SystemAccountMetaConfig(program.programId);
let remainingAccounts = new PackedAccounts();
remainingAccounts.addSystemAccounts(systemAccountConfig);
const proofRpcResult = await rpc.getValidityProofV0(
[
{
hash: compressedAccount.hash,
tree: compressedAccount.treeInfo.tree,
queue: compressedAccount.treeInfo.queue,
},
],
[],
);
const merkleTreePubkeyIndex = remainingAccounts.insertOrGet(
compressedAccount.treeInfo.tree,
);
const queuePubkeyIndex = remainingAccounts.insertOrGet(
compressedAccount.treeInfo.queue,
);
const outputStateTreeIndex =
remainingAccounts.insertOrGet(outputStateTree);
const coder = new anchor.BorshCoder(reinitIdl as anchor.Idl);
const currentAccount = coder.types.decode(
"MyCompressedAccount",
compressedAccount.data.data,
);
const compressedAccountMeta = {
treeInfo: {
merkleTreePubkeyIndex,
queuePubkeyIndex,
leafIndex: compressedAccount.leafIndex,
proveByIndex: false,
rootIndex: proofRpcResult.rootIndices[0],
},
address: compressedAccount.address,
outputStateTreeIndex,
};
let proof = {
0: proofRpcResult.compressedProof,
};
const computeBudgetIx = web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 1000000,
});
let tx = await program.methods
.closeAccount(proof, compressedAccountMeta, message)
.accounts({
signer: signer.publicKey,
})
.preInstructions([computeBudgetIx])
.remainingAccounts(remainingAccounts.toAccountMetas().remainingAccounts)
.signers([signer])
.transaction();
tx.recentBlockhash = (await rpc.getRecentBlockhash()).blockhash;
tx.sign(signer);
const sig = await rpc.sendTransaction(tx, [signer]);
await confirmTx(rpc, sig);
return sig;
}
async function reinitCompressedAccount(
rpc: Rpc,
compressedAccount: CompressedAccountWithMerkleContext,
program: anchor.Program<Reinit>,
outputStateTree: anchor.web3.PublicKey,
signer: anchor.web3.Keypair,
) {
const systemAccountConfig = new SystemAccountMetaConfig(program.programId);
let remainingAccounts = new PackedAccounts();
remainingAccounts.addSystemAccounts(systemAccountConfig);
const proofRpcResult = await rpc.getValidityProofV0(
[
{
hash: compressedAccount.hash,
tree: compressedAccount.treeInfo.tree,
queue: compressedAccount.treeInfo.queue,
},
],
[],
);
const merkleTreePubkeyIndex = remainingAccounts.insertOrGet(
compressedAccount.treeInfo.tree,
);
const queuePubkeyIndex = remainingAccounts.insertOrGet(
compressedAccount.treeInfo.queue,
);
const outputStateTreeIndex =
remainingAccounts.insertOrGet(outputStateTree);
const compressedAccountMeta = {
treeInfo: {
merkleTreePubkeyIndex,
queuePubkeyIndex,
leafIndex: compressedAccount.leafIndex,
proveByIndex: false,
rootIndex: proofRpcResult.rootIndices[0],
},
address: compressedAccount.address,
outputStateTreeIndex,
};
let proof = {
0: proofRpcResult.compressedProof,
};
const computeBudgetIx = web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 1000000,
});
let tx = await program.methods
.reinitAccount(proof, compressedAccountMeta)
.accounts({
signer: signer.publicKey,
})
.preInstructions([computeBudgetIx])
.remainingAccounts(remainingAccounts.toAccountMetas().remainingAccounts)
.signers([signer])
.transaction();
tx.recentBlockhash = (await rpc.getRecentBlockhash()).blockhash;
tx.sign(signer);
const sig = await rpc.sendTransaction(tx, [signer]);
await confirmTx(rpc, sig);
return sig;
}// Burn Compressed Account Example
import * as anchor from "@coral-xyz/anchor";
import { Program, web3 } from "@coral-xyz/anchor";
import { Burn } from "../target/types/burn";
import burnIdl from "../target/idl/burn.json";
import {
bn,
CompressedAccountWithMerkleContext,
confirmTx,
createRpc,
defaultStaticAccountsStruct,
defaultTestStateTreeAccounts,
deriveAddress,
deriveAddressSeed,
LightSystemProgram,
PackedAccounts,
Rpc,
sleep,
SystemAccountMetaConfig,
} from "@lightprotocol/stateless.js";
import * as assert from "assert";
const path = require("path");
const os = require("os");
require("dotenv").config();
const anchorWalletPath = path.join(os.homedir(), ".config/solana/id.json");
process.env.ANCHOR_WALLET = anchorWalletPath;
describe("test-anchor-burn", () => {
const burnProgram = anchor.workspace.Burn as Program<Burn>;
const burnCoder = new anchor.BorshCoder(burnIdl as anchor.Idl);
it("burn compressed account", async () => {
let signer = new web3.Keypair();
let rpc = createRpc(
"http://127.0.0.1:8899",
"http://127.0.0.1:8784",
"http://127.0.0.1:3001",
{
commitment: "confirmed",
},
);
let lamports = web3.LAMPORTS_PER_SOL;
await rpc.requestAirdrop(signer.publicKey, lamports);
await sleep(2000);
const outputStateTree = defaultTestStateTreeAccounts().merkleTree;
const addressTree = defaultTestStateTreeAccounts().addressTree;
const addressQueue = defaultTestStateTreeAccounts().addressQueue;
const messageSeed = new TextEncoder().encode("message");
const seed = deriveAddressSeed(
[messageSeed, signer.publicKey.toBytes()],
new web3.PublicKey(burnProgram.idl.address),
);
const address = deriveAddress(seed, addressTree);
// Step 1: Create compressed account with initial message
const createTxId = await createCompressedAccount(
rpc,
addressTree,
addressQueue,
address,
burnProgram,
outputStateTree,
signer,
"Hello, compressed world!",
);
console.log("Create Transaction ID:", createTxId);
// Wait for indexer to process the create transaction
let slot = await rpc.getSlot();
await rpc.confirmTransactionIndexed(slot);
// Step 2: Get the created account and verify
let compressedAccount = await rpc.getCompressedAccount(bn(address.toBytes()));
let myAccount = burnCoder.types.decode(
"MyCompressedAccount",
compressedAccount.data.data,
);
assert.strictEqual(myAccount.message, "Hello, compressed world!");
assert.ok(myAccount.owner.equals(signer.publicKey), "Owner should match signer public key");
console.log("Created message:", myAccount.message);
// Step 3: Burn the account permanently
const burnTxId = await burnCompressedAccount(
rpc,
compressedAccount,
burnProgram,
signer,
"Hello, compressed world!",
);
console.log("Burn Transaction ID:", burnTxId);
// Wait for indexer to process the burn transaction
slot = await rpc.getSlot();
await rpc.confirmTransactionIndexed(slot);
// Step 4: Verify the account is burned (does not exist)
try {
await rpc.getCompressedAccount(bn(address.toBytes()));
assert.fail("Expected account to not exist after burning");
} catch (error: any) {
// Account should not exist after burn
console.log("Verified account was burned");
}
});
});
async function createCompressedAccount(
rpc: Rpc,
addressTree: anchor.web3.PublicKey,
addressQueue: anchor.web3.PublicKey,
address: anchor.web3.PublicKey,
program: anchor.Program<Burn>,
outputStateTree: anchor.web3.PublicKey,
signer: anchor.web3.Keypair,
message: string,
) {
const proofRpcResult = await rpc.getValidityProofV0(
[],
[
{
tree: addressTree,
queue: addressQueue,
address: bn(address.toBytes()),
},
],
);
const systemAccountConfig = new SystemAccountMetaConfig(program.programId);
let remainingAccounts = new PackedAccounts();
remainingAccounts.addSystemAccounts(systemAccountConfig);
const addressMerkleTreePubkeyIndex =
remainingAccounts.insertOrGet(addressTree);
const addressQueuePubkeyIndex = remainingAccounts.insertOrGet(addressQueue);
const packedAddressTreeInfo = {
rootIndex: proofRpcResult.rootIndices[0],
addressMerkleTreePubkeyIndex,
addressQueuePubkeyIndex,
};
const outputStateTreeIndex =
remainingAccounts.insertOrGet(outputStateTree);
let proof = {
0: proofRpcResult.compressedProof,
};
const computeBudgetIx = web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 1000000,
});
let tx = await program.methods
.createAccount(proof, packedAddressTreeInfo, outputStateTreeIndex, message)
.accounts({
signer: signer.publicKey,
})
.preInstructions([computeBudgetIx])
.remainingAccounts(remainingAccounts.toAccountMetas().remainingAccounts)
.signers([signer])
.transaction();
tx.recentBlockhash = (await rpc.getRecentBlockhash()).blockhash;
tx.sign(signer);
const sig = await rpc.sendTransaction(tx, [signer]);
await confirmTx(rpc, sig);
return sig;
}
async function burnCompressedAccount(
rpc: Rpc,
compressedAccount: CompressedAccountWithMerkleContext,
program: anchor.Program<Burn>,
signer: anchor.web3.Keypair,
currentMessage: string,
) {
const proofRpcResult = await rpc.getValidityProofV0(
[
{
hash: compressedAccount.hash,
tree: compressedAccount.treeInfo.tree,
queue: compressedAccount.treeInfo.queue,
},
],
[],
);
const systemAccountConfig = new SystemAccountMetaConfig(program.programId);
let remainingAccounts = new PackedAccounts();
remainingAccounts.addSystemAccounts(systemAccountConfig);
const merkleTreePubkeyIndex = remainingAccounts.insertOrGet(
compressedAccount.treeInfo.tree,
);
const queuePubkeyIndex = remainingAccounts.insertOrGet(
compressedAccount.treeInfo.queue,
);
// CompressedAccountMetaBurn does not have output_state_tree_index
const compressedAccountMeta = {
treeInfo: {
merkleTreePubkeyIndex,
queuePubkeyIndex,
leafIndex: compressedAccount.leafIndex,
proveByIndex: false,
rootIndex: proofRpcResult.rootIndices[0],
},
address: compressedAccount.address,
};
let proof = {
0: proofRpcResult.compressedProof,
};
const computeBudgetIx = web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 1000000,
});
let tx = await program.methods
.burnAccount(proof, compressedAccountMeta, currentMessage)
.accounts({
signer: signer.publicKey,
})
.preInstructions([computeBudgetIx])
.remainingAccounts(remainingAccounts.toAccountMetas().remainingAccounts)
.signers([signer])
.transaction();
tx.recentBlockhash = (await rpc.getRecentBlockhash()).blockhash;
tx.sign(signer);
const sig = await rpc.sendTransaction(tx, [signer]);
await confirmTx(rpc, sig);
return sig;
}Next Steps
Start building programs to create, or interact with compressed accounts.
GuidesLast updated
Was this helpful?