Skip to main content
import * as anchor from "@coral-xyz/anchor";
import { Program, web3 } from "@coral-xyz/anchor";
import { CreateAndUpdate } from "../target/types/create_and_update";
import {
  bn,
  CompressedAccountWithMerkleContext,
  confirmTx,
  createRpc,
  featureFlags,
  PackedAccounts,
  Rpc,
  sleep,
  SystemAccountMetaConfig,
  VERSION,
  selectStateTreeInfo,
  TreeInfo,
  PackedAddressTreeInfo,
  deriveAddressV2,
  deriveAddressSeedV2,
  buildAndSignTx,
  sendAndConfirmTx,
} from "@lightprotocol/stateless.js";
import * as assert from "assert";
const path = require("path");
const os = require("os");
require("dotenv").config();

// v2 feature flag
featureFlags.version = VERSION.V2;

const anchorWalletPath = path.join(os.homedir(), ".config/solana/id.json");
process.env.ANCHOR_WALLET = anchorWalletPath;

describe("create-and-update anchor", () => {
  const program = anchor.workspace.CreateAndUpdate as Program<CreateAndUpdate>;
  const coder = new anchor.BorshCoder(program.idl);

  it("creates and updates compressed accounts atomically", async () => {
    const signer = new web3.Keypair();
    const rpc = createRpc();

    await rpc.requestAirdrop(signer.publicKey, web3.LAMPORTS_PER_SOL);
    await sleep(2000);

    const stateTreeInfos = await rpc.getStateTreeInfos();
    const stateTreeInfo = selectStateTreeInfo(stateTreeInfos);

    // v2 address tree info
    const addressTreeInfo = await rpc.getAddressTreeInfoV2();
    // v2 derive
    const firstSeed = new TextEncoder().encode("first");
    const firstAddressSeed = deriveAddressSeedV2([
      firstSeed,
      signer.publicKey.toBytes(),
    ]);
    const firstAddress = deriveAddressV2(
      firstAddressSeed,
      addressTreeInfo.tree,
      program.programId
    );

    await createCompressedAccount(
      rpc,
      addressTreeInfo,
      firstAddress,
      program,
      stateTreeInfo,
      signer,
      "Initial message"
    );

    let firstAccount = await rpc.getCompressedAccount(
      bn(firstAddress.toBytes())
    );
    if (!firstAccount) {
      throw new Error("Failed to fetch the initial compressed account");
    }

    let decoded = coder.types.decode("dataAccount", firstAccount.data.data);
    assert.ok(
      decoded.owner.equals(signer.publicKey),
      "owner should match signer"
    );
    assert.strictEqual(decoded.message, "Initial message");

    const secondSeed = new TextEncoder().encode("second");
    const secondAddressSeed = deriveAddressSeedV2([
      secondSeed,
      signer.publicKey.toBytes(),
    ]);
    const secondAddress = deriveAddressV2(
      secondAddressSeed,
      addressTreeInfo.tree,
      program.programId
    );

    await createAndUpdateAccounts(
      rpc,
      program,
      signer,
      firstAccount,
      secondAddress,
      addressTreeInfo,
      "Hello from second account",
      "Updated first message"
    );
  });

  async function waitForIndexer(rpc: Rpc) {
    const slot = await rpc.getSlot();
    await rpc.confirmTransactionIndexed(slot);
  }

  async function createCompressedAccount(
    rpc: Rpc,
    addressTreeInfo: TreeInfo,
    address: anchor.web3.PublicKey,
    program: Program<CreateAndUpdate>,
    stateTreeInfo: TreeInfo,
    signer: anchor.web3.Keypair,
    message: string
  ) {
    const proofRpcResult = await rpc.getValidityProofV0(
      [],
      [
        {
          tree: addressTreeInfo.tree,
          queue: addressTreeInfo.queue,
          address: bn(address.toBytes()),
        },
      ]
    );

    const config = SystemAccountMetaConfig.new(program.programId);
    const packedAccounts = PackedAccounts.newWithSystemAccountsV2(config);

    const outputStateTreeIndex = packedAccounts.insertOrGet(
      stateTreeInfo.queue
    );
    const addressQueueIndex = packedAccounts.insertOrGet(addressTreeInfo.queue);
    const addressTreeIndex = packedAccounts.insertOrGet(addressTreeInfo.tree);
    const packedAddressTreeInfo: PackedAddressTreeInfo = {
      rootIndex: proofRpcResult.rootIndices[0],
      addressMerkleTreePubkeyIndex: addressTreeIndex,
      addressQueuePubkeyIndex: addressQueueIndex,
    };
    const proof = {
      0: proofRpcResult.compressedProof,
    };
    const computeBudgetIx = web3.ComputeBudgetProgram.setComputeUnitLimit({
      units: 300_000,
    });

    const remainingAccounts = packedAccounts.toAccountMetas().remainingAccounts;
    const tx = await program.methods
      .createCompressedAccount(
        proof,
        packedAddressTreeInfo,
        outputStateTreeIndex,
        message
      )
      .accounts({
        signer: signer.publicKey,
      })
      .preInstructions([computeBudgetIx])
      .remainingAccounts(remainingAccounts)
      .signers([signer])
      .transaction();

    const recentBlockhash = (await rpc.getRecentBlockhash()).blockhash;

    const signedTx = buildAndSignTx(tx.instructions, signer, recentBlockhash);
    const sig = await sendAndConfirmTx(rpc, signedTx);
    return sig;
  }

  async function createAndUpdateAccounts(
    rpc: Rpc,
    program: Program<CreateAndUpdate>,
    signer: anchor.web3.Keypair,
    existingAccount: CompressedAccountWithMerkleContext,
    newAddress: anchor.web3.PublicKey,
    addressTreeInfo: TreeInfo,
    newAccountMessage: string,
    updatedMessage: string
  ) {
    if (!existingAccount.address) {
      throw new Error("Existing compressed account missing address data");
    }

    const proofRpcResult = await rpc.getValidityProofV0(
      [
        {
          hash: existingAccount.hash,
          tree: existingAccount.treeInfo.tree,
          queue: existingAccount.treeInfo.queue,
        },
      ],
      // new account's address
      [
        {
          tree: addressTreeInfo.tree,
          queue: addressTreeInfo.queue,
          address: bn(newAddress.toBytes()),
        },
      ]
    );

    const coder = new anchor.BorshCoder(program.idl);
    const currentAccountData = coder.types.decode(
      "dataAccount",
      existingAccount.data.data
    );

    const config = SystemAccountMetaConfig.new(program.programId);
    const packedAccounts = PackedAccounts.newWithSystemAccountsV2(config);

    const existingAccountMeta = {
      treeInfo: {
        rootIndex: proofRpcResult.rootIndices[0],
        // Note: set this to true for local testing.
        proveByIndex: true,
        merkleTreePubkeyIndex: packedAccounts.insertOrGet(
          existingAccount.treeInfo.tree
        ),
        queuePubkeyIndex: packedAccounts.insertOrGet(
          existingAccount.treeInfo.queue
        ),
        leafIndex: existingAccount.leafIndex,
      },
      address: existingAccount.address,
      outputStateTreeIndex: packedAccounts.insertOrGet(
        existingAccount.treeInfo.queue
      ),
    };

    // for new account's address
    const addressQueueIndex = packedAccounts.insertOrGet(addressTreeInfo.queue);
    const addressTreeIndex = packedAccounts.insertOrGet(addressTreeInfo.tree);

    const packedAddressTreeInfo: PackedAddressTreeInfo = {
      rootIndex: proofRpcResult.rootIndices[1],
      addressMerkleTreePubkeyIndex: addressTreeIndex,
      addressQueuePubkeyIndex: addressQueueIndex,
    };

    const proof = {
      0: proofRpcResult.compressedProof,
    };
    const computeBudgetIx = web3.ComputeBudgetProgram.setComputeUnitLimit({
      units: 1000000,
    });

    const remainingAccounts = packedAccounts.toAccountMetas().remainingAccounts;

    const tx = await program.methods
      .createAndUpdate(
        proof,
        {
          accountMeta: existingAccountMeta,
          message: currentAccountData.message,
          updateMessage: updatedMessage,
        },
        {
          addressTreeInfo: packedAddressTreeInfo,
          message: newAccountMessage,
        }
      )
      .accounts({
        signer: signer.publicKey,
      })
      .preInstructions([computeBudgetIx])
      .remainingAccounts(remainingAccounts)
      .signers([signer])
      .transaction();

    const recentBlockhash = (await rpc.getRecentBlockhash()).blockhash;
    const signedTx = buildAndSignTx(tx.instructions, signer, recentBlockhash);
    const sig = await sendAndConfirmTx(rpc, signedTx);
    return sig;
  }
});