From a1b43140939834aab26f8fb2ace0d6018e97e677 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 25 Mar 2025 04:05:35 +0000 Subject: [PATCH 01/19] stateless.js add treeinfos --- js/stateless.js/src/actions/compress.ts | 30 ++--- js/stateless.js/src/actions/create-account.ts | 76 ++++++------ js/stateless.js/src/actions/decompress.ts | 33 ++--- js/stateless.js/src/actions/transfer.ts | 36 +++--- js/stateless.js/src/constants.ts | 10 +- .../instruction/pack-compressed-accounts.ts | 27 ++-- js/stateless.js/src/programs/layout.ts | 27 ++++ js/stateless.js/src/programs/system.ts | 32 +++-- js/stateless.js/src/rpc.ts | 48 ++----- js/stateless.js/src/state/types.ts | 53 +++++++- .../src/test-helpers/test-rpc/test-rpc.ts | 10 +- .../src/utils/get-light-state-tree-info.ts | 117 +++++++++++++----- 12 files changed, 307 insertions(+), 192 deletions(-) diff --git a/js/stateless.js/src/actions/compress.ts b/js/stateless.js/src/actions/compress.ts index 3a3c13edcb..d1c47e539a 100644 --- a/js/stateless.js/src/actions/compress.ts +++ b/js/stateless.js/src/actions/compress.ts @@ -7,45 +7,45 @@ import { } from '@solana/web3.js'; import { LightSystemProgram } from '../programs'; -import { pickRandomTreeAndQueue, Rpc } from '../rpc'; -import { buildAndSignTx, sendAndConfirmTx } from '../utils'; +import { Rpc } from '../rpc'; +import { buildAndSignTx, pickStateTreeInfo, sendAndConfirmTx } from '../utils'; import BN from 'bn.js'; +import { StateTreeInfo } from '../state'; /** * Compress lamports to a solana address * - * @param rpc RPC to use - * @param payer Payer of the transaction and initialization fees - * @param lamports Amount of lamports to compress - * @param toAddress Address of the recipient compressed account - * @param outputStateTree Optional output state tree. Defaults to a current shared state tree. - * @param confirmOptions Options for confirming the transaction + * @param rpc RPC to use + * @param payer Payer of the transaction and initialization fees + * @param lamports Amount of lamports to compress + * @param toAddress Address of the recipient compressed account + * @param outputStateTreeInfo Optional output state tree. If not provided, + * fetches a random active state tree. + * @param confirmOptions Options for confirming the transaction * * @return Transaction signature */ -/// TODO: add multisig support -/// TODO: add support for payer != owner + export async function compress( rpc: Rpc, payer: Signer, lamports: number | BN, toAddress: PublicKey, - outputStateTree?: PublicKey, + outputStateTreeInfo?: StateTreeInfo, confirmOptions?: ConfirmOptions, ): Promise { const { blockhash } = await rpc.getLatestBlockhash(); - if (!outputStateTree) { + if (!outputStateTreeInfo) { const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); - const { tree } = pickRandomTreeAndQueue(stateTreeInfo); - outputStateTree = tree; + outputStateTreeInfo = pickStateTreeInfo(stateTreeInfo); } const ix = await LightSystemProgram.compress({ payer: payer.publicKey, toAddress, lamports, - outputStateTree, + outputStateTreeInfo, }); const tx = buildAndSignTx( diff --git a/js/stateless.js/src/actions/create-account.ts b/js/stateless.js/src/actions/create-account.ts index e9e582b10b..b80d0454bb 100644 --- a/js/stateless.js/src/actions/create-account.ts +++ b/js/stateless.js/src/actions/create-account.ts @@ -9,34 +9,35 @@ import { LightSystemProgram, selectMinCompressedSolAccountsForTransfer, } from '../programs'; -import { pickRandomTreeAndQueue, Rpc } from '../rpc'; +import { Rpc } from '../rpc'; import { NewAddressParams, buildAndSignTx, deriveAddress, deriveAddressSeed, + pickStateTreeInfo, sendAndConfirmTx, } from '../utils'; import { defaultTestStateTreeAccounts } from '../constants'; -import { bn } from '../state'; +import { bn, StateTreeInfo } from '../state'; import BN from 'bn.js'; /** * Create compressed account with address * - * @param rpc RPC to use - * @param payer Payer of the transaction and initialization fees - * @param seeds Seeds to derive the new account address - * @param programId Owner of the new account - * @param addressTree Optional address tree. Defaults to a current shared - * address tree. - * @param addressQueue Optional address queue. Defaults to a current shared - * address queue. - * @param outputStateTree Optional output state tree. Defaults to a current - * shared state tree. - * @param confirmOptions Options for confirming the transaction + * @param rpc RPC to use + * @param payer Payer of the transaction and initialization fees + * @param seeds Seeds to derive the new account address + * @param programId Owner of the new account + * @param addressTree Optional address tree. Defaults to a current + * shared address tree. + * @param addressQueue Optional address queue. Defaults to a current + * shared address queue. + * @param outputStateTreeInfo Optional output state tree. Defaults to fetching + * a current shared state tree. + * @param confirmOptions Options for confirming the transaction * - * @return Transaction signature + * @return Transaction signature */ export async function createAccount( rpc: Rpc, @@ -45,7 +46,7 @@ export async function createAccount( programId: PublicKey, addressTree?: PublicKey, addressQueue?: PublicKey, - outputStateTree?: PublicKey, + outputStateTreeInfo?: StateTreeInfo, confirmOptions?: ConfirmOptions, ): Promise { const { blockhash } = await rpc.getLatestBlockhash(); @@ -56,10 +57,9 @@ export async function createAccount( const seed = deriveAddressSeed(seeds, programId); const address = deriveAddress(seed, addressTree); - if (!outputStateTree) { + if (!outputStateTreeInfo) { const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); - const { tree } = pickRandomTreeAndQueue(stateTreeInfo); - outputStateTree = tree; + outputStateTreeInfo = pickStateTreeInfo(stateTreeInfo); } const proof = await rpc.getValidityProofV0(undefined, [ @@ -83,7 +83,7 @@ export async function createAccount( newAddress: Array.from(address.toBytes()), recentValidityProof: proof.compressedProof, programId, - outputStateTree, + outputStateTreeInfo, }); const tx = buildAndSignTx( @@ -101,23 +101,22 @@ export async function createAccount( /** * Create compressed account with address and lamports * - * @param rpc RPC to use - * @param payer Payer of the transaction and initialization fees - * @param seeds Seeds to derive the new account address - * @param lamports Number of compressed lamports to initialize the - * account with - * @param programId Owner of the new account - * @param addressTree Optional address tree. Defaults to a current shared - * address tree. - * @param addressQueue Optional address queue. Defaults to a current shared - * address queue. - * @param outputStateTree Optional output state tree. Defaults to a current - * shared state tree. - * @param confirmOptions Options for confirming the transaction + * @param rpc RPC to use + * @param payer Payer of the transaction and initialization fees + * @param seeds Seeds to derive the new account address + * @param lamports Number of compressed lamports to initialize the + * account with + * @param programId Owner of the new account + * @param addressTree Optional address tree. Defaults to a current + * shared address tree. + * @param addressQueue Optional address queue. Defaults to a current + * shared address queue. + * @param outputStateTreeInfo Optional output state tree. Defaults to a + * current shared state tree. + * @param confirmOptions Options for confirming the transaction * - * @return Transaction signature + * @return Transaction signature */ -// TODO: add support for payer != user owner export async function createAccountWithLamports( rpc: Rpc, payer: Signer, @@ -126,7 +125,7 @@ export async function createAccountWithLamports( programId: PublicKey, addressTree?: PublicKey, addressQueue?: PublicKey, - outputStateTree?: PublicKey, + outputStateTreeInfo?: StateTreeInfo, confirmOptions?: ConfirmOptions, ): Promise { lamports = bn(lamports); @@ -140,10 +139,9 @@ export async function createAccountWithLamports( lamports, ); - if (!outputStateTree) { + if (!outputStateTreeInfo) { const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); - const { tree } = pickRandomTreeAndQueue(stateTreeInfo); - outputStateTree = tree; + outputStateTreeInfo = pickStateTreeInfo(stateTreeInfo); } const { blockhash } = await rpc.getLatestBlockhash(); @@ -180,7 +178,7 @@ export async function createAccountWithLamports( inputCompressedAccounts: inputAccounts, inputStateRootIndices: proof.rootIndices, programId, - outputStateTree, + outputStateTreeInfo, }); const tx = buildAndSignTx( diff --git a/js/stateless.js/src/actions/decompress.ts b/js/stateless.js/src/actions/decompress.ts index ca848c4ce0..70679d69dd 100644 --- a/js/stateless.js/src/actions/decompress.ts +++ b/js/stateless.js/src/actions/decompress.ts @@ -7,34 +7,39 @@ import { } from '@solana/web3.js'; import { LightSystemProgram, sumUpLamports } from '../programs'; import { Rpc } from '../rpc'; -import { buildAndSignTx, sendAndConfirmTx } from '../utils'; +import { buildAndSignTx, pickStateTreeInfo, sendAndConfirmTx } from '../utils'; import BN from 'bn.js'; -import { CompressedAccountWithMerkleContext, bn } from '../state'; +import { + CompressedAccountWithMerkleContext, + StateTreeInfo, + bn, +} from '../state'; /** * Decompress lamports into a solana account * - * @param rpc RPC to use - * @param payer Payer of the transaction and initialization fees - * @param lamports Amount of lamports to compress - * @param toAddress Address of the recipient compressed account - * @param outputStateTree Optional output state tree. Defaults to a current shared state tree. - * @param confirmOptions Options for confirming the transaction + * @param rpc RPC to use + * @param payer Payer of the transaction and initialization fees + * @param lamports Amount of lamports to compress + * @param toAddress Address of the recipient compressed account + * @param outputStateTreeInfo Optional output state tree. Defaults to fetching + * a current shared state tree. + * @param confirmOptions Options for confirming the transaction * * @return Transaction signature */ -/// TODO: add multisig support -/// TODO: add support for payer != owner export async function decompress( rpc: Rpc, payer: Signer, lamports: number | BN, recipient: PublicKey, - outputStateTree?: PublicKey, + outputStateTreeInfo?: StateTreeInfo, confirmOptions?: ConfirmOptions, ): Promise { - /// TODO: use dynamic state tree and nullifier queue - + if (!outputStateTreeInfo) { + const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); + outputStateTreeInfo = pickStateTreeInfo(stateTreeInfo); + } const userCompressedAccountsWithMerkleContext: CompressedAccountWithMerkleContext[] = (await rpc.getCompressedAccountsByOwner(payer.publicKey)).items; @@ -58,7 +63,7 @@ export async function decompress( const ix = await LightSystemProgram.decompress({ payer: payer.publicKey, toAddress: recipient, - outputStateTree: outputStateTree, + outputStateTreeInfo, inputCompressedAccounts: userCompressedAccountsWithMerkleContext, recentValidityProof: proof.compressedProof, recentInputStateRootIndices: proof.rootIndices, diff --git a/js/stateless.js/src/actions/transfer.ts b/js/stateless.js/src/actions/transfer.ts index 7df9d106dc..5610e6d576 100644 --- a/js/stateless.js/src/actions/transfer.ts +++ b/js/stateless.js/src/actions/transfer.ts @@ -13,23 +13,26 @@ import { } from '../programs'; import { Rpc } from '../rpc'; -import { bn, CompressedAccountWithMerkleContext } from '../state'; -import { buildAndSignTx, sendAndConfirmTx } from '../utils'; +import { + bn, + CompressedAccountWithMerkleContext, + StateTreeInfo, +} from '../state'; +import { buildAndSignTx, pickStateTreeInfo, sendAndConfirmTx } from '../utils'; import { GetCompressedAccountsByOwnerConfig } from '../rpc-interface'; /** * Transfer compressed lamports from one owner to another * - * @param rpc Rpc to use - * @param payer Payer of transaction fees - * @param lamports Number of lamports to transfer - * @param owner Owner of the compressed lamports - * @param toAddress Destination address of the recipient - * @param merkleTree State tree account that the compressed lamports should be - * inserted into. Defaults to the default state tree account. - * @param confirmOptions Options for confirming the transaction - * @param config Configuration for fetching compressed accounts - * + * @param rpc Rpc to use + * @param payer Payer of transaction fees + * @param lamports Number of lamports to transfer + * @param owner Owner of the compressed lamports + * @param toAddress Destination address of the recipient + * @param outputStateTreeInfo State tree account that the compressed lamports + * should be inserted into. Defaults to the default + * state tree account. + * @param confirmOptions Options for confirming the transaction * * @return Signature of the confirmed transaction */ @@ -39,7 +42,7 @@ export async function transfer( lamports: number | BN, owner: Signer, toAddress: PublicKey, - merkleTree?: PublicKey, + outputStateTreeInfo?: StateTreeInfo, confirmOptions?: ConfirmOptions, ): Promise { let accumulatedLamports = bn(0); @@ -48,6 +51,11 @@ export async function transfer( const batchSize = 1000; // Maximum allowed by the API lamports = bn(lamports); + if (!outputStateTreeInfo) { + const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); + outputStateTreeInfo = pickStateTreeInfo(stateTreeInfo); + } + while (accumulatedLamports.lt(lamports)) { const batchConfig: GetCompressedAccountsByOwnerConfig = { filters: undefined, @@ -95,7 +103,7 @@ export async function transfer( lamports, recentInputStateRootIndices: proof.rootIndices, recentValidityProof: proof.compressedProof, - outputStateTrees: merkleTree, + outputStateTreeInfo, }); const { blockhash } = await rpc.getLatestBlockhash(); diff --git a/js/stateless.js/src/constants.ts b/js/stateless.js/src/constants.ts index c4c4e8c807..19c250d927 100644 --- a/js/stateless.js/src/constants.ts +++ b/js/stateless.js/src/constants.ts @@ -1,7 +1,7 @@ import BN from 'bn.js'; import { Buffer } from 'buffer'; import { ConfirmOptions, PublicKey } from '@solana/web3.js'; -import { ActiveTreeBundle, TreeType } from './state/types'; +import { StateTreeInfo, TreeType } from './state/types'; export const FIELD_SIZE = new BN( '21888242871839275222246405745257275088548364400416034343698204186575808495617', @@ -104,26 +104,26 @@ export const isLocalTest = (url: string) => { /** * @internal */ -export const localTestActiveStateTreeInfo = (): ActiveTreeBundle[] => { +export const localTestActiveStateTreeInfo = (): StateTreeInfo[] => { return [ { tree: new PublicKey(merkletreePubkey), queue: new PublicKey(nullifierQueuePubkey), cpiContext: new PublicKey(cpiContextPubkey), - treeType: TreeType.State, + treeType: TreeType.StateV1, }, { tree: new PublicKey(merkleTree2Pubkey), queue: new PublicKey(nullifierQueue2Pubkey), cpiContext: new PublicKey(cpiContext2Pubkey), - treeType: TreeType.State, + treeType: TreeType.StateV1, }, ]; }; /** * Use only with Localnet testing. - * For public networks, fetch via {@link defaultStateTreeLookupTables} and {@link getLightStateTreeInfo}. + * For public networks, fetch via {@link defaultStateTreeLookupTables} and {@link getActiveStateTreeInfos}. */ export const defaultTestStateTreeAccounts = () => { return { diff --git a/js/stateless.js/src/instruction/pack-compressed-accounts.ts b/js/stateless.js/src/instruction/pack-compressed-accounts.ts index 66b6270579..9a66550272 100644 --- a/js/stateless.js/src/instruction/pack-compressed-accounts.ts +++ b/js/stateless.js/src/instruction/pack-compressed-accounts.ts @@ -3,6 +3,8 @@ import { CompressedAccount, OutputCompressedAccountWithPackedContext, PackedCompressedAccountWithMerkleContext, + StateTreeInfo, + TreeType, } from '../state'; import { CompressedAccountWithMerkleContext } from '../state/compressed-account'; import { toArray } from '../utils/conversion'; @@ -98,11 +100,9 @@ export function toAccountMetas(remainingAccounts: PublicKey[]): AccountMeta[] { * input state. The expiry is tied to * the proof. * @param outputCompressedAccounts Ix output state to be created - * @param outputStateMerkleTrees Optional output state trees to be - * inserted into the output state. - * Defaults to the 0th state tree of - * the input state. Gets padded to the - * length of outputCompressedAccounts. + * @param outputStateTreeInfo The output state tree info. Gets + * padded to the length of + * outputCompressedAccounts. * * @param remainingAccounts Optional existing array of accounts * to append to. @@ -111,7 +111,7 @@ export function packCompressedAccounts( inputCompressedAccounts: CompressedAccountWithMerkleContext[], inputStateRootIndices: number[], outputCompressedAccounts: CompressedAccount[], - outputStateMerkleTrees?: PublicKey[] | PublicKey, + outputStateTreeInfo: StateTreeInfo, remainingAccounts: PublicKey[] = [], ): { packedInputCompressedAccounts: PackedCompressedAccountWithMerkleContext[]; @@ -156,17 +156,16 @@ export function packCompressedAccounts( }); }); - if ( - outputStateMerkleTrees === undefined && - inputCompressedAccounts.length === 0 - ) { - throw new Error( - 'No input compressed accounts nor output state trees provided. Please pass in at least one of the following: outputStateMerkleTree or inputCompressedAccount', - ); + if (outputStateTreeInfo.treeType === TreeType.StateV2) { + throw new Error('V2 trees are not supported yet'); } + // internal. v2 trees require the output queue account instead of directly + // appending to the merkle tree. + const outputTreeOrQueue = outputStateTreeInfo.tree; + /// output const paddedOutputStateMerkleTrees = padOutputStateMerkleTrees( - outputStateMerkleTrees, + outputTreeOrQueue, outputCompressedAccounts.length, inputCompressedAccounts, ); diff --git a/js/stateless.js/src/programs/layout.ts b/js/stateless.js/src/programs/layout.ts index f50ce276ab..d63bb8d2ee 100644 --- a/js/stateless.js/src/programs/layout.ts +++ b/js/stateless.js/src/programs/layout.ts @@ -154,14 +154,41 @@ export function decodeInstructionDataInvokeCpi( } export type invokeAccountsLayoutParams = { + /** + * Fee payer. + */ feePayer: PublicKey; + /** + * Authority. + */ authority: PublicKey; + /** + * The registered program pda + */ registeredProgramPda: PublicKey; + /** + * Noop program. + */ noopProgram: PublicKey; + /** + * Account compression authority. + */ accountCompressionAuthority: PublicKey; + /** + * Account compression program. + */ accountCompressionProgram: PublicKey; + /** + * Solana pool pda. Some() if compression or decompression is done. + */ solPoolPda: PublicKey | null; + /** + * Decompression recipient. + */ decompressionRecipient: PublicKey | null; + /** + * Solana system program. + */ systemProgram: PublicKey; }; diff --git a/js/stateless.js/src/programs/system.ts b/js/stateless.js/src/programs/system.ts index 401a9117c4..67ad7f36a5 100644 --- a/js/stateless.js/src/programs/system.ts +++ b/js/stateless.js/src/programs/system.ts @@ -10,6 +10,7 @@ import { CompressedAccountWithMerkleContext, CompressedProof, InstructionDataInvoke, + StateTreeInfo, bn, createCompressedAccount, } from '../state'; @@ -31,6 +32,13 @@ export const sumUpLamports = ( ); }; +type StorageOptions = { + /** + * The state tree info that the tx output should be inserted into. Defaults to a + * public state tree if unspecified. + */ + stateTreeInfo: StateTreeInfo; +}; /** * Create compressed account system transaction params */ @@ -52,7 +60,7 @@ type CreateAccountWithSeedParams = { /** * State tree pubkey. Defaults to a public state tree if unspecified. */ - outputStateTree?: PublicKey; + outputStateTreeInfo: StateTreeInfo; /** * Public key of the program to assign as the owner of the created account */ @@ -110,7 +118,7 @@ type TransferParams = { * single PublicKey or an array of PublicKey. Defaults to the 0th state tree * of input state. */ - outputStateTrees?: PublicKey[] | PublicKey; + outputStateTreeInfo: StateTreeInfo; }; /// TODO: @@ -136,7 +144,7 @@ type CompressParams = { * The state tree that the tx output should be inserted into. Defaults to a * public state tree if unspecified. */ - outputStateTree?: PublicKey; + outputStateTreeInfo: StateTreeInfo; }; /** @@ -176,7 +184,7 @@ type DecompressParams = { * single PublicKey or an array of PublicKey. Defaults to the 0th state tree * of input state. */ - outputStateTree?: PublicKey; + outputStateTreeInfo: StateTreeInfo; }; const SOL_POOL_PDA_SEED = Buffer.from('sol_pool_pda'); @@ -305,7 +313,7 @@ export class LightSystemProgram { newAddressParams, newAddress, recentValidityProof, - outputStateTree, + outputStateTreeInfo, inputCompressedAccounts, inputStateRootIndices, lamports, @@ -326,7 +334,7 @@ export class LightSystemProgram { inputCompressedAccounts ?? [], inputStateRootIndices ?? [], outputCompressedAccounts, - outputStateTree, + outputStateTreeInfo, ); const { newAddressParamsPacked, remainingAccounts } = @@ -372,7 +380,7 @@ export class LightSystemProgram { lamports, recentInputStateRootIndices, recentValidityProof, - outputStateTrees, + outputStateTreeInfo, }: TransferParams): Promise { /// Create output state const outputCompressedAccounts = this.createTransferOutputState( @@ -390,7 +398,7 @@ export class LightSystemProgram { inputCompressedAccounts, recentInputStateRootIndices, outputCompressedAccounts, - outputStateTrees, + outputStateTreeInfo, ); /// Encode instruction data @@ -434,7 +442,7 @@ export class LightSystemProgram { payer, toAddress, lamports, - outputStateTree, + outputStateTreeInfo, }: CompressParams): Promise { /// Create output state lamports = bn(lamports); @@ -453,7 +461,7 @@ export class LightSystemProgram { [], [], [outputCompressedAccount], - outputStateTree, + outputStateTreeInfo, ); /// Encode instruction data @@ -499,7 +507,7 @@ export class LightSystemProgram { lamports, recentInputStateRootIndices, recentValidityProof, - outputStateTree, + outputStateTreeInfo, }: DecompressParams): Promise { /// Create output state lamports = bn(lamports); @@ -518,7 +526,7 @@ export class LightSystemProgram { inputCompressedAccounts, recentInputStateRootIndices, outputCompressedAccounts, - outputStateTree, + outputStateTreeInfo, ); /// Encode instruction data const rawInputs: InstructionDataInvoke = { diff --git a/js/stateless.js/src/rpc.ts b/js/stateless.js/src/rpc.ts index f10a76fbb8..0bca1953ea 100644 --- a/js/stateless.js/src/rpc.ts +++ b/js/stateless.js/src/rpc.ts @@ -71,8 +71,8 @@ import { negateAndCompressProof, } from './utils/parse-validity-proof'; import { LightWasm } from './test-helpers'; -import { getLightStateTreeInfo } from './utils/get-light-state-tree-info'; -import { ActiveTreeBundle } from './state/types'; +import { getActiveStateTreeInfos } from './utils/get-light-state-tree-info'; +import { StateTreeInfo } from './state/types'; import { validateNumbersForProof } from './utils'; /** @internal */ @@ -188,7 +188,7 @@ async function getCompressedTokenAccountsByOwnerOrDelegate( /** @internal */ function buildCompressedAccountWithMaybeTokenData( accountStructWithOptionalTokenData: any, - activeStateTreeInfo: ActiveTreeBundle[], + activeStateTreeInfo: StateTreeInfo[], ): { account: CompressedAccountWithMerkleContext; maybeTokenData: TokenData | null; @@ -559,7 +559,7 @@ export function getPublicInputHash( * @returns The queue for the given tree, or undefined if not found */ export function getQueueForTree( - info: ActiveTreeBundle[], + info: StateTreeInfo[], tree: PublicKey, ): PublicKey { const index = info.findIndex(t => t.tree.equals(tree)); @@ -582,7 +582,7 @@ export function getQueueForTree( * @returns The tree for the given queue, or undefined if not found */ export function getTreeForQueue( - info: ActiveTreeBundle[], + info: StateTreeInfo[], queue: PublicKey, ): PublicKey { const index = info.findIndex(q => q.queue?.equals(queue)); @@ -597,37 +597,13 @@ export function getTreeForQueue( return info[index].tree; } -/** - * Get a random tree and queue from the active state tree addresses. - * - * Prevents write lock contention on state trees. - * - * @param info - The active state tree addresses - * @returns A random tree and queue - */ -export function pickRandomTreeAndQueue(info: ActiveTreeBundle[]): { - tree: PublicKey; - queue: PublicKey; -} { - const length = info.length; - const index = Math.floor(Math.random() * length); - - if (!info[index].queue) { - throw new Error('Queue must not be null for state tree'); - } - return { - tree: info[index].tree, - queue: info[index].queue, - }; -} - /** * */ export class Rpc extends Connection implements CompressionApiInterface { compressionApiEndpoint: string; proverEndpoint: string; - activeStateTreeInfo: ActiveTreeBundle[] | null = null; + activeStateTreeInfo: StateTreeInfo[] | null = null; constructor( endpoint: string, @@ -643,7 +619,7 @@ export class Rpc extends Connection implements CompressionApiInterface { /** * Manually set state tree addresses */ - setStateTreeInfo(info: ActiveTreeBundle[]): void { + setStateTreeInfo(info: StateTreeInfo[]): void { this.activeStateTreeInfo = info; } @@ -651,16 +627,16 @@ export class Rpc extends Connection implements CompressionApiInterface { * Get the active state tree addresses from the cluster. * If not already cached, fetches from the cluster. */ - async getCachedActiveStateTreeInfo(): Promise { + async getCachedActiveStateTreeInfo(): Promise { if (isLocalTest(this.rpcEndpoint)) { return localTestActiveStateTreeInfo(); } - let info: ActiveTreeBundle[] | null = null; + let info: StateTreeInfo[] | null = null; if (!this.activeStateTreeInfo) { const { mainnet, devnet } = defaultStateTreeLookupTables(); try { - info = await getLightStateTreeInfo({ + info = await getActiveStateTreeInfos({ connection: this, stateTreeLookupTableAddress: mainnet[0].stateTreeLookupTable, @@ -668,7 +644,7 @@ export class Rpc extends Connection implements CompressionApiInterface { }); this.activeStateTreeInfo = info; } catch { - info = await getLightStateTreeInfo({ + info = await getActiveStateTreeInfos({ connection: this, stateTreeLookupTableAddress: devnet[0].stateTreeLookupTable, nullifyTableAddress: devnet[0].nullifyTable, @@ -690,7 +666,7 @@ export class Rpc extends Connection implements CompressionApiInterface { /** * Fetch the latest state tree addresses from the cluster. */ - async getLatestActiveStateTreeInfo(): Promise { + async getLatestActiveStateTreeInfo(): Promise { this.activeStateTreeInfo = null; return await this.getCachedActiveStateTreeInfo(); } diff --git a/js/stateless.js/src/state/types.ts b/js/stateless.js/src/state/types.ts index 1717951314..9186d37554 100644 --- a/js/stateless.js/src/state/types.ts +++ b/js/stateless.js/src/state/types.ts @@ -7,25 +7,68 @@ export enum TreeType { /** * v1 state merkle tree */ - State = 0, + StateV1 = 1, /** * v1 address merkle tree */ - Address = 1, + AddressV1 = 2, /** * v2 state merkle tree */ - BatchedState = 2, + StateV2 = 3, /** * v2 address merkle tree */ - BatchedAddress = 3, + AddressV2 = 4, } -export type ActiveTreeBundle = { +// /** +// * @deprecated Use {@link StateTreeInfo} instead. +// * A bundle of active trees for a given tree type. +// */ +// export type ActiveTreeBundle = { +// tree: PublicKey; +// queue: PublicKey | null; +// cpiContext: PublicKey | null; +// treeType: TreeType; +// }; + +/** + * Public keys for a state tree, versioned via {@link TreeType}. The protocol + * stores compressed accounts in state trees. + * + * Onchain Accounts are subject to Solana's write-lock limits. + * + * To load balance transactions, use {@link pickStateTreeInfo} to + * select a random tree from active Trees. + * + * Example: + * ```typescript + * const infos = await getCachedActiveStateTreeInfos(); + * const info = pickStateTreeInfo(infos); + * const ix = CompressedTokenProgram.compress({ + * ... // other params + * outputStateTree: info + * }); + * ``` + */ +export type StateTreeInfo = { + /** + * Account containing the Sparse Merkle tree in which a compressed + * account is stored. + */ tree: PublicKey; + /** + * The state nullfier queue belonging to merkleTree. + */ queue: PublicKey | null; + /** + * The compressed cpi context account. + */ cpiContext: PublicKey | null; + /** + * The type of tree. One of {@link TreeType}. + */ treeType: TreeType; }; diff --git a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts index aaef9cb7e1..c3bbdbf9d5 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts @@ -51,7 +51,7 @@ import { convertNonInclusionMerkleProofInputsToHex, proverRequest, } from '../../rpc'; -import { ActiveTreeBundle } from '../../state/types'; +import { StateTreeInfo } from '../../state/types'; export interface TestRpcConfig { /** @@ -151,7 +151,7 @@ export class TestRpc extends Connection implements CompressionApiInterface { lightWasm: LightWasm; depth: number; log = false; - activeStateTreeInfo: ActiveTreeBundle[] | null = null; + activeStateTreeInfo: StateTreeInfo[] | null = null; /** * Establish a Compression-compatible JSON RPC mock-connection @@ -207,21 +207,21 @@ export class TestRpc extends Connection implements CompressionApiInterface { /** * Manually set state tree addresses */ - setStateTreeInfo(info: ActiveTreeBundle[]): void { + setStateTreeInfo(info: StateTreeInfo[]): void { this.activeStateTreeInfo = info; } /** * Returns local test state trees. */ - async getCachedActiveStateTreeInfo(): Promise { + async getCachedActiveStateTreeInfo(): Promise { return localTestActiveStateTreeInfo(); } /** * Returns local test state trees. */ - async getLatestActiveStateTreeInfo(): Promise { + async getLatestActiveStateTreeInfo(): Promise { return localTestActiveStateTreeInfo(); } diff --git a/js/stateless.js/src/utils/get-light-state-tree-info.ts b/js/stateless.js/src/utils/get-light-state-tree-info.ts index e129111b2d..8f16fb2d4d 100644 --- a/js/stateless.js/src/utils/get-light-state-tree-info.ts +++ b/js/stateless.js/src/utils/get-light-state-tree-info.ts @@ -7,17 +7,67 @@ import { } from '@solana/web3.js'; import { buildAndSignTx, sendAndConfirmTx } from './send-and-confirm'; import { dedupeSigner } from '../actions'; -import { ActiveTreeBundle, TreeType } from '../state/types'; +import { StateTreeInfo, TreeType } from '../state/types'; +import { Rpc } from '../rpc'; + +/** + * @deprecated use {@link pickStateTreeInfo} instead. Get a random tree and + * queue from the active state tree addresses. + * + * Prevents write lock contention on state trees. + * + * @param info The active state tree addresses + * @returns A random tree and queue + */ +export function pickRandomTreeAndQueue(info: StateTreeInfo[]): { + tree: PublicKey; + queue: PublicKey; +} { + const length = info.length; + const index = Math.floor(Math.random() * length); + + if (!info[index].queue) { + throw new Error('Queue must not be null for state tree'); + } + return { + tree: info[index].tree, + queue: info[index].queue, + }; +} + +/** + * Get a random State tree and context from the active state tree addresses. + * + * Prevents write lock contention on state trees. + * + * @param info The active state tree addresses + * @param treeType The type of tree. Defaults to TreeType.StateV2 + * @returns A random tree and queue + */ +export function pickStateTreeInfo( + info: StateTreeInfo[], + treeType: TreeType = TreeType.StateV1, +): StateTreeInfo { + const filteredInfo = info.filter(t => t.treeType === treeType); + const length = filteredInfo.length; + const index = Math.floor(Math.random() * length); + + if (!filteredInfo[index].queue) { + throw new Error('Queue must not be null for state tree'); + } + + return filteredInfo[index]; +} /** * Create two lookup tables storing all public state tree and queue addresses * returns lookup table addresses and txId * * @internal - * @param connection - Connection to the Solana network - * @param payer - Keypair of the payer - * @param authority - Keypair of the authority - * @param recentSlot - Slot of the recent block + * @param connection Connection to the Solana network + * @param payer Keypair of the payer + * @param authority Keypair of the authority + * @param recentSlot Slot of the recent block */ export async function createStateTreeLookupTable({ connection, @@ -45,8 +95,8 @@ export async function createStateTreeLookupTable({ blockhash.blockhash, dedupeSigner(payer as Signer, [authority]), ); - // @ts-expect-error - const txId = await sendAndConfirmTx(connection, tx); + + const txId = await sendAndConfirmTx(connection as Rpc, tx); return { address: lookupTableAddress1, @@ -56,14 +106,15 @@ export async function createStateTreeLookupTable({ /** * Extend state tree lookup table with new state tree and queue addresses + * * @internal - * @param connection - Connection to the Solana network - * @param tableAddress - Address of the lookup table to extend - * @param newStateTreeAddresses - Addresses of the new state trees to add - * @param newQueueAddresses - Addresses of the new queues to add - * @param newCpiContextAddresses - Addresses of the new cpi contexts to add - * @param payer - Keypair of the payer - * @param authority - Keypair of the authority + * @param connection Connection to the Solana network + * @param tableAddress Address of the lookup table to extend + * @param newStateTreeAddresses Addresses of the new state trees to add + * @param newQueueAddresses Addresses of the new queues to add + * @param newCpiContextAddresses Addresses of the new cpi contexts to add + * @param payer Keypair of the payer + * @param authority Keypair of the authority */ export async function extendStateTreeLookupTable({ connection, @@ -117,9 +168,8 @@ export async function extendStateTreeLookupTable({ blockhash.blockhash, dedupeSigner(payer as Signer, [authority]), ); - // we pass a Connection type so we don't have to depend on the Rpc module. - // @ts-expect-error - const txId = await sendAndConfirmTx(connection, tx); + + const txId = await sendAndConfirmTx(connection as Rpc, tx); return { tableAddress, @@ -130,15 +180,16 @@ export async function extendStateTreeLookupTable({ /** * Adds state tree address to lookup table. Acts as nullifier lookup for rolled * over state trees. + * * @internal - * @param connection - Connection to the Solana network - * @param stateTreeAddress - Address of the state tree to nullify - * @param nullifyTableAddress - Address of the nullifier lookup table to store - * address in - * @param stateTreeLookupTableAddress - lookup table storing all state tree - * addresses - * @param payer - Keypair of the payer - * @param authority - Keypair of the authority + * @param connection Connection to the Solana network + * @param stateTreeAddress Address of the state tree to nullify + * @param nullifyTableAddress Address of the nullifier lookup table to + * store address in + * @param stateTreeLookupTableAddress lookup table storing all state tree + * addresses + * @param payer Keypair of the payer + * @param authority Keypair of the authority */ export async function nullifyLookupTable({ connection, @@ -204,10 +255,10 @@ export async function nullifyLookupTable({ } /** - * Get most recent , active state tree data - * we store in lookup table for each public state tree + * Get most recent active state tree data we store in lookup table for each + * public state tree */ -export async function getLightStateTreeInfo({ +export async function getActiveStateTreeInfos({ connection, stateTreeLookupTableAddress, nullifyTableAddress, @@ -215,7 +266,7 @@ export async function getLightStateTreeInfo({ connection: Connection; stateTreeLookupTableAddress: PublicKey; nullifyTableAddress: PublicKey; -}): Promise { +}): Promise { const stateTreeLookupTable = await connection.getAddressLookupTable( stateTreeLookupTableAddress, ); @@ -238,20 +289,20 @@ export async function getLightStateTreeInfo({ const stateTreePubkeys = stateTreeLookupTable.value.state.addresses; const nullifyTablePubkeys = nullifyTable.value.state.addresses; - const bundles: ActiveTreeBundle[] = []; + const contexts: StateTreeInfo[] = []; for (let i = 0; i < stateTreePubkeys.length; i += 3) { const tree = stateTreePubkeys[i]; // Skip rolledover (full or almost full) Merkle trees if (!nullifyTablePubkeys.includes(tree)) { - bundles.push({ + contexts.push({ tree, queue: stateTreePubkeys[i + 1], cpiContext: stateTreePubkeys[i + 2], - treeType: TreeType.State, + treeType: TreeType.StateV1, }); } } - return bundles; + return contexts; } From 70c3db7d8bd7ace8efa3d4d9d1ca4507897cd570 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 25 Mar 2025 04:59:57 +0000 Subject: [PATCH 02/19] wip --- .../src/actions/approve-and-mint-to.ts | 25 +++++-- js/compressed-token/src/actions/compress.ts | 2 +- js/compressed-token/src/actions/mint-to.ts | 2 +- .../pack-compressed-token-accounts.ts | 7 +- js/compressed-token/src/program.ts | 73 +++++++++++++------ js/compressed-token/src/types.ts | 47 ++++++++++++ .../tests/e2e/rpc-multi-trees.test.ts | 2 +- js/stateless.js/src/actions/compress.ts | 2 +- js/stateless.js/src/actions/create-account.ts | 4 +- js/stateless.js/src/actions/decompress.ts | 2 +- js/stateless.js/src/actions/transfer.ts | 2 +- js/stateless.js/src/rpc.ts | 21 +++--- .../src/test-helpers/test-rpc/test-rpc.ts | 4 +- .../tests/e2e/rpc-multi-trees.test.ts | 10 +-- 14 files changed, 143 insertions(+), 60 deletions(-) diff --git a/js/compressed-token/src/actions/approve-and-mint-to.ts b/js/compressed-token/src/actions/approve-and-mint-to.ts index ff12b120d4..a7bc973404 100644 --- a/js/compressed-token/src/actions/approve-and-mint-to.ts +++ b/js/compressed-token/src/actions/approve-and-mint-to.ts @@ -12,9 +12,12 @@ import { Rpc, dedupeSigner, pickRandomTreeAndQueue, + StateTreeInfo, + pickStateTreeInfo, } from '@lightprotocol/stateless.js'; import { CompressedTokenProgram } from '../program'; import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token'; +import { StorageOptions } from '../types'; /** * Mint compressed tokens to a solana address from an external mint authority @@ -25,8 +28,7 @@ import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token'; * @param destination Address of the account to mint to * @param authority Minting authority * @param amount Amount to mint - * @param merkleTree State tree account that the compressed tokens should be - * part of. Defaults to random public state tree account. + * @param storageOptions Options for storing the tokens accounts * @param confirmOptions Options for confirming the transaction * * @return Signature of the confirmed transaction @@ -38,7 +40,7 @@ export async function approveAndMintTo( destination: PublicKey, authority: Signer, amount: number | BN, - merkleTree?: PublicKey, + storageOptions?: StorageOptions, confirmOptions?: ConfirmOptions, tokenProgramId?: PublicKey, ): Promise { @@ -57,10 +59,16 @@ export async function approveAndMintTo( tokenProgramId, ); - if (!merkleTree) { - const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); - const { tree } = pickRandomTreeAndQueue(stateTreeInfo); - merkleTree = tree; + if (!storageOptions) storageOptions = {}; + if (!storageOptions.stateTreeInfo) { + const stateTreeInfos = await rpc.getCachedActiveStateTreeInfos(); + const info = pickStateTreeInfo(stateTreeInfos); + storageOptions.stateTreeInfo = info; + } + if (!storageOptions.tokenPoolInfos) { + const tokenPoolInfos = await rpc.getTokenPoolInfos(); + const info = pickTokenPoolInfos(tokenPoolInfos); + storageOptions.tokenPoolInfos = info; } const ixs = await CompressedTokenProgram.approveAndMintTo({ @@ -70,7 +78,8 @@ export async function approveAndMintTo( authorityTokenAccount: authorityTokenAccount.address, amount, toPubkey: destination, - merkleTree, + outputStateTreeInfo: storageOptions.stateTreeInfo, + tokenPoolInfos: storageOptions.tokenPoolInfos, tokenProgramId, }); diff --git a/js/compressed-token/src/actions/compress.ts b/js/compressed-token/src/actions/compress.ts index 8d58972be0..fcb5ee39fb 100644 --- a/js/compressed-token/src/actions/compress.ts +++ b/js/compressed-token/src/actions/compress.ts @@ -52,7 +52,7 @@ export async function compress( : await CompressedTokenProgram.get_mint_program_id(mint, rpc); if (!merkleTree) { - const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); + const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); const { tree } = pickRandomTreeAndQueue(stateTreeInfo); merkleTree = tree; } diff --git a/js/compressed-token/src/actions/mint-to.ts b/js/compressed-token/src/actions/mint-to.ts index 2f304613c4..dcca4518cc 100644 --- a/js/compressed-token/src/actions/mint-to.ts +++ b/js/compressed-token/src/actions/mint-to.ts @@ -50,7 +50,7 @@ export async function mintTo( const additionalSigners = dedupeSigner(payer, [authority]); if (!merkleTree) { - const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); + const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); const { tree } = pickRandomTreeAndQueue(stateTreeInfo); merkleTree = tree; } diff --git a/js/compressed-token/src/instructions/pack-compressed-token-accounts.ts b/js/compressed-token/src/instructions/pack-compressed-token-accounts.ts index a29d6e512e..a7d75ebe9b 100644 --- a/js/compressed-token/src/instructions/pack-compressed-token-accounts.ts +++ b/js/compressed-token/src/instructions/pack-compressed-token-accounts.ts @@ -4,6 +4,7 @@ import { getIndexOrAdd, bn, padOutputStateMerkleTrees, + StateTreeInfo, } from '@lightprotocol/stateless.js'; import { PublicKey, AccountMeta } from '@solana/web3.js'; import { @@ -19,7 +20,7 @@ export type PackCompressedTokenAccountsParams = { * state tree of the input state. Gets padded to the length of * outputCompressedAccounts. */ - outputStateTrees?: PublicKey[] | PublicKey; + outputStateTreeInfo: StateTreeInfo; /** Optional remaining accounts to append to */ remainingAccounts?: PublicKey[]; /** @@ -42,7 +43,7 @@ export function packCompressedTokenAccounts( } { const { inputCompressedTokenAccounts, - outputStateTrees, + outputStateTreeInfo, remainingAccounts = [], rootIndices, tokenTransferOutputs, @@ -96,7 +97,7 @@ export function packCompressedTokenAccounts( /// pack output state trees const paddedOutputStateMerkleTrees = padOutputStateMerkleTrees( - outputStateTrees, + outputStateTreeInfo.tree, tokenTransferOutputs.length, inputCompressedTokenAccounts.map(acc => acc.compressedAccount), ); diff --git a/js/compressed-token/src/program.ts b/js/compressed-token/src/program.ts index e0ddef75bc..14b81f084d 100644 --- a/js/compressed-token/src/program.ts +++ b/js/compressed-token/src/program.ts @@ -18,6 +18,7 @@ import { validateSameOwner, validateSufficientBalance, defaultTestStateTreeAccounts, + StateTreeInfo, } from '@lightprotocol/stateless.js'; import { MINT_SIZE, @@ -42,6 +43,7 @@ import { } from './layout'; import { CompressedTokenInstructionDataTransfer, + TokenPoolInfo, TokenTransferOutputData, } from './types'; @@ -75,7 +77,11 @@ export type CompressParams = { * The state tree that the tx output should be inserted into. Defaults to a * public state tree if unspecified. */ - outputStateTree?: PublicKey; + outputStateTreeInfo: StateTreeInfo; + /** + * Tokenpools. One or more token pools can be provided. + */ + tokenPoolInfos: TokenPoolInfo | TokenPoolInfo[]; /** * Optional: The token program ID. Default: SPL Token Program ID */ @@ -106,7 +112,7 @@ export type CompressSplTokenAccountParams = { /** * The state tree that the compressed token account should be inserted into. */ - outputStateTree: PublicKey; + outputStateTreeInfo: StateTreeInfo; /** * Optional: The token program ID. Default: SPL Token Program ID */ @@ -144,7 +150,11 @@ export type DecompressParams = { * The state tree that the change tx output should be inserted into. * Defaults to a public state tree if unspecified. */ - outputStateTree?: PublicKey; + outputStateTreeInfo: StateTreeInfo; + /** + * Tokenpool addresses. One or more token pools can be provided. + */ + tokenPoolInfos: TokenPoolInfo[]; /** * Optional: The token program ID. Default: SPL Token Program ID */ @@ -184,7 +194,7 @@ export type TransferParams = { * single PublicKey or an array of PublicKey. Defaults to the 0th state tree * of input state. */ - outputStateTrees?: PublicKey[] | PublicKey; + outputStateTreeInfo: StateTreeInfo; }; /** @@ -248,7 +258,7 @@ export type MergeTokenAccountsParams = { /** * Optional: Public key of the state tree to merge into */ - outputStateTree: PublicKey; + outputStateTreeInfo: StateTreeInfo; /** * Optional: Recent validity proof for state inclusion */ @@ -284,10 +294,14 @@ export type MintToParams = { */ amount: BN | BN[] | number | number[]; /** - * Public key of the state tree to mint into. Defaults to a public state - * tree if unspecified. + * The state tree to mint into. Defaults to a public state tree if + * unspecified. */ - merkleTree?: PublicKey; + outputStateTreeInfo: StateTreeInfo; + /** + * Tokenpool addresses. One or more token pools can be provided. + */ + tokenPoolInfos: TokenPoolInfo[]; /** * Optional: The token program ID. Default: SPL Token Program ID */ @@ -338,10 +352,14 @@ export type ApproveAndMintToParams = { */ amount: BN | number; /** - * Public key of the state tree to mint into. Defaults to a public state + * The state tree to mint into. Defaults to a public state * tree if unspecified. */ - merkleTree?: PublicKey; + outputStateTreeInfo: StateTreeInfo; + /** + * Tokenpool addresses. One or more token pools can be provided. + */ + tokenPoolInfos: TokenPoolInfo[]; /** * Optional: The token program ID. Default: SPL Token Program ID */ @@ -638,10 +656,11 @@ export class CompressedTokenProgram { mint, feePayer, authority, - merkleTree, + outputStateTreeInfo, toPubkey, amount, tokenProgramId, + tokenPoolInfos, } = params; const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; @@ -669,7 +688,9 @@ export class CompressedTokenProgram { noopProgram: systemKeys.noopProgram, accountCompressionAuthority: systemKeys.accountCompressionAuthority, accountCompressionProgram: systemKeys.accountCompressionProgram, - merkleTree: merkleTree ?? defaultTestStateTreeAccounts().merkleTree, + merkleTree: outputStateTreeInfo + ? outputStateTreeInfo.tree + : defaultTestStateTreeAccounts().merkleTree, selfProgram: this.programId, systemProgram: SystemProgram.programId, solPoolPda: null, // TODO: add lamports support @@ -697,9 +718,10 @@ export class CompressedTokenProgram { feePayer, authorityTokenAccount, authority, - merkleTree, + outputStateTreeInfo, toPubkey, tokenProgramId, + tokenPoolInfos, } = params; const amount: bigint = BigInt(params.amount.toString()); @@ -722,12 +744,14 @@ export class CompressedTokenProgram { toAddress: toPubkey, mint, amount: params.amount, - outputStateTree: merkleTree, + outputStateTreeInfo, + tokenPoolInfos, tokenProgramId, }); return [splMintToInstruction, compressInstruction]; } + /** * Construct transfer instruction for compressed tokens */ @@ -740,7 +764,7 @@ export class CompressedTokenProgram { recentInputStateRootIndices, recentValidityProof, amount, - outputStateTrees, + outputStateTreeInfo, toAddress, } = params; @@ -756,7 +780,7 @@ export class CompressedTokenProgram { remainingAccountMetas, } = packCompressedTokenAccounts({ inputCompressedTokenAccounts, - outputStateTrees, + outputStateTreeInfo, rootIndices: recentInputStateRootIndices, tokenTransferOutputs, }); @@ -876,8 +900,9 @@ export class CompressedTokenProgram { source, toAddress, mint, - outputStateTree, + outputStateTreeInfo, tokenProgramId, + tokenPoolInfos, } = params; if (Array.isArray(params.amount) !== Array.isArray(params.toAddress)) { @@ -920,7 +945,7 @@ export class CompressedTokenProgram { remainingAccountMetas, } = packCompressedTokenAccounts({ inputCompressedTokenAccounts: [], - outputStateTrees: outputStateTree, + outputStateTreeInfo, rootIndices: [], tokenTransferOutputs, }); @@ -976,7 +1001,7 @@ export class CompressedTokenProgram { payer, inputCompressedTokenAccounts, toAddress, - outputStateTree, + outputStateTreeInfo, recentValidityProof, recentInputStateRootIndices, tokenProgramId, @@ -995,7 +1020,7 @@ export class CompressedTokenProgram { remainingAccountMetas, } = packCompressedTokenAccounts({ inputCompressedTokenAccounts, - outputStateTrees: outputStateTree, + outputStateTreeInfo, rootIndices: recentInputStateRootIndices, tokenTransferOutputs: tokenTransferOutputs, }); @@ -1056,7 +1081,7 @@ export class CompressedTokenProgram { payer, owner, inputCompressedTokenAccounts, - outputStateTree, + outputStateTreeInfo, recentValidityProof, recentInputStateRootIndices, } = params; @@ -1073,7 +1098,7 @@ export class CompressedTokenProgram { (sum, account) => sum.add(account.parsed.amount), new BN(0), ), - outputStateTrees: outputStateTree, + outputStateTreeInfo, recentInputStateRootIndices, recentValidityProof, }); @@ -1090,14 +1115,14 @@ export class CompressedTokenProgram { tokenAccount, mint, remainingAmount, - outputStateTree, + outputStateTreeInfo, tokenProgramId, } = params; const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; const remainingAccountMetas: AccountMeta[] = [ { - pubkey: outputStateTree, + pubkey: outputStateTreeInfo.tree, isSigner: false, isWritable: true, }, diff --git a/js/compressed-token/src/types.ts b/js/compressed-token/src/types.ts index 208ea5b849..ef7822bcd1 100644 --- a/js/compressed-token/src/types.ts +++ b/js/compressed-token/src/types.ts @@ -3,6 +3,7 @@ import BN from 'bn.js'; import { CompressedProof, PackedMerkleContext, + StateTreeInfo, } from '@lightprotocol/stateless.js'; export type CompressedCpiContext = { @@ -78,6 +79,52 @@ export type CompressSplTokenAccountInstructionData = { cpiContext: CompressedCpiContext | null; }; +export enum Action { + Compress = 1, + Decompress = 2, + Transfer = 3, +} + +export type TokenPoolActivity = { + signature: string; + amount: BN; + action: Action; +}; + +export type TokenPoolInfo = { + /** + * The mint of the token pool + */ + mint: PublicKey; + /** + * The token pool address + */ + tokenPoolAddress: PublicKey; + /** + * The token program of the token pool + */ + tokenProgram: PublicKey; + /** + * count of txs and volume in the past 60 seconds. + */ + activity: { + txs: number; + amountAdded: BN; + amountRemoved: BN; + }; +}; + +export type StorageOptions = { + /** + * State tree info + */ + stateTreeInfo?: StateTreeInfo; + /** + * Whether to store the token pool info in the state tree + */ + tokenPoolInfos?: TokenPoolInfo | TokenPoolInfo[]; +}; + export type CompressedTokenInstructionDataTransfer = { /** * Validity proof diff --git a/js/compressed-token/tests/e2e/rpc-multi-trees.test.ts b/js/compressed-token/tests/e2e/rpc-multi-trees.test.ts index c97a5db3b1..900fe48fe3 100644 --- a/js/compressed-token/tests/e2e/rpc-multi-trees.test.ts +++ b/js/compressed-token/tests/e2e/rpc-multi-trees.test.ts @@ -29,7 +29,7 @@ describe('rpc-multi-trees', () => { rpc = createRpc(); treeAndQueue = pickRandomTreeAndQueue( - await rpc.getCachedActiveStateTreeInfo(), + await rpc.getCachedActiveStateTreeInfos(), ); payer = await newAccountWithLamports(rpc, 1e9, 252); diff --git a/js/stateless.js/src/actions/compress.ts b/js/stateless.js/src/actions/compress.ts index d1c47e539a..6ed7e414c9 100644 --- a/js/stateless.js/src/actions/compress.ts +++ b/js/stateless.js/src/actions/compress.ts @@ -37,7 +37,7 @@ export async function compress( const { blockhash } = await rpc.getLatestBlockhash(); if (!outputStateTreeInfo) { - const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); + const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); outputStateTreeInfo = pickStateTreeInfo(stateTreeInfo); } diff --git a/js/stateless.js/src/actions/create-account.ts b/js/stateless.js/src/actions/create-account.ts index b80d0454bb..138d35d538 100644 --- a/js/stateless.js/src/actions/create-account.ts +++ b/js/stateless.js/src/actions/create-account.ts @@ -58,7 +58,7 @@ export async function createAccount( const address = deriveAddress(seed, addressTree); if (!outputStateTreeInfo) { - const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); + const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); outputStateTreeInfo = pickStateTreeInfo(stateTreeInfo); } @@ -140,7 +140,7 @@ export async function createAccountWithLamports( ); if (!outputStateTreeInfo) { - const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); + const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); outputStateTreeInfo = pickStateTreeInfo(stateTreeInfo); } diff --git a/js/stateless.js/src/actions/decompress.ts b/js/stateless.js/src/actions/decompress.ts index 70679d69dd..a344f80f72 100644 --- a/js/stateless.js/src/actions/decompress.ts +++ b/js/stateless.js/src/actions/decompress.ts @@ -37,7 +37,7 @@ export async function decompress( confirmOptions?: ConfirmOptions, ): Promise { if (!outputStateTreeInfo) { - const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); + const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); outputStateTreeInfo = pickStateTreeInfo(stateTreeInfo); } const userCompressedAccountsWithMerkleContext: CompressedAccountWithMerkleContext[] = diff --git a/js/stateless.js/src/actions/transfer.ts b/js/stateless.js/src/actions/transfer.ts index 5610e6d576..2eea015ad5 100644 --- a/js/stateless.js/src/actions/transfer.ts +++ b/js/stateless.js/src/actions/transfer.ts @@ -52,7 +52,7 @@ export async function transfer( lamports = bn(lamports); if (!outputStateTreeInfo) { - const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); + const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); outputStateTreeInfo = pickStateTreeInfo(stateTreeInfo); } diff --git a/js/stateless.js/src/rpc.ts b/js/stateless.js/src/rpc.ts index 0bca1953ea..eda7f8a471 100644 --- a/js/stateless.js/src/rpc.ts +++ b/js/stateless.js/src/rpc.ts @@ -126,7 +126,7 @@ async function getCompressedTokenAccountsByOwnerOrDelegate( } const accounts: ParsedTokenAccount[] = []; - const activeStateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); + const activeStateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); res.result.value.items.map(item => { const _account = item.account; @@ -624,10 +624,11 @@ export class Rpc extends Connection implements CompressionApiInterface { } /** + * * Get the active state tree addresses from the cluster. * If not already cached, fetches from the cluster. */ - async getCachedActiveStateTreeInfo(): Promise { + async getCachedActiveStateTreeInfos(): Promise { if (isLocalTest(this.rpcEndpoint)) { return localTestActiveStateTreeInfo(); } @@ -666,9 +667,9 @@ export class Rpc extends Connection implements CompressionApiInterface { /** * Fetch the latest state tree addresses from the cluster. */ - async getLatestActiveStateTreeInfo(): Promise { + async getActiveStateTreeInfos(): Promise { this.activeStateTreeInfo = null; - return await this.getCachedActiveStateTreeInfo(); + return await this.getCachedActiveStateTreeInfos(); } /** @@ -706,7 +707,7 @@ export class Rpc extends Connection implements CompressionApiInterface { return null; } - const activeStateTreeInfo = await this.getCachedActiveStateTreeInfo(); + const activeStateTreeInfo = await this.getCachedActiveStateTreeInfos(); const associatedQueue = getQueueForTree( activeStateTreeInfo, res.result.value.tree!, @@ -815,7 +816,7 @@ export class Rpc extends Connection implements CompressionApiInterface { `failed to get proof for compressed account ${hash.toString()}`, ); } - const activeStateTreeInfo = await this.getCachedActiveStateTreeInfo(); + const activeStateTreeInfo = await this.getCachedActiveStateTreeInfos(); const associatedQueue = getQueueForTree( activeStateTreeInfo, res.result.value.merkleTree, @@ -860,7 +861,7 @@ export class Rpc extends Connection implements CompressionApiInterface { `failed to get info for compressed accounts ${hashes.map(hash => encodeBN254toBase58(hash)).join(', ')}`, ); } - const activeStateTreeInfo = await this.getCachedActiveStateTreeInfo(); + const activeStateTreeInfo = await this.getCachedActiveStateTreeInfos(); const accounts: CompressedAccountWithMerkleContext[] = []; res.result.value.items.map(item => { const associatedQueue = getQueueForTree( @@ -916,7 +917,7 @@ export class Rpc extends Connection implements CompressionApiInterface { const merkleProofs: MerkleContextWithMerkleProof[] = []; - const activeStateTreeInfo = await this.getCachedActiveStateTreeInfo(); + const activeStateTreeInfo = await this.getCachedActiveStateTreeInfos(); for (const proof of res.result.value) { const associatedQueue = getQueueForTree( activeStateTreeInfo, @@ -974,7 +975,7 @@ export class Rpc extends Connection implements CompressionApiInterface { }; } const accounts: CompressedAccountWithMerkleContext[] = []; - const activeStateTreeInfo = await this.getCachedActiveStateTreeInfo(); + const activeStateTreeInfo = await this.getCachedActiveStateTreeInfos(); res.result.value.items.map(item => { const associatedQueue = getQueueForTree( @@ -1233,7 +1234,7 @@ export class Rpc extends Connection implements CompressionApiInterface { maybeTokenData: TokenData | null; }[] = []; - const activeStateTreeInfo = await this.getCachedActiveStateTreeInfo(); + const activeStateTreeInfo = await this.getCachedActiveStateTreeInfos(); res.result.compressionInfo.closedAccounts.map(item => { closedAccounts.push( diff --git a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts index c3bbdbf9d5..f6e5fe6a3c 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts @@ -214,14 +214,14 @@ export class TestRpc extends Connection implements CompressionApiInterface { /** * Returns local test state trees. */ - async getCachedActiveStateTreeInfo(): Promise { + async getCachedActiveStateTreeInfos(): Promise { return localTestActiveStateTreeInfo(); } /** * Returns local test state trees. */ - async getLatestActiveStateTreeInfo(): Promise { + async getActiveStateTreeInfos(): Promise { return localTestActiveStateTreeInfo(); } diff --git a/js/stateless.js/tests/e2e/rpc-multi-trees.test.ts b/js/stateless.js/tests/e2e/rpc-multi-trees.test.ts index 0af156b695..268caeeec5 100644 --- a/js/stateless.js/tests/e2e/rpc-multi-trees.test.ts +++ b/js/stateless.js/tests/e2e/rpc-multi-trees.test.ts @@ -78,7 +78,7 @@ describe('rpc-multi-trees', () => { let address: PublicKey; it('must create account with random output tree (pickRandomTreeAndQueue)', async () => { const tree = pickRandomTreeAndQueue( - await rpc.getCachedActiveStateTreeInfo(), + await rpc.getCachedActiveStateTreeInfos(), ); const seed = randomBytes(32); @@ -121,7 +121,7 @@ describe('rpc-multi-trees', () => { /// Executes transfers using random output trees const tree1 = pickRandomTreeAndQueue( - await rpc.getCachedActiveStateTreeInfo(), + await rpc.getCachedActiveStateTreeInfos(), ); await transfer(rpc, payer, 1e5, payer, bob.publicKey, tree1.tree); executedTxs++; @@ -129,7 +129,7 @@ describe('rpc-multi-trees', () => { randQueues.push(tree1.queue); const tree2 = pickRandomTreeAndQueue( - await rpc.getCachedActiveStateTreeInfo(), + await rpc.getCachedActiveStateTreeInfos(), ); await transfer(rpc, payer, 1e5, payer, bob.publicKey, tree2.tree); executedTxs++; @@ -196,7 +196,7 @@ describe('rpc-multi-trees', () => { /// Creates a compressed account with address and lamports using a /// (combined) 'validityProof' from Photon const tree = pickRandomTreeAndQueue( - await rpc.getCachedActiveStateTreeInfo(), + await rpc.getCachedActiveStateTreeInfos(), ); await createAccountWithLamports( rpc, @@ -239,7 +239,7 @@ describe('rpc-multi-trees', () => { }); const tree = pickRandomTreeAndQueue( - await rpc.getCachedActiveStateTreeInfo(), + await rpc.getCachedActiveStateTreeInfos(), ); await transfer( rpc, From 3865aac27eef5b92037f6cac6b08e37a960d5fc4 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 25 Mar 2025 18:31:04 +0000 Subject: [PATCH 03/19] add getTokenPoolInfos --- js/compressed-token/src/program.ts | 12 +++++++++ js/stateless.js/src/rpc.ts | 40 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/js/compressed-token/src/program.ts b/js/compressed-token/src/program.ts index 14b81f084d..3fa2f7058d 100644 --- a/js/compressed-token/src/program.ts +++ b/js/compressed-token/src/program.ts @@ -557,6 +557,18 @@ export class CompressedTokenProgram { ); return address; } + /** @internal */ + static deriveTokenPoolPdaWithBump( + mint: PublicKey, + bump: number, + ): PublicKey { + const seeds = [POOL_SEED, mint.toBuffer(), Buffer.from([bump])]; + const [address, _] = PublicKey.findProgramAddressSync( + seeds, + this.programId, + ); + return address; + } /** @internal */ static get deriveCpiAuthorityPda(): PublicKey { diff --git a/js/stateless.js/src/rpc.ts b/js/stateless.js/src/rpc.ts index eda7f8a471..146fee4899 100644 --- a/js/stateless.js/src/rpc.ts +++ b/js/stateless.js/src/rpc.ts @@ -597,6 +597,23 @@ export function getTreeForQueue( return info[index].tree; } +export function deriveTokenPoolPdaWithBump( + mint: PublicKey, + bump: number, +): PublicKey { + let seeds: Buffer[] = []; + if (bump === 0) { + seeds = [Buffer.from('pool'), mint.toBuffer()]; + } else { + seeds = [Buffer.from('pool'), mint.toBuffer(), Buffer.from([bump])]; + } + const [address, _] = PublicKey.findProgramAddressSync( + seeds, + new PublicKey('cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m'), + ); + return address; +} + /** * */ @@ -623,6 +640,29 @@ export class Rpc extends Connection implements CompressionApiInterface { this.activeStateTreeInfo = info; } + async getTokenPoolInfos(mint: PublicKey): Promise { + const tokenPoolInfo0 = deriveTokenPoolPdaWithBump(mint, 0); + const tokenPoolInfo1 = deriveTokenPoolPdaWithBump(mint, 1); + const tokenPoolInfo2 = deriveTokenPoolPdaWithBump(mint, 2); + const tokenPoolInfo3 = deriveTokenPoolPdaWithBump(mint, 3); + const tokenPoolInfo4 = deriveTokenPoolPdaWithBump(mint, 4); + const tokenPoolInfo5 = deriveTokenPoolPdaWithBump(mint, 5); + + const tokenPoolInfos = await Promise.all([ + this.getTokenAccountBalance(tokenPoolInfo0), + this.getTokenAccountBalance(tokenPoolInfo1), + this.getTokenAccountBalance(tokenPoolInfo2), + this.getTokenAccountBalance(tokenPoolInfo3), + this.getTokenAccountBalance(tokenPoolInfo4), + this.getTokenAccountBalance(tokenPoolInfo5), + ]); + const infos: TokenPoolInfo[] = tokenPoolInfos.map(tokenPoolInfo => ({ + tokenPool: mint, + tokenPoolInfo: tokenPoolInfo.value.amount, + })); + return infos; + } + /** * * Get the active state tree addresses from the cluster. From 208c3e91c14c182ac7b38a661aa6c524a3d5739e Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 26 Mar 2025 02:13:42 +0000 Subject: [PATCH 04/19] wip --- js/compressed-token/src/types.ts | 35 ---------- js/compressed-token/tests/e2e/layout.test.ts | 6 +- js/stateless.js/src/constants.ts | 3 + js/stateless.js/src/rpc.ts | 69 ++++++++++++++----- .../test-rpc/get-compressed-token-accounts.ts | 11 ++- js/stateless.js/tests/e2e/layout.test.ts | 7 +- 6 files changed, 62 insertions(+), 69 deletions(-) diff --git a/js/compressed-token/src/types.ts b/js/compressed-token/src/types.ts index ef7822bcd1..91dc97ae6f 100644 --- a/js/compressed-token/src/types.ts +++ b/js/compressed-token/src/types.ts @@ -79,41 +79,6 @@ export type CompressSplTokenAccountInstructionData = { cpiContext: CompressedCpiContext | null; }; -export enum Action { - Compress = 1, - Decompress = 2, - Transfer = 3, -} - -export type TokenPoolActivity = { - signature: string; - amount: BN; - action: Action; -}; - -export type TokenPoolInfo = { - /** - * The mint of the token pool - */ - mint: PublicKey; - /** - * The token pool address - */ - tokenPoolAddress: PublicKey; - /** - * The token program of the token pool - */ - tokenProgram: PublicKey; - /** - * count of txs and volume in the past 60 seconds. - */ - activity: { - txs: number; - amountAdded: BN; - amountRemoved: BN; - }; -}; - export type StorageOptions = { /** * State tree info diff --git a/js/compressed-token/tests/e2e/layout.test.ts b/js/compressed-token/tests/e2e/layout.test.ts index f198509db2..c384b2f25c 100644 --- a/js/compressed-token/tests/e2e/layout.test.ts +++ b/js/compressed-token/tests/e2e/layout.test.ts @@ -41,11 +41,7 @@ const getTestProgram = (): Program => { }, ); setProvider(mockProvider); - return new Program( - IDL, - new PublicKey('cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m'), - mockProvider, - ); + return new Program(IDL, COMPRESSED_TOKEN_PROGRAM_ID, mockProvider); }; function deepEqual(ref: any, val: any) { diff --git a/js/stateless.js/src/constants.ts b/js/stateless.js/src/constants.ts index 19c250d927..bdf95d6538 100644 --- a/js/stateless.js/src/constants.ts +++ b/js/stateless.js/src/constants.ts @@ -145,6 +145,9 @@ export const defaultTestStateTreeAccounts2 = () => { }; }; +export const COMPRESSED_TOKEN_PROGRAM_ID = new PublicKey( + 'cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m', +); export const stateTreeLookupTableMainnet = '7i86eQs3GSqHjN47WdWLTCGMW6gde1q96G2EVnUyK2st'; export const nullifiedStateTreeLookupTableMainnet = diff --git a/js/stateless.js/src/rpc.ts b/js/stateless.js/src/rpc.ts index 146fee4899..f5f0d70f54 100644 --- a/js/stateless.js/src/rpc.ts +++ b/js/stateless.js/src/rpc.ts @@ -62,6 +62,7 @@ import { localTestActiveStateTreeInfo, isLocalTest, defaultStateTreeLookupTables, + COMPRESSED_TOKEN_PROGRAM_ID, } from './constants'; import BN from 'bn.js'; import { toCamelCase, toHex } from './utils/conversion'; @@ -609,11 +610,45 @@ export function deriveTokenPoolPdaWithBump( } const [address, _] = PublicKey.findProgramAddressSync( seeds, - new PublicKey('cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m'), + COMPRESSED_TOKEN_PROGRAM_ID, ); return address; } +export type TokenPoolActivity = { + signature: string; + amount: BN; + action: Action; +}; + +export type TokenPoolInfo = { + /** + * The mint of the token pool + */ + mint: PublicKey; + /** + * The token pool address + */ + tokenPoolAddress: PublicKey; + /** + * The token program of the token pool + */ + tokenProgram: PublicKey; + /** + * count of txs and volume in the past 60 seconds. + */ + activity?: { + txs: number; + amountAdded: BN; + amountRemoved: BN; + }; +}; +export enum Action { + Compress = 1, + Decompress = 2, + Transfer = 3, +} + /** * */ @@ -641,24 +676,22 @@ export class Rpc extends Connection implements CompressionApiInterface { } async getTokenPoolInfos(mint: PublicKey): Promise { - const tokenPoolInfo0 = deriveTokenPoolPdaWithBump(mint, 0); - const tokenPoolInfo1 = deriveTokenPoolPdaWithBump(mint, 1); - const tokenPoolInfo2 = deriveTokenPoolPdaWithBump(mint, 2); - const tokenPoolInfo3 = deriveTokenPoolPdaWithBump(mint, 3); - const tokenPoolInfo4 = deriveTokenPoolPdaWithBump(mint, 4); - const tokenPoolInfo5 = deriveTokenPoolPdaWithBump(mint, 5); - - const tokenPoolInfos = await Promise.all([ - this.getTokenAccountBalance(tokenPoolInfo0), - this.getTokenAccountBalance(tokenPoolInfo1), - this.getTokenAccountBalance(tokenPoolInfo2), - this.getTokenAccountBalance(tokenPoolInfo3), - this.getTokenAccountBalance(tokenPoolInfo4), - this.getTokenAccountBalance(tokenPoolInfo5), - ]); + const tokenPoolInfos = await Promise.all( + Array.from({ length: 6 }, (_, i) => { + const tokenPoolPda = deriveTokenPoolPdaWithBump(mint, i); + return this.getTokenAccountBalance(tokenPoolPda).then( + balance => ({ + tokenPoolPda, + balance, + }), + ); + }), + ); const infos: TokenPoolInfo[] = tokenPoolInfos.map(tokenPoolInfo => ({ - tokenPool: mint, - tokenPoolInfo: tokenPoolInfo.value.amount, + mint, + tokenPoolAddress: tokenPoolInfo.tokenPoolPda, + tokenProgram: COMPRESSED_TOKEN_PROGRAM_ID, + activity: undefined, })); return infos; } diff --git a/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts index c010d08491..f54b80077a 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts @@ -1,7 +1,10 @@ import { PublicKey } from '@solana/web3.js'; import { getParsedEvents } from './get-parsed-events'; import BN from 'bn.js'; -import { defaultTestStateTreeAccounts } from '../../constants'; +import { + COMPRESSED_TOKEN_PROGRAM_ID, + defaultTestStateTreeAccounts, +} from '../../constants'; import { Rpc } from '../../rpc'; import { ParsedTokenAccount, WithCursor } from '../../rpc-interface'; import { @@ -21,10 +24,6 @@ import { Layout, } from '@coral-xyz/borsh'; -const tokenProgramId: PublicKey = new PublicKey( - 'cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m', -); - type TokenData = { mint: PublicKey; owner: PublicKey; @@ -55,7 +54,7 @@ export type EventWithParsedTokenTlvData = { */ export function parseTokenLayoutWithIdl( compressedAccount: CompressedAccount, - programId: PublicKey = tokenProgramId, + programId: PublicKey = COMPRESSED_TOKEN_PROGRAM_ID, ): TokenData | null { if (compressedAccount.data === null) return null; diff --git a/js/stateless.js/tests/e2e/layout.test.ts b/js/stateless.js/tests/e2e/layout.test.ts index d5260e2fec..2865f662e0 100644 --- a/js/stateless.js/tests/e2e/layout.test.ts +++ b/js/stateless.js/tests/e2e/layout.test.ts @@ -17,6 +17,7 @@ import { import { PublicTransactionEvent } from '../../src/state'; import { + COMPRESSED_TOKEN_PROGRAM_ID, defaultStaticAccountsStruct, IDL, LightSystemProgramIDL, @@ -34,11 +35,7 @@ const getTestProgram = (): Program => { }, ); setProvider(mockProvider); - return new Program( - IDL, - new PublicKey('cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m'), - mockProvider, - ); + return new Program(IDL, COMPRESSED_TOKEN_PROGRAM_ID, mockProvider); }; function deepEqual(ref: any, val: any) { From 5bc29af6ffedb8b6061b97ec98fc3667a9fd6772 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 28 Mar 2025 04:17:56 +0000 Subject: [PATCH 05/19] wip --- .../src/actions/approve-and-mint-to.ts | 5 +- .../src/actions/merge-token-accounts.ts | 2 +- js/stateless.js/src/rpc-interface.ts | 56 ++++++++++++++++++ js/stateless.js/src/rpc.ts | 59 +++---------------- js/stateless.js/src/state/types.ts | 1 + .../src/test-helpers/test-rpc/test-rpc.ts | 27 +++++++++ ...e-tree-info.ts => get-state-tree-infos.ts} | 0 .../src/utils/get-token-pool-infos.ts | 41 +++++++++++++ js/stateless.js/src/utils/index.ts | 2 +- 9 files changed, 136 insertions(+), 57 deletions(-) rename js/stateless.js/src/utils/{get-light-state-tree-info.ts => get-state-tree-infos.ts} (100%) create mode 100644 js/stateless.js/src/utils/get-token-pool-infos.ts diff --git a/js/compressed-token/src/actions/approve-and-mint-to.ts b/js/compressed-token/src/actions/approve-and-mint-to.ts index a7bc973404..b0cd9a87ab 100644 --- a/js/compressed-token/src/actions/approve-and-mint-to.ts +++ b/js/compressed-token/src/actions/approve-and-mint-to.ts @@ -66,9 +66,8 @@ export async function approveAndMintTo( storageOptions.stateTreeInfo = info; } if (!storageOptions.tokenPoolInfos) { - const tokenPoolInfos = await rpc.getTokenPoolInfos(); - const info = pickTokenPoolInfos(tokenPoolInfos); - storageOptions.tokenPoolInfos = info; + const tokenPoolInfos = await rpc.getTokenPoolInfos(mint); + storageOptions.tokenPoolInfos = tokenPoolInfos; } const ixs = await CompressedTokenProgram.approveAndMintTo({ diff --git a/js/compressed-token/src/actions/merge-token-accounts.ts b/js/compressed-token/src/actions/merge-token-accounts.ts index 84212a4c27..9eef01a9c1 100644 --- a/js/compressed-token/src/actions/merge-token-accounts.ts +++ b/js/compressed-token/src/actions/merge-token-accounts.ts @@ -72,7 +72,7 @@ export async function mergeTokenAccounts( owner: owner.publicKey, mint, inputCompressedTokenAccounts: batch, - outputStateTree: merkleTree!, + outputStateTreeInfo: merkleTree!, recentValidityProof: proof.compressedProof, recentInputStateRootIndices: proof.rootIndices, }); diff --git a/js/stateless.js/src/rpc-interface.ts b/js/stateless.js/src/rpc-interface.ts index f5312d3a63..944e2b9951 100644 --- a/js/stateless.js/src/rpc-interface.ts +++ b/js/stateless.js/src/rpc-interface.ts @@ -24,6 +24,62 @@ import { TokenData, } from './state'; import BN from 'bn.js'; +import { COMPRESSED_TOKEN_PROGRAM_ID } from './constants'; + +export type TokenPoolActivity = { + signature: string; + amount: BN; + action: Action; +}; + +export function deriveTokenPoolPdaWithBump( + mint: PublicKey, + bump: number, +): PublicKey { + let seeds: Buffer[] = []; + if (bump === 0) { + seeds = [Buffer.from('pool'), mint.toBuffer()]; + } else { + seeds = [Buffer.from('pool'), mint.toBuffer(), Buffer.from([bump])]; + } + const [address, _] = PublicKey.findProgramAddressSync( + seeds, + COMPRESSED_TOKEN_PROGRAM_ID, + ); + return address; +} + +export type TokenPoolInfo = { + /** + * The mint of the token pool + */ + mint: PublicKey; + /** + * The token pool address + */ + tokenPoolAddress: PublicKey; + /** + * The token program of the token pool + */ + tokenProgram: PublicKey; + /** + * count of txs and volume in the past 60 seconds. + */ + activity?: { + txs: number; + amountAdded: BN; + amountRemoved: BN; + }; + /** + * The balance of the token pool + */ + balance: BN; +}; +export enum Action { + Compress = 1, + Decompress = 2, + Transfer = 3, +} export interface LatestNonVotingSignatures { context: { slot: number }; diff --git a/js/stateless.js/src/rpc.ts b/js/stateless.js/src/rpc.ts index f5f0d70f54..a69f7c46a2 100644 --- a/js/stateless.js/src/rpc.ts +++ b/js/stateless.js/src/rpc.ts @@ -44,6 +44,8 @@ import { TokenBalance, TokenBalanceListResultV2, PaginatedOptions, + TokenPoolInfo, + deriveTokenPoolPdaWithBump, } from './rpc-interface'; import { MerkleContextWithMerkleProof, @@ -72,7 +74,7 @@ import { negateAndCompressProof, } from './utils/parse-validity-proof'; import { LightWasm } from './test-helpers'; -import { getActiveStateTreeInfos } from './utils/get-light-state-tree-info'; +import { getActiveStateTreeInfos } from './utils/get-state-tree-infos'; import { StateTreeInfo } from './state/types'; import { validateNumbersForProof } from './utils'; @@ -598,57 +600,6 @@ export function getTreeForQueue( return info[index].tree; } -export function deriveTokenPoolPdaWithBump( - mint: PublicKey, - bump: number, -): PublicKey { - let seeds: Buffer[] = []; - if (bump === 0) { - seeds = [Buffer.from('pool'), mint.toBuffer()]; - } else { - seeds = [Buffer.from('pool'), mint.toBuffer(), Buffer.from([bump])]; - } - const [address, _] = PublicKey.findProgramAddressSync( - seeds, - COMPRESSED_TOKEN_PROGRAM_ID, - ); - return address; -} - -export type TokenPoolActivity = { - signature: string; - amount: BN; - action: Action; -}; - -export type TokenPoolInfo = { - /** - * The mint of the token pool - */ - mint: PublicKey; - /** - * The token pool address - */ - tokenPoolAddress: PublicKey; - /** - * The token program of the token pool - */ - tokenProgram: PublicKey; - /** - * count of txs and volume in the past 60 seconds. - */ - activity?: { - txs: number; - amountAdded: BN; - amountRemoved: BN; - }; -}; -export enum Action { - Compress = 1, - Decompress = 2, - Transfer = 3, -} - /** * */ @@ -678,6 +629,10 @@ export class Rpc extends Connection implements CompressionApiInterface { async getTokenPoolInfos(mint: PublicKey): Promise { const tokenPoolInfos = await Promise.all( Array.from({ length: 6 }, (_, i) => { + // TODO: + // 1. use getAccounts and parse them myself. + // 2. set initialized flag + // 3. test suite with local pools setup const tokenPoolPda = deriveTokenPoolPdaWithBump(mint, i); return this.getTokenAccountBalance(tokenPoolPda).then( balance => ({ diff --git a/js/stateless.js/src/state/types.ts b/js/stateless.js/src/state/types.ts index 9186d37554..8fc36d8af7 100644 --- a/js/stateless.js/src/state/types.ts +++ b/js/stateless.js/src/state/types.ts @@ -91,6 +91,7 @@ export interface QueueIndex { index: number; // u16 } + /** * Describe the generic compressed account details applicable to every * compressed account. diff --git a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts index f6e5fe6a3c..350407d57a 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts @@ -14,6 +14,7 @@ import { import { MerkleTree } from '../merkle-tree/merkle-tree'; import { getParsedEvents } from './get-parsed-events'; import { + COMPRESSED_TOKEN_PROGRAM_ID, defaultTestStateTreeAccounts, localTestActiveStateTreeInfo, } from '../../constants'; @@ -29,6 +30,8 @@ import { SignatureWithMetadata, WithContext, WithCursor, + deriveTokenPoolPdaWithBump, + TokenPoolInfo, } from '../../rpc-interface'; import { CompressedProofWithContext, @@ -211,6 +214,30 @@ export class TestRpc extends Connection implements CompressionApiInterface { this.activeStateTreeInfo = info; } + async getTokenPoolInfos(mint: PublicKey): Promise { + const tokenPoolInfos = await Promise.all( + Array.from({ length: 6 }, (_, i) => { + // TODO: + // 1. use getAccounts and parse them myself. + // 2. set initialized flag + // 3. test suite with local pools setup + const tokenPoolPda = deriveTokenPoolPdaWithBump(mint, i); + return this.getTokenAccountBalance(tokenPoolPda).then( + balance => ({ + tokenPoolPda, + balance, + }), + ); + }), + ); + const infos: TokenPoolInfo[] = tokenPoolInfos.map(tokenPoolInfo => ({ + mint, + tokenPoolAddress: tokenPoolInfo.tokenPoolPda, + tokenProgram: COMPRESSED_TOKEN_PROGRAM_ID, + activity: undefined, + })); + return infos; + } /** * Returns local test state trees. */ diff --git a/js/stateless.js/src/utils/get-light-state-tree-info.ts b/js/stateless.js/src/utils/get-state-tree-infos.ts similarity index 100% rename from js/stateless.js/src/utils/get-light-state-tree-info.ts rename to js/stateless.js/src/utils/get-state-tree-infos.ts diff --git a/js/stateless.js/src/utils/get-token-pool-infos.ts b/js/stateless.js/src/utils/get-token-pool-infos.ts new file mode 100644 index 0000000000..bb62fc1508 --- /dev/null +++ b/js/stateless.js/src/utils/get-token-pool-infos.ts @@ -0,0 +1,41 @@ +import { + AddressLookupTableProgram, + Connection, + Keypair, + PublicKey, + Signer, +} from '@solana/web3.js'; +import { buildAndSignTx, sendAndConfirmTx } from './send-and-confirm'; +import { dedupeSigner } from '../actions'; +import { StateTreeInfo, TreeType } from '../state/types'; +import { Rpc } from '../rpc'; +import { TokenPoolInfo } from '../rpc-interface'; +import BN from 'bn.js'; + +/** + * Get a random token pool info from the token pool infos. + * Filters out token pool infos that are not initialized. + * Filters out token pools with insufficient balance. + * Returns multiple token pool infos if multiple will be required for the required amount. + * + * @param infos The token pool infos + * @returns A random token pool info + */ +export function pickTokenPoolInfos( + infos: TokenPoolInfo[], + amount: number, +): TokenPoolInfo[] { + // Shuffle the infos array + for (let i = infos.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [infos[i], infos[j]] = [infos[j], infos[i]]; + } + + // Find the first info where balance is 10x the requested amount + const sufficientBalanceInfo = infos.find(info => + info.balance.gte(new BN(amount).mul(new BN(10))), + ); + + // If none found, return all infos + return sufficientBalanceInfo ? [sufficientBalanceInfo] : infos; +} diff --git a/js/stateless.js/src/utils/index.ts b/js/stateless.js/src/utils/index.ts index 6a5101897c..82f9316db6 100644 --- a/js/stateless.js/src/utils/index.ts +++ b/js/stateless.js/src/utils/index.ts @@ -7,4 +7,4 @@ export * from './send-and-confirm'; export * from './sleep'; export * from './validation'; export * from './calculate-compute-unit-price'; -export * from './get-light-state-tree-info'; +export * from './get-state-tree-infos'; From 89271fe2a3f002c337a137ae5ed85d0c8346c953 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 1 Apr 2025 04:25:23 +0100 Subject: [PATCH 06/19] wip - storageoptions --- .../src/actions/approve-and-mint-to.ts | 10 +- js/compressed-token/src/actions/compress.ts | 2 +- js/compressed-token/src/program.ts | 56 +++--- js/compressed-token/src/types.ts | 1 + .../src/utils/get-token-pool-infos.ts | 168 ++++++++++++++++++ js/stateless.js/src/rpc-interface.ts | 56 ------ js/stateless.js/src/rpc.ts | 28 --- .../src/test-helpers/test-rpc/test-rpc.ts | 26 --- .../src/utils/get-token-pool-infos.ts | 41 ----- 9 files changed, 213 insertions(+), 175 deletions(-) create mode 100644 js/compressed-token/src/utils/get-token-pool-infos.ts delete mode 100644 js/stateless.js/src/utils/get-token-pool-infos.ts diff --git a/js/compressed-token/src/actions/approve-and-mint-to.ts b/js/compressed-token/src/actions/approve-and-mint-to.ts index b0cd9a87ab..96cf4bf63a 100644 --- a/js/compressed-token/src/actions/approve-and-mint-to.ts +++ b/js/compressed-token/src/actions/approve-and-mint-to.ts @@ -18,6 +18,10 @@ import { import { CompressedTokenProgram } from '../program'; import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token'; import { StorageOptions } from '../types'; +import { + getTokenPoolInfos, + pickTokenPoolInfos, +} from '../utils/get-token-pool-infos'; /** * Mint compressed tokens to a solana address from an external mint authority @@ -66,8 +70,8 @@ export async function approveAndMintTo( storageOptions.stateTreeInfo = info; } if (!storageOptions.tokenPoolInfos) { - const tokenPoolInfos = await rpc.getTokenPoolInfos(mint); - storageOptions.tokenPoolInfos = tokenPoolInfos; + const tokenPoolInfos = await getTokenPoolInfos(rpc, mint); + storageOptions.tokenPoolInfos = pickTokenPoolInfos(tokenPoolInfos); } const ixs = await CompressedTokenProgram.approveAndMintTo({ @@ -78,7 +82,7 @@ export async function approveAndMintTo( amount, toPubkey: destination, outputStateTreeInfo: storageOptions.stateTreeInfo, - tokenPoolInfos: storageOptions.tokenPoolInfos, + tokenPoolInfo: toArray(storageOptions.tokenPoolInfos)[0], tokenProgramId, }); diff --git a/js/compressed-token/src/actions/compress.ts b/js/compressed-token/src/actions/compress.ts index fcb5ee39fb..d03f8dbb1e 100644 --- a/js/compressed-token/src/actions/compress.ts +++ b/js/compressed-token/src/actions/compress.ts @@ -64,7 +64,7 @@ export async function compress( toAddress, amount, mint, - outputStateTree: merkleTree, + outputStateTreeInfo: merkleTree, tokenProgramId, }); diff --git a/js/compressed-token/src/program.ts b/js/compressed-token/src/program.ts index 3fa2f7058d..27c64fbcbd 100644 --- a/js/compressed-token/src/program.ts +++ b/js/compressed-token/src/program.ts @@ -43,9 +43,12 @@ import { } from './layout'; import { CompressedTokenInstructionDataTransfer, - TokenPoolInfo, TokenTransferOutputData, } from './types'; +import { + checkTokenPoolInfo, + TokenPoolInfo, +} from './utils/get-token-pool-infos'; export type CompressParams = { /** @@ -79,9 +82,9 @@ export type CompressParams = { */ outputStateTreeInfo: StateTreeInfo; /** - * Tokenpools. One or more token pools can be provided. + * Tokenpool. */ - tokenPoolInfos: TokenPoolInfo | TokenPoolInfo[]; + tokenPoolInfo: TokenPoolInfo; /** * Optional: The token program ID. Default: SPL Token Program ID */ @@ -113,6 +116,10 @@ export type CompressSplTokenAccountParams = { * The state tree that the compressed token account should be inserted into. */ outputStateTreeInfo: StateTreeInfo; + /** + * The token pool info. + */ + tokenPoolInfo: TokenPoolInfo; /** * Optional: The token program ID. Default: SPL Token Program ID */ @@ -154,7 +161,7 @@ export type DecompressParams = { /** * Tokenpool addresses. One or more token pools can be provided. */ - tokenPoolInfos: TokenPoolInfo[]; + tokenPoolInfos: TokenPoolInfo | TokenPoolInfo[]; /** * Optional: The token program ID. Default: SPL Token Program ID */ @@ -294,14 +301,13 @@ export type MintToParams = { */ amount: BN | BN[] | number | number[]; /** - * The state tree to mint into. Defaults to a public state tree if - * unspecified. + * The state tree to mint into. */ outputStateTreeInfo: StateTreeInfo; /** * Tokenpool addresses. One or more token pools can be provided. */ - tokenPoolInfos: TokenPoolInfo[]; + tokenPoolInfo: TokenPoolInfo; /** * Optional: The token program ID. Default: SPL Token Program ID */ @@ -359,7 +365,7 @@ export type ApproveAndMintToParams = { /** * Tokenpool addresses. One or more token pools can be provided. */ - tokenPoolInfos: TokenPoolInfo[]; + tokenPoolInfo: TokenPoolInfo; /** * Optional: The token program ID. Default: SPL Token Program ID */ @@ -672,11 +678,11 @@ export class CompressedTokenProgram { toPubkey, amount, tokenProgramId, - tokenPoolInfos, + tokenPoolInfo, } = params; const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; - const tokenPoolPda = this.deriveTokenPoolPda(mint); + checkTokenPoolInfo(tokenPoolInfo, mint); const amounts = toArray(amount).map(amount => bn(amount)); @@ -694,15 +700,13 @@ export class CompressedTokenProgram { authority, cpiAuthorityPda: this.deriveCpiAuthorityPda, tokenProgram, - tokenPoolPda, + tokenPoolPda: tokenPoolInfo.tokenPoolPda, lightSystemProgram: LightSystemProgram.programId, registeredProgramPda: systemKeys.registeredProgramPda, noopProgram: systemKeys.noopProgram, accountCompressionAuthority: systemKeys.accountCompressionAuthority, accountCompressionProgram: systemKeys.accountCompressionProgram, - merkleTree: outputStateTreeInfo - ? outputStateTreeInfo.tree - : defaultTestStateTreeAccounts().merkleTree, + merkleTree: outputStateTreeInfo.tree, selfProgram: this.programId, systemProgram: SystemProgram.programId, solPoolPda: null, // TODO: add lamports support @@ -733,7 +737,7 @@ export class CompressedTokenProgram { outputStateTreeInfo, toPubkey, tokenProgramId, - tokenPoolInfos, + tokenPoolInfo, } = params; const amount: bigint = BigInt(params.amount.toString()); @@ -757,7 +761,7 @@ export class CompressedTokenProgram { mint, amount: params.amount, outputStateTreeInfo, - tokenPoolInfos, + tokenPoolInfo, tokenProgramId, }); @@ -914,7 +918,7 @@ export class CompressedTokenProgram { mint, outputStateTreeInfo, tokenProgramId, - tokenPoolInfos, + tokenPoolInfo, } = params; if (Array.isArray(params.amount) !== Array.isArray(params.toAddress)) { @@ -981,6 +985,8 @@ export class CompressedTokenProgram { const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; + checkTokenPoolInfo(tokenPoolInfo, mint); + const keys = transferAccountsLayout({ ...defaultStaticAccountsStruct(), feePayer: payer, @@ -989,7 +995,7 @@ export class CompressedTokenProgram { lightSystemProgram: LightSystemProgram.programId, selfProgram: this.programId, systemProgram: SystemProgram.programId, - tokenPoolPda: this.deriveTokenPoolPda(mint), + tokenPoolPda: tokenPoolInfo.tokenPoolPda, compressOrDecompressTokenAccount: source, tokenProgram, }); @@ -1019,6 +1025,7 @@ export class CompressedTokenProgram { tokenProgramId, } = params; const amount = bn(params.amount); + const tokenPoolInfos = toArray(params.tokenPoolInfos); const tokenTransferOutputs = createDecompressOutputState( inputCompressedTokenAccounts, @@ -1071,13 +1078,20 @@ export class CompressedTokenProgram { accountCompressionAuthority: accountCompressionAuthority, accountCompressionProgram: accountCompressionProgram, selfProgram: this.programId, - tokenPoolPda: this.deriveTokenPoolPda(mint), + tokenPoolPda: tokenPoolInfos.splice(0, 1)[0].tokenPoolPda, compressOrDecompressTokenAccount: toAddress, tokenProgram, systemProgram: SystemProgram.programId, }); keys.push(...remainingAccountMetas); + keys.push( + ...tokenPoolInfos.map(info => ({ + pubkey: info.tokenPoolPda, + isSigner: false, + isWritable: true, + })), + ); return new TransactionInstruction({ programId: this.programId, @@ -1128,10 +1142,12 @@ export class CompressedTokenProgram { mint, remainingAmount, outputStateTreeInfo, + tokenPoolInfo, tokenProgramId, } = params; const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; + checkTokenPoolInfo(tokenPoolInfo, mint); const remainingAccountMetas: AccountMeta[] = [ { pubkey: outputStateTreeInfo.tree, @@ -1161,7 +1177,7 @@ export class CompressedTokenProgram { accountCompressionAuthority: accountCompressionAuthority, accountCompressionProgram: accountCompressionProgram, selfProgram: this.programId, - tokenPoolPda: this.deriveTokenPoolPda(mint), + tokenPoolPda: tokenPoolInfo.tokenPoolPda, compressOrDecompressTokenAccount: tokenAccount, tokenProgram, systemProgram: SystemProgram.programId, diff --git a/js/compressed-token/src/types.ts b/js/compressed-token/src/types.ts index 91dc97ae6f..c33b17eb79 100644 --- a/js/compressed-token/src/types.ts +++ b/js/compressed-token/src/types.ts @@ -5,6 +5,7 @@ import { PackedMerkleContext, StateTreeInfo, } from '@lightprotocol/stateless.js'; +import { TokenPoolInfo } from './utils/get-token-pool-infos'; export type CompressedCpiContext = { setContext: boolean; diff --git a/js/compressed-token/src/utils/get-token-pool-infos.ts b/js/compressed-token/src/utils/get-token-pool-infos.ts new file mode 100644 index 0000000000..67288dd427 --- /dev/null +++ b/js/compressed-token/src/utils/get-token-pool-infos.ts @@ -0,0 +1,168 @@ +import { Commitment, PublicKey } from '@solana/web3.js'; +import { TOKEN_PROGRAM_ID, unpackAccount } from '@solana/spl-token'; +import { CompressedTokenProgram } from '../program'; +import { Rpc } from '@lightprotocol/stateless.js'; +import BN from 'bn.js'; + +/** + * Check if the token pool info is initialized and has a balance. + * @param mint The mint of the token pool + * @param tokenPoolInfo The token pool info + * @returns True if the token pool info is initialized and has a balance + */ +export function checkTokenPoolInfo( + tokenPoolInfo: TokenPoolInfo, + mint: PublicKey, +): boolean { + if (!tokenPoolInfo.mint.equals(mint)) { + throw new Error(`TokenPool mint does not match the provided mint.`); + } + + if (!tokenPoolInfo.isInitialized) { + throw new Error( + `TokenPool is not initialized. Please create a compressed token pool for mint: ${mint.toBase58()} via createTokenPool().`, + ); + } + return true; +} + +export async function getTokenPoolInfos( + rpc: Rpc, + mint: PublicKey, + commitment?: Commitment, +): Promise { + const addresses = Array.from({ length: 5 }, (_, i) => + deriveTokenPoolPdaWithBump(mint, i), + ); + const accountInfos = await rpc.getMultipleAccountsInfo( + addresses, + commitment, + ); + + if (accountInfos[0] === null) { + throw new Error( + `TokenPool not found. Please create a compressed token pool for mint: ${mint.toBase58()} via createTokenPool().`, + ); + } + + const parsedInfos = addresses.map((address, i) => + accountInfos[i] + ? unpackAccount(address, accountInfos[i], accountInfos[i].owner) + : null, + ); + + const tokenProgram = parsedInfos[0]!.owner; + + return parsedInfos.map((parsedInfo, i) => { + if (!parsedInfo) { + return { + mint, + tokenPoolPda: addresses[i], + tokenProgram, + activity: undefined, + balance: new BN(0), + isInitialized: false, + }; + } + + return { + mint, + tokenPoolPda: parsedInfo.address, + tokenProgram, + activity: undefined, + balance: new BN(parsedInfo.amount.toString()), + isInitialized: true, + }; + }); +} + +export type TokenPoolActivity = { + signature: string; + amount: BN; + action: Action; +}; + +export function deriveTokenPoolPdaWithBump( + mint: PublicKey, + bump: number, +): PublicKey { + let seeds: Buffer[] = []; + if (bump === 0) { + seeds = [Buffer.from('pool'), mint.toBuffer()]; // legacy, 1st + } else { + seeds = [Buffer.from('pool'), mint.toBuffer(), Buffer.from([bump])]; + } + const [address, _] = PublicKey.findProgramAddressSync( + seeds, + CompressedTokenProgram.programId, + ); + return address; +} + +export type TokenPoolInfo = { + /** + * The mint of the token pool + */ + mint: PublicKey; + /** + * The token pool address + */ + tokenPoolPda: PublicKey; + /** + * The token program of the token pool + */ + tokenProgram: PublicKey; + /** + * count of txs and volume in the past 60 seconds. + */ + activity?: { + txs: number; + amountAdded: BN; + amountRemoved: BN; + }; + /** + * Whether the token pool is initialized + */ + isInitialized: boolean; + /** + * The balance of the token pool + */ + balance: BN; +}; +export enum Action { + Compress = 1, + Decompress = 2, + Transfer = 3, +} +/** + * Get a random token pool info from the token pool infos. Filters out token + * pool infos that are not initialized. Filters out token pools with + * insufficient balance. Returns multiple token pool infos if multiple will be + * required for the required amount. + * + * @param infos The token pool infos + * @param decompressAmount The amount of tokens to withdraw. Only provide if + * you want to withdraw a specific amount. + * + * @returns A random token pool info + */ +export function pickTokenPoolInfos( + infos: TokenPoolInfo[], + decompressAmount?: number, +): TokenPoolInfo[] { + // Shuffle the infos array + for (let i = infos.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [infos[i], infos[j]] = [infos[j], infos[i]]; + } + + // Find the first info where balance is 10x the requested amount + const sufficientBalanceInfo = infos.find(info => + decompressAmount + ? info.balance.gte(new BN(decompressAmount).mul(new BN(10))) + : true, + ); + + // If none found, return all infos + return sufficientBalanceInfo ? [sufficientBalanceInfo] : infos; +} diff --git a/js/stateless.js/src/rpc-interface.ts b/js/stateless.js/src/rpc-interface.ts index 944e2b9951..f5312d3a63 100644 --- a/js/stateless.js/src/rpc-interface.ts +++ b/js/stateless.js/src/rpc-interface.ts @@ -24,62 +24,6 @@ import { TokenData, } from './state'; import BN from 'bn.js'; -import { COMPRESSED_TOKEN_PROGRAM_ID } from './constants'; - -export type TokenPoolActivity = { - signature: string; - amount: BN; - action: Action; -}; - -export function deriveTokenPoolPdaWithBump( - mint: PublicKey, - bump: number, -): PublicKey { - let seeds: Buffer[] = []; - if (bump === 0) { - seeds = [Buffer.from('pool'), mint.toBuffer()]; - } else { - seeds = [Buffer.from('pool'), mint.toBuffer(), Buffer.from([bump])]; - } - const [address, _] = PublicKey.findProgramAddressSync( - seeds, - COMPRESSED_TOKEN_PROGRAM_ID, - ); - return address; -} - -export type TokenPoolInfo = { - /** - * The mint of the token pool - */ - mint: PublicKey; - /** - * The token pool address - */ - tokenPoolAddress: PublicKey; - /** - * The token program of the token pool - */ - tokenProgram: PublicKey; - /** - * count of txs and volume in the past 60 seconds. - */ - activity?: { - txs: number; - amountAdded: BN; - amountRemoved: BN; - }; - /** - * The balance of the token pool - */ - balance: BN; -}; -export enum Action { - Compress = 1, - Decompress = 2, - Transfer = 3, -} export interface LatestNonVotingSignatures { context: { slot: number }; diff --git a/js/stateless.js/src/rpc.ts b/js/stateless.js/src/rpc.ts index a69f7c46a2..3f583377d1 100644 --- a/js/stateless.js/src/rpc.ts +++ b/js/stateless.js/src/rpc.ts @@ -44,8 +44,6 @@ import { TokenBalance, TokenBalanceListResultV2, PaginatedOptions, - TokenPoolInfo, - deriveTokenPoolPdaWithBump, } from './rpc-interface'; import { MerkleContextWithMerkleProof, @@ -64,7 +62,6 @@ import { localTestActiveStateTreeInfo, isLocalTest, defaultStateTreeLookupTables, - COMPRESSED_TOKEN_PROGRAM_ID, } from './constants'; import BN from 'bn.js'; import { toCamelCase, toHex } from './utils/conversion'; @@ -626,31 +623,6 @@ export class Rpc extends Connection implements CompressionApiInterface { this.activeStateTreeInfo = info; } - async getTokenPoolInfos(mint: PublicKey): Promise { - const tokenPoolInfos = await Promise.all( - Array.from({ length: 6 }, (_, i) => { - // TODO: - // 1. use getAccounts and parse them myself. - // 2. set initialized flag - // 3. test suite with local pools setup - const tokenPoolPda = deriveTokenPoolPdaWithBump(mint, i); - return this.getTokenAccountBalance(tokenPoolPda).then( - balance => ({ - tokenPoolPda, - balance, - }), - ); - }), - ); - const infos: TokenPoolInfo[] = tokenPoolInfos.map(tokenPoolInfo => ({ - mint, - tokenPoolAddress: tokenPoolInfo.tokenPoolPda, - tokenProgram: COMPRESSED_TOKEN_PROGRAM_ID, - activity: undefined, - })); - return infos; - } - /** * * Get the active state tree addresses from the cluster. diff --git a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts index 350407d57a..faf6988a4b 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts @@ -30,8 +30,6 @@ import { SignatureWithMetadata, WithContext, WithCursor, - deriveTokenPoolPdaWithBump, - TokenPoolInfo, } from '../../rpc-interface'; import { CompressedProofWithContext, @@ -214,30 +212,6 @@ export class TestRpc extends Connection implements CompressionApiInterface { this.activeStateTreeInfo = info; } - async getTokenPoolInfos(mint: PublicKey): Promise { - const tokenPoolInfos = await Promise.all( - Array.from({ length: 6 }, (_, i) => { - // TODO: - // 1. use getAccounts and parse them myself. - // 2. set initialized flag - // 3. test suite with local pools setup - const tokenPoolPda = deriveTokenPoolPdaWithBump(mint, i); - return this.getTokenAccountBalance(tokenPoolPda).then( - balance => ({ - tokenPoolPda, - balance, - }), - ); - }), - ); - const infos: TokenPoolInfo[] = tokenPoolInfos.map(tokenPoolInfo => ({ - mint, - tokenPoolAddress: tokenPoolInfo.tokenPoolPda, - tokenProgram: COMPRESSED_TOKEN_PROGRAM_ID, - activity: undefined, - })); - return infos; - } /** * Returns local test state trees. */ diff --git a/js/stateless.js/src/utils/get-token-pool-infos.ts b/js/stateless.js/src/utils/get-token-pool-infos.ts deleted file mode 100644 index bb62fc1508..0000000000 --- a/js/stateless.js/src/utils/get-token-pool-infos.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - AddressLookupTableProgram, - Connection, - Keypair, - PublicKey, - Signer, -} from '@solana/web3.js'; -import { buildAndSignTx, sendAndConfirmTx } from './send-and-confirm'; -import { dedupeSigner } from '../actions'; -import { StateTreeInfo, TreeType } from '../state/types'; -import { Rpc } from '../rpc'; -import { TokenPoolInfo } from '../rpc-interface'; -import BN from 'bn.js'; - -/** - * Get a random token pool info from the token pool infos. - * Filters out token pool infos that are not initialized. - * Filters out token pools with insufficient balance. - * Returns multiple token pool infos if multiple will be required for the required amount. - * - * @param infos The token pool infos - * @returns A random token pool info - */ -export function pickTokenPoolInfos( - infos: TokenPoolInfo[], - amount: number, -): TokenPoolInfo[] { - // Shuffle the infos array - for (let i = infos.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [infos[i], infos[j]] = [infos[j], infos[i]]; - } - - // Find the first info where balance is 10x the requested amount - const sufficientBalanceInfo = infos.find(info => - info.balance.gte(new BN(amount).mul(new BN(10))), - ); - - // If none found, return all infos - return sufficientBalanceInfo ? [sufficientBalanceInfo] : infos; -} From 1e3d3bd2dbd5c69209e1804506251141da82a7c4 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Thu, 3 Apr 2025 16:00:13 +0100 Subject: [PATCH 07/19] wip --- .../src/actions/approve-and-mint-to.ts | 49 ++++++++++++++----- js/compressed-token/src/actions/decompress.ts | 18 +++++-- js/compressed-token/src/types.ts | 16 +++--- .../src/utils/get-token-pool-infos.ts | 47 +++++++++++------- js/stateless.js/src/actions/compress.ts | 8 ++- js/stateless.js/src/actions/create-account.ts | 6 +-- js/stateless.js/src/actions/decompress.ts | 8 ++- js/stateless.js/src/actions/transfer.ts | 8 ++- js/stateless.js/src/state/types.ts | 5 +- .../src/utils/get-state-tree-infos.ts | 4 +- 10 files changed, 115 insertions(+), 54 deletions(-) diff --git a/js/compressed-token/src/actions/approve-and-mint-to.ts b/js/compressed-token/src/actions/approve-and-mint-to.ts index 96cf4bf63a..a38cde2d8d 100644 --- a/js/compressed-token/src/actions/approve-and-mint-to.ts +++ b/js/compressed-token/src/actions/approve-and-mint-to.ts @@ -13,16 +13,39 @@ import { dedupeSigner, pickRandomTreeAndQueue, StateTreeInfo, - pickStateTreeInfo, + selectStateTreeInfo, + toArray, } from '@lightprotocol/stateless.js'; import { CompressedTokenProgram } from '../program'; import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token'; -import { StorageOptions } from '../types'; +import { isSingleTokenPoolInfo, StorageOptions } from '../types'; import { getTokenPoolInfos, - pickTokenPoolInfos, + selectTokenPoolInfo, + selectTokenPoolInfosForDecompression, + TokenPoolInfo, } from '../utils/get-token-pool-infos'; +async function getStorageOptions( + rpc: Rpc, + mint: PublicKey, + decompressAmount?: number | BN, +): Promise { + const res = await Promise.all([ + rpc.getCachedActiveStateTreeInfos(), + getTokenPoolInfos(rpc, mint), + ]); + + return { + stateTreeInfo: selectStateTreeInfo(res[0]), + tokenPoolInfos: decompressAmount + ? selectTokenPoolInfosForDecompression(res[1], decompressAmount) + : selectTokenPoolInfo(res[1]), + }; +} + + + /** * Mint compressed tokens to a solana address from an external mint authority * @@ -63,15 +86,17 @@ export async function approveAndMintTo( tokenProgramId, ); - if (!storageOptions) storageOptions = {}; - if (!storageOptions.stateTreeInfo) { + let selectedStateTreeInfo: StateTreeInfo; + let selectedTokenPoolInfo: TokenPoolInfo; + if (!storageOptions) { const stateTreeInfos = await rpc.getCachedActiveStateTreeInfos(); - const info = pickStateTreeInfo(stateTreeInfos); - storageOptions.stateTreeInfo = info; - } - if (!storageOptions.tokenPoolInfos) { + selectedStateTreeInfo = selectStateTreeInfo(stateTreeInfos); + const tokenPoolInfos = await getTokenPoolInfos(rpc, mint); - storageOptions.tokenPoolInfos = pickTokenPoolInfos(tokenPoolInfos); + selectedTokenPoolInfo = selectTokenPoolInfos(tokenPoolInfos); + } else { + selectedStateTreeInfo = storageOptions.stateTreeInfo; + selectedTokenPoolInfo = toArray(storageOptions.tokenPoolInfos)[0]; } const ixs = await CompressedTokenProgram.approveAndMintTo({ @@ -81,8 +106,8 @@ export async function approveAndMintTo( authorityTokenAccount: authorityTokenAccount.address, amount, toPubkey: destination, - outputStateTreeInfo: storageOptions.stateTreeInfo, - tokenPoolInfo: toArray(storageOptions.tokenPoolInfos)[0], + outputStateTreeInfo: selectedStateTreeInfo, + tokenPoolInfo: selectedTokenPoolInfo, tokenProgramId, }); diff --git a/js/compressed-token/src/actions/decompress.ts b/js/compressed-token/src/actions/decompress.ts index d0ebf3aed5..1ef8cebcd0 100644 --- a/js/compressed-token/src/actions/decompress.ts +++ b/js/compressed-token/src/actions/decompress.ts @@ -11,12 +11,15 @@ import { buildAndSignTx, Rpc, dedupeSigner, + selectStateTreeInfo, } from '@lightprotocol/stateless.js'; import BN from 'bn.js'; import { CompressedTokenProgram } from '../program'; import { selectMinCompressedTokenAccountsForTransfer } from '../utils'; +import { selectTokenPoolInfosForDecompression } from '../utils/get-token-pool-infos'; +import { getTokenPoolInfos } from '../utils/get-token-pool-infos'; /** * Decompress compressed tokens @@ -60,7 +63,6 @@ export async function decompress( }, ); - /// TODO: consider using a different selection algorithm const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer( compressedTokenAccounts.items, amount, @@ -70,12 +72,22 @@ export async function decompress( inputAccounts.map(account => bn(account.compressedAccount.hash)), ); + const stateTreeInfos = await rpc.getCachedActiveStateTreeInfos(); + const selectedStateTreeInfo = selectStateTreeInfo(stateTreeInfos); + + const tokenPoolInfos = await getTokenPoolInfos(rpc, mint); + const selectedTokenPoolInfos = selectTokenPoolInfosForDecompression( + tokenPoolInfos, + amount, + ); + const ix = await CompressedTokenProgram.decompress({ payer: payer.publicKey, inputCompressedTokenAccounts: inputAccounts, - toAddress, // TODO: add explicit check that it is a token account + toAddress, amount, - outputStateTree: merkleTree, + outputStateTreeInfo: selectedStateTreeInfo, + tokenPoolInfos: selectedTokenPoolInfos, recentInputStateRootIndices: proof.rootIndices, recentValidityProof: proof.compressedProof, tokenProgramId, diff --git a/js/compressed-token/src/types.ts b/js/compressed-token/src/types.ts index c33b17eb79..ca9e3e9c65 100644 --- a/js/compressed-token/src/types.ts +++ b/js/compressed-token/src/types.ts @@ -81,16 +81,16 @@ export type CompressSplTokenAccountInstructionData = { }; export type StorageOptions = { - /** - * State tree info - */ - stateTreeInfo?: StateTreeInfo; - /** - * Whether to store the token pool info in the state tree - */ - tokenPoolInfos?: TokenPoolInfo | TokenPoolInfo[]; + stateTreeInfo: StateTreeInfo; + tokenPoolInfos: TokenPoolInfo | TokenPoolInfo[]; }; +export function isSingleTokenPoolInfo( + tokenPoolInfos: TokenPoolInfo | TokenPoolInfo[], +): tokenPoolInfos is TokenPoolInfo { + return !Array.isArray(tokenPoolInfos); +} + export type CompressedTokenInstructionDataTransfer = { /** * Validity proof diff --git a/js/compressed-token/src/utils/get-token-pool-infos.ts b/js/compressed-token/src/utils/get-token-pool-infos.ts index 67288dd427..c930efcca8 100644 --- a/js/compressed-token/src/utils/get-token-pool-infos.ts +++ b/js/compressed-token/src/utils/get-token-pool-infos.ts @@ -134,33 +134,46 @@ export enum Action { Decompress = 2, Transfer = 3, } + +const shuffleArray = (array: T[]): T[] => { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +}; + /** - * Get a random token pool info from the token pool infos. Filters out token - * pool infos that are not initialized. Filters out token pools with - * insufficient balance. Returns multiple token pool infos if multiple will be - * required for the required amount. + * Select a random token pool info from the token pool infos. + * + * @param infos The token pool infos + * + * @returns A random token pool info + */ +export function selectTokenPoolInfo(infos: TokenPoolInfo[]): TokenPoolInfo { + infos = shuffleArray(infos); + + // Return a single random token pool info + return infos[0]; +} + +/** + * Select one or multiple token pool infos from the token pool infos. * * @param infos The token pool infos * @param decompressAmount The amount of tokens to withdraw. Only provide if * you want to withdraw a specific amount. * - * @returns A random token pool info + * @returns One or multiple token pool infos */ -export function pickTokenPoolInfos( +export function selectTokenPoolInfosForDecompression( infos: TokenPoolInfo[], - decompressAmount?: number, -): TokenPoolInfo[] { - // Shuffle the infos array - for (let i = infos.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [infos[i], infos[j]] = [infos[j], infos[i]]; - } - + decompressAmount: number | BN, +): TokenPoolInfo | TokenPoolInfo[] { + infos = shuffleArray(infos); // Find the first info where balance is 10x the requested amount const sufficientBalanceInfo = infos.find(info => - decompressAmount - ? info.balance.gte(new BN(decompressAmount).mul(new BN(10))) - : true, + info.balance.gte(new BN(decompressAmount).mul(new BN(10))), ); // If none found, return all infos diff --git a/js/stateless.js/src/actions/compress.ts b/js/stateless.js/src/actions/compress.ts index 6ed7e414c9..bda66cd073 100644 --- a/js/stateless.js/src/actions/compress.ts +++ b/js/stateless.js/src/actions/compress.ts @@ -8,7 +8,11 @@ import { import { LightSystemProgram } from '../programs'; import { Rpc } from '../rpc'; -import { buildAndSignTx, pickStateTreeInfo, sendAndConfirmTx } from '../utils'; +import { + buildAndSignTx, + selectStateTreeInfo, + sendAndConfirmTx, +} from '../utils'; import BN from 'bn.js'; import { StateTreeInfo } from '../state'; @@ -38,7 +42,7 @@ export async function compress( if (!outputStateTreeInfo) { const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); - outputStateTreeInfo = pickStateTreeInfo(stateTreeInfo); + outputStateTreeInfo = selectStateTreeInfo(stateTreeInfo); } const ix = await LightSystemProgram.compress({ diff --git a/js/stateless.js/src/actions/create-account.ts b/js/stateless.js/src/actions/create-account.ts index 138d35d538..a856103cb2 100644 --- a/js/stateless.js/src/actions/create-account.ts +++ b/js/stateless.js/src/actions/create-account.ts @@ -15,7 +15,7 @@ import { buildAndSignTx, deriveAddress, deriveAddressSeed, - pickStateTreeInfo, + selectStateTreeInfo, sendAndConfirmTx, } from '../utils'; import { defaultTestStateTreeAccounts } from '../constants'; @@ -59,7 +59,7 @@ export async function createAccount( if (!outputStateTreeInfo) { const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); - outputStateTreeInfo = pickStateTreeInfo(stateTreeInfo); + outputStateTreeInfo = selectStateTreeInfo(stateTreeInfo); } const proof = await rpc.getValidityProofV0(undefined, [ @@ -141,7 +141,7 @@ export async function createAccountWithLamports( if (!outputStateTreeInfo) { const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); - outputStateTreeInfo = pickStateTreeInfo(stateTreeInfo); + outputStateTreeInfo = selectStateTreeInfo(stateTreeInfo); } const { blockhash } = await rpc.getLatestBlockhash(); diff --git a/js/stateless.js/src/actions/decompress.ts b/js/stateless.js/src/actions/decompress.ts index a344f80f72..3e5ffb2ab4 100644 --- a/js/stateless.js/src/actions/decompress.ts +++ b/js/stateless.js/src/actions/decompress.ts @@ -7,7 +7,11 @@ import { } from '@solana/web3.js'; import { LightSystemProgram, sumUpLamports } from '../programs'; import { Rpc } from '../rpc'; -import { buildAndSignTx, pickStateTreeInfo, sendAndConfirmTx } from '../utils'; +import { + buildAndSignTx, + selectStateTreeInfo, + sendAndConfirmTx, +} from '../utils'; import BN from 'bn.js'; import { CompressedAccountWithMerkleContext, @@ -38,7 +42,7 @@ export async function decompress( ): Promise { if (!outputStateTreeInfo) { const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); - outputStateTreeInfo = pickStateTreeInfo(stateTreeInfo); + outputStateTreeInfo = selectStateTreeInfo(stateTreeInfo); } const userCompressedAccountsWithMerkleContext: CompressedAccountWithMerkleContext[] = (await rpc.getCompressedAccountsByOwner(payer.publicKey)).items; diff --git a/js/stateless.js/src/actions/transfer.ts b/js/stateless.js/src/actions/transfer.ts index 2eea015ad5..1a2f14ca7a 100644 --- a/js/stateless.js/src/actions/transfer.ts +++ b/js/stateless.js/src/actions/transfer.ts @@ -18,7 +18,11 @@ import { CompressedAccountWithMerkleContext, StateTreeInfo, } from '../state'; -import { buildAndSignTx, pickStateTreeInfo, sendAndConfirmTx } from '../utils'; +import { + buildAndSignTx, + selectStateTreeInfo, + sendAndConfirmTx, +} from '../utils'; import { GetCompressedAccountsByOwnerConfig } from '../rpc-interface'; /** @@ -53,7 +57,7 @@ export async function transfer( if (!outputStateTreeInfo) { const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); - outputStateTreeInfo = pickStateTreeInfo(stateTreeInfo); + outputStateTreeInfo = selectStateTreeInfo(stateTreeInfo); } while (accumulatedLamports.lt(lamports)) { diff --git a/js/stateless.js/src/state/types.ts b/js/stateless.js/src/state/types.ts index 8fc36d8af7..828b3235db 100644 --- a/js/stateless.js/src/state/types.ts +++ b/js/stateless.js/src/state/types.ts @@ -39,13 +39,13 @@ export enum TreeType { * * Onchain Accounts are subject to Solana's write-lock limits. * - * To load balance transactions, use {@link pickStateTreeInfo} to + * To load balance transactions, use {@link selectStateTreeInfo} to * select a random tree from active Trees. * * Example: * ```typescript * const infos = await getCachedActiveStateTreeInfos(); - * const info = pickStateTreeInfo(infos); + * const info = selectStateTreeInfo(infos); * const ix = CompressedTokenProgram.compress({ * ... // other params * outputStateTree: info @@ -91,7 +91,6 @@ export interface QueueIndex { index: number; // u16 } - /** * Describe the generic compressed account details applicable to every * compressed account. diff --git a/js/stateless.js/src/utils/get-state-tree-infos.ts b/js/stateless.js/src/utils/get-state-tree-infos.ts index 8f16fb2d4d..7167de3771 100644 --- a/js/stateless.js/src/utils/get-state-tree-infos.ts +++ b/js/stateless.js/src/utils/get-state-tree-infos.ts @@ -11,7 +11,7 @@ import { StateTreeInfo, TreeType } from '../state/types'; import { Rpc } from '../rpc'; /** - * @deprecated use {@link pickStateTreeInfo} instead. Get a random tree and + * @deprecated use {@link selectStateTreeInfo} instead. Get a random tree and * queue from the active state tree addresses. * * Prevents write lock contention on state trees. @@ -44,7 +44,7 @@ export function pickRandomTreeAndQueue(info: StateTreeInfo[]): { * @param treeType The type of tree. Defaults to TreeType.StateV2 * @returns A random tree and queue */ -export function pickStateTreeInfo( +export function selectStateTreeInfo( info: StateTreeInfo[], treeType: TreeType = TreeType.StateV1, ): StateTreeInfo { From 6420a47d6575e2b742e7d3d61591a09e6b988196 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sat, 5 Apr 2025 08:02:33 +0100 Subject: [PATCH 08/19] wip --- cli/src/commands/compress-spl/index.ts | 1 + cli/src/commands/decompress-spl/index.ts | 1 + examples/browser/nextjs/src/app/page.tsx | 11 ++-- .../src/actions/approve-and-mint-to.ts | 65 ++++++++----------- .../src/actions/compress-spl-token-account.ts | 48 +++++++++----- js/compressed-token/src/actions/compress.ts | 42 +++++++----- .../src/actions/create-mint.ts | 23 +++---- js/compressed-token/src/actions/decompress.ts | 44 ++++++++----- .../src/actions/merge-token-accounts.ts | 38 +++++------ js/compressed-token/src/actions/mint-to.ts | 35 ++++++---- js/compressed-token/src/actions/transfer.ts | 36 +++++----- js/stateless.js/src/actions/compress.ts | 2 + js/stateless.js/src/state/types.ts | 2 +- js/stateless.js/tests/e2e/compress.test.ts | 36 +++++----- js/stateless.js/tests/e2e/rpc-interop.test.ts | 30 +-------- .../tests/e2e/rpc-multi-trees.test.ts | 50 +++++++------- js/stateless.js/tests/e2e/test-rpc.test.ts | 21 ++---- js/stateless.js/tests/e2e/testnet.test.ts | 2 +- js/stateless.js/tests/e2e/transfer.test.ts | 2 +- 19 files changed, 239 insertions(+), 250 deletions(-) diff --git a/cli/src/commands/compress-spl/index.ts b/cli/src/commands/compress-spl/index.ts index f1c4c6d6d6..ee1920ad91 100644 --- a/cli/src/commands/compress-spl/index.ts +++ b/cli/src/commands/compress-spl/index.ts @@ -75,6 +75,7 @@ class CompressSplCommand extends Command { toPublicKey, undefined, undefined, + undefined, tokenProgramId, ); diff --git a/cli/src/commands/decompress-spl/index.ts b/cli/src/commands/decompress-spl/index.ts index 0b61c53f88..36d532962e 100644 --- a/cli/src/commands/decompress-spl/index.ts +++ b/cli/src/commands/decompress-spl/index.ts @@ -77,6 +77,7 @@ class DecompressSplCommand extends Command { recipientAta.address, undefined, undefined, + undefined, tokenProgramId, ); diff --git a/examples/browser/nextjs/src/app/page.tsx b/examples/browser/nextjs/src/app/page.tsx index 570afe8168..b3da441875 100644 --- a/examples/browser/nextjs/src/app/page.tsx +++ b/examples/browser/nextjs/src/app/page.tsx @@ -24,9 +24,9 @@ import { bn, buildTx, confirmTx, - defaultTestStateTreeAccounts, selectMinCompressedSolAccountsForTransfer, createRpc, + selectStateTreeInfo, } from '@lightprotocol/stateless.js'; // Default styles that can be overridden by your app @@ -36,7 +36,10 @@ const SendButton: FC = () => { const { publicKey, sendTransaction } = useWallet(); const onClick = useCallback(async () => { - const connection = await createRpc(); + const connection = createRpc(); + const stateTreeInfo = selectStateTreeInfo( + await connection.getCachedActiveStateTreeInfos(), + ); if (!publicKey) throw new WalletNotConnectedError(); @@ -51,7 +54,7 @@ const SendButton: FC = () => { payer: publicKey, toAddress: publicKey, lamports: 1e8, - outputStateTree: defaultTestStateTreeAccounts().merkleTree, + outputStateTreeInfo: stateTreeInfo, }); const compressInstructions = [ ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), @@ -109,7 +112,7 @@ const SendButton: FC = () => { toAddress: recipient, lamports: 1e7, inputCompressedAccounts: selectedAccounts, - outputStateTrees: [defaultTestStateTreeAccounts().merkleTree], + outputStateTreeInfo: stateTreeInfo, recentValidityProof: compressedProof, recentInputStateRootIndices: rootIndices, }); diff --git a/js/compressed-token/src/actions/approve-and-mint-to.ts b/js/compressed-token/src/actions/approve-and-mint-to.ts index a38cde2d8d..24d405c522 100644 --- a/js/compressed-token/src/actions/approve-and-mint-to.ts +++ b/js/compressed-token/src/actions/approve-and-mint-to.ts @@ -44,19 +44,19 @@ async function getStorageOptions( }; } - - /** * Mint compressed tokens to a solana address from an external mint authority * - * @param rpc Rpc to use - * @param payer Payer of the transaction fees - * @param mint Mint for the account - * @param destination Address of the account to mint to - * @param authority Minting authority - * @param amount Amount to mint - * @param storageOptions Options for storing the tokens accounts - * @param confirmOptions Options for confirming the transaction + * @param rpc Rpc to use + * @param payer Payer of the transaction fees + * @param mint Mint for the account + * @param toPubkey Address of the account to mint to + * @param authority Minting authority + * @param amount Amount to mint + * @param outputStateTreeInfo State tree info + * @param tokenPoolInfo Token pool info + * @param confirmOptions Options for confirming the transaction + * @param tokenProgramId Token program id * * @return Signature of the confirmed transaction */ @@ -64,16 +64,23 @@ export async function approveAndMintTo( rpc: Rpc, payer: Signer, mint: PublicKey, - destination: PublicKey, + toPubkey: PublicKey, authority: Signer, amount: number | BN, - storageOptions?: StorageOptions, + outputStateTreeInfo?: StateTreeInfo, + tokenPoolInfo?: TokenPoolInfo, confirmOptions?: ConfirmOptions, tokenProgramId?: PublicKey, ): Promise { - tokenProgramId = tokenProgramId - ? tokenProgramId - : await CompressedTokenProgram.get_mint_program_id(mint, rpc); + tokenProgramId = + tokenProgramId ?? + (await CompressedTokenProgram.get_mint_program_id(mint, rpc)); + outputStateTreeInfo = + outputStateTreeInfo ?? + selectStateTreeInfo(await rpc.getCachedActiveStateTreeInfos()); + tokenPoolInfo = + tokenPoolInfo ?? + selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); const authorityTokenAccount = await getOrCreateAssociatedTokenAccount( rpc, @@ -86,28 +93,15 @@ export async function approveAndMintTo( tokenProgramId, ); - let selectedStateTreeInfo: StateTreeInfo; - let selectedTokenPoolInfo: TokenPoolInfo; - if (!storageOptions) { - const stateTreeInfos = await rpc.getCachedActiveStateTreeInfos(); - selectedStateTreeInfo = selectStateTreeInfo(stateTreeInfos); - - const tokenPoolInfos = await getTokenPoolInfos(rpc, mint); - selectedTokenPoolInfo = selectTokenPoolInfos(tokenPoolInfos); - } else { - selectedStateTreeInfo = storageOptions.stateTreeInfo; - selectedTokenPoolInfo = toArray(storageOptions.tokenPoolInfos)[0]; - } - const ixs = await CompressedTokenProgram.approveAndMintTo({ feePayer: payer.publicKey, mint, authority: authority.publicKey, authorityTokenAccount: authorityTokenAccount.address, amount, - toPubkey: destination, - outputStateTreeInfo: selectedStateTreeInfo, - tokenPoolInfo: selectedTokenPoolInfo, + toPubkey, + outputStateTreeInfo, + tokenPoolInfo, tokenProgramId, }); @@ -115,16 +109,11 @@ export async function approveAndMintTo( const additionalSigners = dedupeSigner(payer, [authority]); const tx = buildAndSignTx( - [ - ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), - ...ixs, - ], + [ComputeBudgetProgram.setComputeUnitLimit({ units: 600_000 }), ...ixs], payer, blockhash, additionalSigners, ); - const txId = await sendAndConfirmTx(rpc, tx, confirmOptions); - - return txId; + return await sendAndConfirmTx(rpc, tx, confirmOptions); } diff --git a/js/compressed-token/src/actions/compress-spl-token-account.ts b/js/compressed-token/src/actions/compress-spl-token-account.ts index b0713cf924..4905f6b864 100644 --- a/js/compressed-token/src/actions/compress-spl-token-account.ts +++ b/js/compressed-token/src/actions/compress-spl-token-account.ts @@ -10,10 +10,17 @@ import { buildAndSignTx, Rpc, dedupeSigner, + StateTreeInfo, + selectStateTreeInfo, } from '@lightprotocol/stateless.js'; import BN from 'bn.js'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + TokenPoolInfo, +} from '../utils/get-token-pool-infos'; import { CompressedTokenProgram } from '../program'; /** @@ -23,10 +30,15 @@ import { CompressedTokenProgram } from '../program'; * @param payer Payer of the transaction fees * @param mint Mint of the token to compress * @param owner Owner of the token account - * @param tokenAccount Token account to compress - * @param outputStateTree State tree to insert the compressed token account into - * @param remainingAmount Optional: amount to leave in token account. Default: 0 - * @param confirmOptions Options for confirming the transaction + * @param tokenAccount Token account to compress + * @param remainingAmount Optional: amount to leave in token account. + * Default: 0 + * @param outputStateTreeInfo State tree to insert the compressed token + * account into + * @param tokenPoolInfo Token pool info + * @param confirmOptions Options for confirming the transaction + * @param tokenProgramId Optional: token program id. Default: SPL Token + * Program ID * * @return Signature of the confirmed transaction */ @@ -36,14 +48,21 @@ export async function compressSplTokenAccount( mint: PublicKey, owner: Signer, tokenAccount: PublicKey, - outputStateTree: PublicKey, remainingAmount?: BN, + outputStateTreeInfo?: StateTreeInfo, + tokenPoolInfo?: TokenPoolInfo, confirmOptions?: ConfirmOptions, tokenProgramId?: PublicKey, ): Promise { - tokenProgramId = tokenProgramId - ? tokenProgramId - : await CompressedTokenProgram.get_mint_program_id(mint, rpc); + tokenProgramId = + tokenProgramId ?? + (await CompressedTokenProgram.get_mint_program_id(mint, rpc)); + outputStateTreeInfo = + outputStateTreeInfo ?? + selectStateTreeInfo(await rpc.getCachedActiveStateTreeInfos()); + tokenPoolInfo = + tokenPoolInfo ?? + selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); const compressIx = await CompressedTokenProgram.compressSplTokenAccount({ feePayer: payer.publicKey, @@ -51,12 +70,14 @@ export async function compressSplTokenAccount( tokenAccount, mint, remainingAmount, - outputStateTree, + outputStateTreeInfo, + tokenPoolInfo, tokenProgramId, }); const blockhashCtx = await rpc.getLatestBlockhash(); const additionalSigners = dedupeSigner(payer, [owner]); + const signedTx = buildAndSignTx( [ ComputeBudgetProgram.setComputeUnitLimit({ @@ -68,11 +89,6 @@ export async function compressSplTokenAccount( blockhashCtx.blockhash, additionalSigners, ); - const txId = await sendAndConfirmTx( - rpc, - signedTx, - confirmOptions, - blockhashCtx, - ); - return txId; + + return await sendAndConfirmTx(rpc, signedTx, confirmOptions, blockhashCtx); } diff --git a/js/compressed-token/src/actions/compress.ts b/js/compressed-token/src/actions/compress.ts index d03f8dbb1e..c063f72625 100644 --- a/js/compressed-token/src/actions/compress.ts +++ b/js/compressed-token/src/actions/compress.ts @@ -11,11 +11,18 @@ import { Rpc, dedupeSigner, pickRandomTreeAndQueue, + StateTreeInfo, + selectStateTreeInfo, } from '@lightprotocol/stateless.js'; import BN from 'bn.js'; import { CompressedTokenProgram } from '../program'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + TokenPoolInfo, +} from '../utils/get-token-pool-infos'; /** * Compress SPL tokens @@ -27,11 +34,13 @@ import { CompressedTokenProgram } from '../program'; * @param owner Owner of the compressed tokens. * @param sourceTokenAccount Source (associated) token account * @param toAddress Destination address of the recipient - * @param merkleTree State tree account that the compressed tokens + * @param outputStateTreeInfo State tree account that the compressed tokens * should be inserted into. Defaults to a default * state tree account. + * @param tokenPoolInfo Token pool info * @param confirmOptions Options for confirming the transaction - * + * @param tokenProgramId Optional: token program id. Default: SPL Token + * Program ID * * @return Signature of the confirmed transaction */ @@ -43,19 +52,20 @@ export async function compress( owner: Signer, sourceTokenAccount: PublicKey, toAddress: PublicKey | Array, - merkleTree?: PublicKey, + outputStateTreeInfo?: StateTreeInfo, + tokenPoolInfo?: TokenPoolInfo, confirmOptions?: ConfirmOptions, tokenProgramId?: PublicKey, ): Promise { tokenProgramId = tokenProgramId ? tokenProgramId : await CompressedTokenProgram.get_mint_program_id(mint, rpc); - - if (!merkleTree) { - const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); - const { tree } = pickRandomTreeAndQueue(stateTreeInfo); - merkleTree = tree; - } + outputStateTreeInfo = + outputStateTreeInfo ?? + selectStateTreeInfo(await rpc.getCachedActiveStateTreeInfos()); + tokenPoolInfo = + tokenPoolInfo ?? + selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); const compressIx = await CompressedTokenProgram.compress({ payer: payer.publicKey, @@ -64,7 +74,8 @@ export async function compress( toAddress, amount, mint, - outputStateTreeInfo: merkleTree, + outputStateTreeInfo, + tokenPoolInfo, tokenProgramId, }); @@ -73,7 +84,7 @@ export async function compress( const signedTx = buildAndSignTx( [ ComputeBudgetProgram.setComputeUnitLimit({ - units: 1_000_000, + units: 600_000, }), compressIx, ], @@ -81,11 +92,6 @@ export async function compress( blockhashCtx.blockhash, additionalSigners, ); - const txId = await sendAndConfirmTx( - rpc, - signedTx, - confirmOptions, - blockhashCtx, - ); - return txId; + + return await sendAndConfirmTx(rpc, signedTx, confirmOptions, blockhashCtx); } diff --git a/js/compressed-token/src/actions/create-mint.ts b/js/compressed-token/src/actions/create-mint.ts index 8d1110b70f..0f42ca6b24 100644 --- a/js/compressed-token/src/actions/create-mint.ts +++ b/js/compressed-token/src/actions/create-mint.ts @@ -21,17 +21,18 @@ import { /** * Create and initialize a new compressed token mint * - * @param rpc RPC to use - * @param payer Payer of the transaction and initialization fees - * @param mintAuthority Account or multisig that will control minting - * @param decimals Location of the decimal place - * @param keypair Optional keypair, defaulting to a new random one - * @param confirmOptions Options for confirming the transaction - * @param tokenProgramId Program ID for the token. Defaults to - * TOKEN_PROGRAM_ID. You can pass in a boolean to - * automatically resolve to TOKEN_2022_PROGRAM_ID if - * true, or TOKEN_PROGRAM_ID if false. - * @param freezeAuthority Account that will control freeze and thaw. Defaults to null. + * @param rpc RPC to use + * @param payer Payer of the transaction and initialization fees + * @param mintAuthority Account or multisig that will control minting + * @param decimals Location of the decimal place + * @param keypair Optional keypair, defaulting to a new random one + * @param confirmOptions Options for confirming the transaction + * @param tokenProgramId Program ID for the token. Defaults to + * TOKEN_PROGRAM_ID. You can pass in a boolean to + * automatically resolve to TOKEN_2022_PROGRAM_ID if + * true, or TOKEN_PROGRAM_ID if false. + * @param freezeAuthority Account that will control freeze and thaw. Defaults + * to null. * * @return Address of the new mint and the transaction signature */ diff --git a/js/compressed-token/src/actions/decompress.ts b/js/compressed-token/src/actions/decompress.ts index 1ef8cebcd0..58d791b827 100644 --- a/js/compressed-token/src/actions/decompress.ts +++ b/js/compressed-token/src/actions/decompress.ts @@ -12,30 +12,36 @@ import { Rpc, dedupeSigner, selectStateTreeInfo, + StateTreeInfo, } from '@lightprotocol/stateless.js'; import BN from 'bn.js'; import { CompressedTokenProgram } from '../program'; import { selectMinCompressedTokenAccountsForTransfer } from '../utils'; -import { selectTokenPoolInfosForDecompression } from '../utils/get-token-pool-infos'; +import { + selectTokenPoolInfosForDecompression, + TokenPoolInfo, +} from '../utils/get-token-pool-infos'; import { getTokenPoolInfos } from '../utils/get-token-pool-infos'; /** * Decompress compressed tokens * - * @param rpc Rpc to use - * @param payer Payer of the transaction fees - * @param mint Mint of the compressed token - * @param amount Number of tokens to transfer - * @param owner Owner of the compressed tokens - * @param toAddress Destination **uncompressed** (associated) token account - * address. - * @param merkleTree State tree account that any change compressed tokens should be - * inserted into. Defaults to a default state tree - * account. - * @param confirmOptions Options for confirming the transaction - * + * @param rpc Rpc to use + * @param payer Payer of the transaction fees + * @param mint Mint of the compressed token + * @param amount Number of tokens to transfer + * @param owner Owner of the compressed tokens + * @param toAddress Destination **uncompressed** (associated) token + * account address. + * @param outputStateTreeInfo State tree account that any change compressed + * tokens should be inserted into. Defaults to a + * default state tree account. + * @param tokenPoolInfos Token pool infos + * @param confirmOptions Options for confirming the transaction + * @param tokenProgramId Optional: token program id. Default: SPL Token + * Program ID * * @return Signature of the confirmed transaction */ @@ -46,7 +52,8 @@ export async function decompress( amount: number | BN, owner: Signer, toAddress: PublicKey, - merkleTree?: PublicKey, + outputStateTreeInfo?: StateTreeInfo, + tokenPoolInfos?: TokenPoolInfo[], confirmOptions?: ConfirmOptions, tokenProgramId?: PublicKey, ): Promise { @@ -72,10 +79,11 @@ export async function decompress( inputAccounts.map(account => bn(account.compressedAccount.hash)), ); - const stateTreeInfos = await rpc.getCachedActiveStateTreeInfos(); - const selectedStateTreeInfo = selectStateTreeInfo(stateTreeInfos); + outputStateTreeInfo = + outputStateTreeInfo ?? + selectStateTreeInfo(await rpc.getCachedActiveStateTreeInfos()); - const tokenPoolInfos = await getTokenPoolInfos(rpc, mint); + tokenPoolInfos = tokenPoolInfos ?? (await getTokenPoolInfos(rpc, mint)); const selectedTokenPoolInfos = selectTokenPoolInfosForDecompression( tokenPoolInfos, amount, @@ -86,7 +94,7 @@ export async function decompress( inputCompressedTokenAccounts: inputAccounts, toAddress, amount, - outputStateTreeInfo: selectedStateTreeInfo, + outputStateTreeInfo, tokenPoolInfos: selectedTokenPoolInfos, recentInputStateRootIndices: proof.rootIndices, recentValidityProof: proof.compressedProof, diff --git a/js/compressed-token/src/actions/merge-token-accounts.ts b/js/compressed-token/src/actions/merge-token-accounts.ts index 9eef01a9c1..19e10253f1 100644 --- a/js/compressed-token/src/actions/merge-token-accounts.ts +++ b/js/compressed-token/src/actions/merge-token-accounts.ts @@ -11,6 +11,8 @@ import { buildAndSignTx, sendAndConfirmTx, bn, + StateTreeInfo, + selectStateTreeInfo, } from '@lightprotocol/stateless.js'; import { CompressedTokenProgram } from '../program'; @@ -18,23 +20,27 @@ import { CompressedTokenProgram } from '../program'; * Merge multiple compressed token accounts for a given mint into a single * account * - * @param rpc RPC to use - * @param payer Payer of the transaction fees - * @param mint Public key of the token's mint - * @param owner Owner of the token accounts to be merged - * @param merkleTree Optional merkle tree for compressed tokens - * @param confirmOptions Options for confirming the transaction + * @param rpc RPC to use + * @param payer Payer of the transaction fees + * @param mint Public key of the token's mint + * @param owner Owner of the token accounts to be merged + * @param outputStateTreeInfo Optional merkle tree for compressed tokens + * @param confirmOptions Options for confirming the transaction * - * @return Array of transaction signatures + * @return signature of the confirmed transaction */ export async function mergeTokenAccounts( rpc: Rpc, payer: Signer, mint: PublicKey, owner: Signer, - merkleTree?: PublicKey, + outputStateTreeInfo?: StateTreeInfo, confirmOptions?: ConfirmOptions, ): Promise { + outputStateTreeInfo = + outputStateTreeInfo ?? + selectStateTreeInfo(await rpc.getCachedActiveStateTreeInfos()); + const compressedTokenAccounts = await rpc.getCompressedTokenAccountsByOwner( owner.publicKey, { mint }, @@ -45,11 +51,6 @@ export async function mergeTokenAccounts( `No compressed token accounts found for mint ${mint.toBase58()}`, ); } - if (compressedTokenAccounts.items.length >= 6) { - throw new Error( - `Too many compressed token accounts used for mint ${mint.toBase58()}`, - ); - } const instructions = [ ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), @@ -57,10 +58,10 @@ export async function mergeTokenAccounts( for ( let i = 0; - i < compressedTokenAccounts.items.slice(0, 6).length; - i += 3 + i < compressedTokenAccounts.items.slice(0, 8).length; + i += 4 ) { - const batch = compressedTokenAccounts.items.slice(i, i + 3); + const batch = compressedTokenAccounts.items.slice(i, i + 4); const proof = await rpc.getValidityProof( batch.map(account => bn(account.compressedAccount.hash)), @@ -72,7 +73,7 @@ export async function mergeTokenAccounts( owner: owner.publicKey, mint, inputCompressedTokenAccounts: batch, - outputStateTreeInfo: merkleTree!, + outputStateTreeInfo, recentValidityProof: proof.compressedProof, recentInputStateRootIndices: proof.rootIndices, }); @@ -89,7 +90,6 @@ export async function mergeTokenAccounts( blockhash, additionalSigners, ); - const txId = await sendAndConfirmTx(rpc, signedTx, confirmOptions); - return txId; + return sendAndConfirmTx(rpc, signedTx, confirmOptions); } diff --git a/js/compressed-token/src/actions/mint-to.ts b/js/compressed-token/src/actions/mint-to.ts index dcca4518cc..cd1524fa42 100644 --- a/js/compressed-token/src/actions/mint-to.ts +++ b/js/compressed-token/src/actions/mint-to.ts @@ -11,9 +11,15 @@ import { buildAndSignTx, Rpc, dedupeSigner, - pickRandomTreeAndQueue, + StateTreeInfo, + selectStateTreeInfo, } from '@lightprotocol/stateless.js'; import { CompressedTokenProgram } from '../program'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + TokenPoolInfo, +} from '../utils/get-token-pool-infos'; /** * Mint compressed tokens to a solana address @@ -26,7 +32,7 @@ import { CompressedTokenProgram } from '../program'; * @param authority Minting authority * @param amount Amount to mint. Can be an array of amounts if the * destination is an array of addresses. - * @param merkleTree State tree account that the compressed tokens should be + * @param outputStateTreeInfo State tree account that the compressed tokens should be * part of. Defaults to the default state tree account. * @param confirmOptions Options for confirming the transaction * @@ -39,7 +45,8 @@ export async function mintTo( destination: PublicKey | PublicKey[], authority: Signer, amount: number | BN | number[] | BN[], - merkleTree?: PublicKey, + outputStateTreeInfo?: StateTreeInfo, + tokenPoolInfo?: TokenPoolInfo, confirmOptions?: ConfirmOptions, tokenProgramId?: PublicKey, ): Promise { @@ -47,25 +54,27 @@ export async function mintTo( ? tokenProgramId : await CompressedTokenProgram.get_mint_program_id(mint, rpc); - const additionalSigners = dedupeSigner(payer, [authority]); + outputStateTreeInfo = + outputStateTreeInfo ?? + selectStateTreeInfo(await rpc.getCachedActiveStateTreeInfos()); - if (!merkleTree) { - const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); - const { tree } = pickRandomTreeAndQueue(stateTreeInfo); - merkleTree = tree; - } + tokenPoolInfo = + tokenPoolInfo ?? + selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); const ix = await CompressedTokenProgram.mintTo({ feePayer: payer.publicKey, mint, authority: authority.publicKey, - amount: amount, + amount, toPubkey: destination, - merkleTree, + outputStateTreeInfo, + tokenPoolInfo, tokenProgramId, }); const { blockhash } = await rpc.getLatestBlockhash(); + const additionalSigners = dedupeSigner(payer, [authority]); const tx = buildAndSignTx( [ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), ix], @@ -74,7 +83,5 @@ export async function mintTo( additionalSigners, ); - const txId = await sendAndConfirmTx(rpc, tx, confirmOptions); - - return txId; + return sendAndConfirmTx(rpc, tx, confirmOptions); } diff --git a/js/compressed-token/src/actions/transfer.ts b/js/compressed-token/src/actions/transfer.ts index 396b46a1ce..286d94237a 100644 --- a/js/compressed-token/src/actions/transfer.ts +++ b/js/compressed-token/src/actions/transfer.ts @@ -11,6 +11,8 @@ import { buildAndSignTx, Rpc, dedupeSigner, + StateTreeInfo, + selectStateTreeInfo, } from '@lightprotocol/stateless.js'; import BN from 'bn.js'; @@ -21,17 +23,16 @@ import { selectMinCompressedTokenAccountsForTransfer } from '../utils'; /** * Transfer compressed tokens from one owner to another * - * @param rpc Rpc to use - * @param payer Payer of the transaction fees - * @param mint Mint of the compressed token - * @param amount Number of tokens to transfer - * @param owner Owner of the compressed tokens - * @param toAddress Destination address of the recipient - * @param merkleTree State tree account that the compressed tokens should be - * inserted into. Defaults to the default state tree - * account. - * @param confirmOptions Options for confirming the transaction - * + * @param rpc Rpc to use + * @param payer Payer of the transaction fees + * @param mint Mint of the compressed token + * @param amount Number of tokens to transfer + * @param owner Owner of the compressed tokens + * @param toAddress Destination address of the recipient + * @param outputStateTreeInfo State tree account that the compressed tokens + * should be inserted into. Defaults to the default + * state tree account. + * @param confirmOptions Options for confirming the transaction * * @return Signature of the confirmed transaction */ @@ -42,7 +43,7 @@ export async function transfer( amount: number | BN, owner: Signer, toAddress: PublicKey, - merkleTree?: PublicKey, + outputStateTreeInfo?: StateTreeInfo, confirmOptions?: ConfirmOptions, ): Promise { amount = bn(amount); @@ -58,6 +59,10 @@ export async function transfer( amount, ); + outputStateTreeInfo = + outputStateTreeInfo ?? + selectStateTreeInfo(await rpc.getCachedActiveStateTreeInfos()); + const proof = await rpc.getValidityProof( inputAccounts.map(account => bn(account.compressedAccount.hash)), ); @@ -69,18 +74,17 @@ export async function transfer( amount, recentInputStateRootIndices: proof.rootIndices, recentValidityProof: proof.compressedProof, - outputStateTrees: merkleTree, + outputStateTreeInfo, }); const { blockhash } = await rpc.getLatestBlockhash(); const additionalSigners = dedupeSigner(payer, [owner]); const signedTx = buildAndSignTx( - [ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), ix], + [ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }), ix], payer, blockhash, additionalSigners, ); - const txId = await sendAndConfirmTx(rpc, signedTx, confirmOptions); - return txId; + return sendAndConfirmTx(rpc, signedTx, confirmOptions); } diff --git a/js/stateless.js/src/actions/compress.ts b/js/stateless.js/src/actions/compress.ts index bda66cd073..138cdde21a 100644 --- a/js/stateless.js/src/actions/compress.ts +++ b/js/stateless.js/src/actions/compress.ts @@ -42,7 +42,9 @@ export async function compress( if (!outputStateTreeInfo) { const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); + console.log('stateTreeInfo', stateTreeInfo); outputStateTreeInfo = selectStateTreeInfo(stateTreeInfo); + console.log('selected outputStateTreeInfo', outputStateTreeInfo); } const ix = await LightSystemProgram.compress({ diff --git a/js/stateless.js/src/state/types.ts b/js/stateless.js/src/state/types.ts index 828b3235db..0d511fde13 100644 --- a/js/stateless.js/src/state/types.ts +++ b/js/stateless.js/src/state/types.ts @@ -61,7 +61,7 @@ export type StateTreeInfo = { /** * The state nullfier queue belonging to merkleTree. */ - queue: PublicKey | null; + queue: PublicKey; /** * The compressed cpi context account. */ diff --git a/js/stateless.js/tests/e2e/compress.test.ts b/js/stateless.js/tests/e2e/compress.test.ts index 7fe342f496..edb5e23eb3 100644 --- a/js/stateless.js/tests/e2e/compress.test.ts +++ b/js/stateless.js/tests/e2e/compress.test.ts @@ -11,11 +11,13 @@ import { newAccountWithLamports } from '../../src/test-helpers/test-utils'; import { Rpc } from '../../src/rpc'; import { LightSystemProgram, + StateTreeInfo, bn, compress, createAccount, createAccountWithLamports, decompress, + selectStateTreeInfo, } from '../../src'; import { TestRpc, getTestRpc } from '../../src/test-helpers/test-rpc'; import { WasmFactory } from '@lightprotocol/hasher.rs'; @@ -65,11 +67,15 @@ function txFees( describe('compress', () => { let rpc: Rpc; let payer: Signer; + let stateTreeInfo: StateTreeInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); rpc = await getTestRpc(lightWasm); payer = await newAccountWithLamports(rpc, 1e9, 256); + stateTreeInfo = selectStateTreeInfo( + await rpc.getCachedActiveStateTreeInfos(), + ); }); it('should create account with address', async () => { @@ -87,7 +93,7 @@ describe('compress', () => { LightSystemProgram.programId, undefined, undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await createAccountWithLamports( @@ -103,7 +109,7 @@ describe('compress', () => { LightSystemProgram.programId, undefined, undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await createAccount( @@ -118,7 +124,7 @@ describe('compress', () => { LightSystemProgram.programId, undefined, undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await createAccount( @@ -133,7 +139,7 @@ describe('compress', () => { LightSystemProgram.programId, undefined, undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await expect( createAccount( @@ -149,7 +155,7 @@ describe('compress', () => { LightSystemProgram.programId, undefined, undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ), ).rejects.toThrow(); const postCreateAccountsBalance = await rpc.getBalance(payer.publicKey); @@ -177,7 +183,7 @@ describe('compress', () => { payer, compressLamportsAmount, payer.publicKey, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); const compressedAccounts = await rpc.getCompressedAccountsByOwner( @@ -211,7 +217,7 @@ describe('compress', () => { LightSystemProgram.programId, undefined, undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); const postCreateAccountBalance = await rpc.getBalance(payer.publicKey); @@ -228,13 +234,7 @@ describe('compress', () => { const preCompressBalance = await rpc.getBalance(payer.publicKey); assert.equal(preCompressBalance, 1e9); - await compress( - rpc, - payer, - compressLamportsAmount, - payer.publicKey, - defaultTestStateTreeAccounts().merkleTree, - ); + await compress(rpc, payer, compressLamportsAmount, payer.publicKey); const compressedAccounts = await rpc.getCompressedAccountsByOwner( payer.publicKey, @@ -273,13 +273,7 @@ describe('compress', () => { Number(compressedAccounts2.items[0].lamports), compressLamportsAmount - decompressLamportsAmount, ); - await decompress( - rpc, - payer, - 1, - decompressRecipient, - defaultTestStateTreeAccounts().merkleTree, - ); + await decompress(rpc, payer, 1, decompressRecipient, stateTreeInfo); const postDecompressBalance = await rpc.getBalance(decompressRecipient); assert.equal( diff --git a/js/stateless.js/tests/e2e/rpc-interop.test.ts b/js/stateless.js/tests/e2e/rpc-interop.test.ts index ab9a096def..d7be8f1caa 100644 --- a/js/stateless.js/tests/e2e/rpc-interop.test.ts +++ b/js/stateless.js/tests/e2e/rpc-interop.test.ts @@ -34,13 +34,7 @@ describe('rpc-interop', () => { payer = await newAccountWithLamports(rpc, 10e9, 256); bob = await newAccountWithLamports(rpc, 10e9, 256); - await compress( - rpc, - payer, - 1e9, - payer.publicKey, - defaultTestStateTreeAccounts().merkleTree, - ); + await compress(rpc, payer, 1e9, payer.publicKey); executedTxs++; }); @@ -172,7 +166,6 @@ describe('rpc-interop', () => { LightSystemProgram.programId, undefined, undefined, - defaultTestStateTreeAccounts().merkleTree, ); executedTxs++; @@ -185,7 +178,6 @@ describe('rpc-interop', () => { LightSystemProgram.programId, undefined, undefined, - defaultTestStateTreeAccounts().merkleTree, ); executedTxs++; }); @@ -316,7 +308,6 @@ describe('rpc-interop', () => { LightSystemProgram.programId, undefined, undefined, - defaultTestStateTreeAccounts().merkleTree, ); executedTxs++; }); @@ -535,13 +526,7 @@ describe('rpc-interop', () => { }); it('getMultipleCompressedAccounts should match', async () => { - await compress( - rpc, - payer, - 1e9, - payer.publicKey, - defaultTestStateTreeAccounts().merkleTree, - ); + await compress(rpc, payer, 1e9, payer.publicKey); executedTxs++; const senderAccounts = await rpc.getCompressedAccountsByOwner( @@ -659,15 +644,7 @@ describe('rpc-interop', () => { const addressTree = defaultTestStateTreeAccounts().addressTree; const address = deriveAddress(seed, addressTree); - await createAccount( - rpc, - payer, - seeds, - LightSystemProgram.programId, - undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, - ); + await createAccount(rpc, payer, seeds, LightSystemProgram.programId); // fetch the owners latest account const accounts = await rpc.getCompressedAccountsByOwner( @@ -700,7 +677,6 @@ describe('rpc-interop', () => { LightSystemProgram.programId, addressTree, addressQueue, - defaultTestStateTreeAccounts().merkleTree, ); // fetch the owners latest account diff --git a/js/stateless.js/tests/e2e/rpc-multi-trees.test.ts b/js/stateless.js/tests/e2e/rpc-multi-trees.test.ts index 268caeeec5..8bfbd804b4 100644 --- a/js/stateless.js/tests/e2e/rpc-multi-trees.test.ts +++ b/js/stateless.js/tests/e2e/rpc-multi-trees.test.ts @@ -1,9 +1,10 @@ import { describe, it, assert, beforeAll, expect } from 'vitest'; import { PublicKey, Signer } from '@solana/web3.js'; import { newAccountWithLamports } from '../../src/test-helpers/test-utils'; -import { Rpc, createRpc, pickRandomTreeAndQueue } from '../../src/rpc'; +import { Rpc, createRpc } from '../../src/rpc'; import { LightSystemProgram, + StateTreeInfo, bn, compress, createAccount, @@ -11,6 +12,8 @@ import { defaultTestStateTreeAccounts2, deriveAddress, deriveAddressSeed, + pickRandomTreeAndQueue, + selectStateTreeInfo, } from '../../src'; import { getTestRpc, TestRpc } from '../../src/test-helpers/test-rpc'; import { transfer } from '../../src/actions/transfer'; @@ -26,26 +29,23 @@ describe('rpc-multi-trees', () => { const randTrees: PublicKey[] = []; const randQueues: PublicKey[] = []; - + let stateTreeInfo2: StateTreeInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); rpc = createRpc(); testRpc = await getTestRpc(lightWasm); + const stateTreeInfo = (await rpc.getCachedActiveStateTreeInfos())[0]; + stateTreeInfo2 = (await rpc.getCachedActiveStateTreeInfos())[1]; + /// These are constant test accounts in between test runs payer = await newAccountWithLamports(rpc, 10e9, 256); bob = await newAccountWithLamports(rpc, 10e9, 256); - await compress( - rpc, - payer, - 1e9, - payer.publicKey, - defaultTestStateTreeAccounts2().merkleTree2, - ); - randTrees.push(defaultTestStateTreeAccounts2().merkleTree2); - randQueues.push(defaultTestStateTreeAccounts2().nullifierQueue2); + await compress(rpc, payer, 1e9, payer.publicKey, stateTreeInfo); + randTrees.push(stateTreeInfo.tree); + randQueues.push(stateTreeInfo.queue); executedTxs++; }); @@ -77,7 +77,7 @@ describe('rpc-multi-trees', () => { let address: PublicKey; it('must create account with random output tree (pickRandomTreeAndQueue)', async () => { - const tree = pickRandomTreeAndQueue( + const tree = selectStateTreeInfo( await rpc.getCachedActiveStateTreeInfos(), ); @@ -95,7 +95,7 @@ describe('rpc-multi-trees', () => { LightSystemProgram.programId, undefined, undefined, - tree.tree, // output state tree + tree, // output state tree ); randTrees.push(tree.tree); @@ -120,18 +120,18 @@ describe('rpc-multi-trees', () => { expect(validityProof.nullifierQueues[0]).toEqual(randQueues[pos]); /// Executes transfers using random output trees - const tree1 = pickRandomTreeAndQueue( + const tree1 = selectStateTreeInfo( await rpc.getCachedActiveStateTreeInfos(), ); - await transfer(rpc, payer, 1e5, payer, bob.publicKey, tree1.tree); + await transfer(rpc, payer, 1e5, payer, bob.publicKey, tree1); executedTxs++; randTrees.push(tree1.tree); randQueues.push(tree1.queue); - const tree2 = pickRandomTreeAndQueue( + const tree2 = selectStateTreeInfo( await rpc.getCachedActiveStateTreeInfos(), ); - await transfer(rpc, payer, 1e5, payer, bob.publicKey, tree2.tree); + await transfer(rpc, payer, 1e5, payer, bob.publicKey, tree2); executedTxs++; randTrees.push(tree2.tree); randQueues.push(tree2.queue); @@ -195,7 +195,7 @@ describe('rpc-multi-trees', () => { /// Creates a compressed account with address and lamports using a /// (combined) 'validityProof' from Photon - const tree = pickRandomTreeAndQueue( + const tree = selectStateTreeInfo( await rpc.getCachedActiveStateTreeInfos(), ); await createAccountWithLamports( @@ -206,7 +206,7 @@ describe('rpc-multi-trees', () => { LightSystemProgram.programId, undefined, undefined, - tree.tree, + tree, ); executedTxs++; randTrees.push(tree.tree); @@ -238,7 +238,7 @@ describe('rpc-multi-trees', () => { ); }); - const tree = pickRandomTreeAndQueue( + const tree = selectStateTreeInfo( await rpc.getCachedActiveStateTreeInfos(), ); await transfer( @@ -247,20 +247,14 @@ describe('rpc-multi-trees', () => { transferAmount, payer, bob.publicKey, - tree.tree, + tree, ); executedTxs++; } }); it('getMultipleCompressedAccounts should match', async () => { - await compress( - rpc, - payer, - 1e9, - payer.publicKey, - defaultTestStateTreeAccounts2().merkleTree2, - ); + await compress(rpc, payer, 1e9, payer.publicKey, stateTreeInfo2); executedTxs++; const senderAccounts = await rpc.getCompressedAccountsByOwner( diff --git a/js/stateless.js/tests/e2e/test-rpc.test.ts b/js/stateless.js/tests/e2e/test-rpc.test.ts index 9d4ab0254e..34737a97b4 100644 --- a/js/stateless.js/tests/e2e/test-rpc.test.ts +++ b/js/stateless.js/tests/e2e/test-rpc.test.ts @@ -10,7 +10,6 @@ import { compress, decompress, transfer } from '../../src/actions'; import { bn, CompressedAccountWithMerkleContext } from '../../src/state'; import { getTestRpc, TestRpc } from '../../src/test-helpers/test-rpc'; import { WasmFactory } from '@lightprotocol/hasher.rs'; -import { createRpc } from '../../src'; /// TODO: add test case for payer != address describe('test-rpc', () => { @@ -33,25 +32,13 @@ describe('test-rpc', () => { payer = await newAccountWithLamports(rpc, 1e9, 148); /// compress refPayer - await compress( - rpc, - refPayer, - refCompressLamports, - refPayer.publicKey, - defaultTestStateTreeAccounts().merkleTree, - ); + await compress(rpc, refPayer, refCompressLamports, refPayer.publicKey); /// compress compressLamportsAmount = 1e7; preCompressBalance = await rpc.getBalance(payer.publicKey); - await compress( - rpc, - payer, - compressLamportsAmount, - payer.publicKey, - defaultTestStateTreeAccounts().merkleTree, - ); + await compress(rpc, payer, compressLamportsAmount, payer.publicKey); }); it('getCompressedAccountsByOwner', async () => { @@ -107,7 +94,7 @@ describe('test-rpc', () => { compressLamportsAmount, payer, payer.publicKey, - merkleTree, + // merkleTree, ); const compressedAccounts1 = await rpc.getCompressedAccountsByOwner( payer.publicKey, @@ -127,7 +114,7 @@ describe('test-rpc', () => { payer, compressLamportsAmount, payer.publicKey, - defaultTestStateTreeAccounts().merkleTree, + // defaultTestStateTreeAccounts().merkleTree, ); const compressedAccounts2 = await rpc.getCompressedAccountsByOwner( payer.publicKey, diff --git a/js/stateless.js/tests/e2e/testnet.test.ts b/js/stateless.js/tests/e2e/testnet.test.ts index 0b42522016..b3a09e49d7 100644 --- a/js/stateless.js/tests/e2e/testnet.test.ts +++ b/js/stateless.js/tests/e2e/testnet.test.ts @@ -25,7 +25,7 @@ describe('testnet transfer', () => { payer, 1e9, payer.publicKey, - defaultTestStateTreeAccounts().merkleTree, + // defaultTestStateTreeAccounts().merkleTree, ); }); diff --git a/js/stateless.js/tests/e2e/transfer.test.ts b/js/stateless.js/tests/e2e/transfer.test.ts index 75a58d62be..13572991de 100644 --- a/js/stateless.js/tests/e2e/transfer.test.ts +++ b/js/stateless.js/tests/e2e/transfer.test.ts @@ -23,7 +23,7 @@ describe('transfer', () => { payer, 1e9, payer.publicKey, - defaultTestStateTreeAccounts().merkleTree, + // defaultTestStateTreeAccounts().merkleTree, ); }); From bcc5dc989c8400bd8dd6bdc08730fbd5746585d4 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Mon, 7 Apr 2025 22:07:27 +0100 Subject: [PATCH 09/19] wip --- js/compressed-token/src/actions/decompress.ts | 3 +- .../tests/e2e/decompress.test.ts | 7 +- js/stateless.js/package.json | 4 +- js/stateless.js/src/actions/compress.ts | 3 +- js/stateless.js/src/actions/index.ts | 2 +- js/stateless.js/src/rpc.ts | 61 ++-- .../test-rpc/get-compressed-accounts.ts | 68 +++- .../test-rpc/get-compressed-token-accounts.ts | 25 +- .../test-rpc/get-parsed-events.ts | 2 +- .../src/test-helpers/test-rpc/test-rpc.ts | 304 ++++++++++++++---- .../common.ts => utils/dedupe-signer.ts} | 0 .../src/utils/get-state-tree-infos.ts | 206 +----------- js/stateless.js/src/utils/index.ts | 2 + .../src/utils/state-tree-lookup-table.ts | 205 ++++++++++++ js/stateless.js/tests/e2e/rpc-interop.test.ts | 40 ++- js/stateless.js/tests/e2e/test-rpc.test.ts | 8 +- js/stateless.js/vitest.config.ts | 2 +- 17 files changed, 609 insertions(+), 333 deletions(-) rename js/stateless.js/src/{actions/common.ts => utils/dedupe-signer.ts} (100%) create mode 100644 js/stateless.js/src/utils/state-tree-lookup-table.ts diff --git a/js/compressed-token/src/actions/decompress.ts b/js/compressed-token/src/actions/decompress.ts index 58d791b827..a1190ce6d1 100644 --- a/js/compressed-token/src/actions/decompress.ts +++ b/js/compressed-token/src/actions/decompress.ts @@ -109,6 +109,5 @@ export async function decompress( blockhash, additionalSigners, ); - const txId = await sendAndConfirmTx(rpc, signedTx, confirmOptions); - return txId; + return await sendAndConfirmTx(rpc, signedTx, confirmOptions); } diff --git a/js/compressed-token/tests/e2e/decompress.test.ts b/js/compressed-token/tests/e2e/decompress.test.ts index aaa6b72ef9..3ab2131dff 100644 --- a/js/compressed-token/tests/e2e/decompress.test.ts +++ b/js/compressed-token/tests/e2e/decompress.test.ts @@ -8,6 +8,9 @@ import { defaultTestStateTreeAccounts, newAccountWithLamports, getTestRpc, + getActiveStateTreeInfos, + selectStateTreeInfo, + StateTreeInfo, } from '@lightprotocol/stateless.js'; import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMint, decompress, mintTo } from '../../src/actions'; @@ -66,11 +69,13 @@ describe('decompress', () => { let charlieAta: PublicKey; let mint: PublicKey; let mintAuthority: Keypair; - const { merkleTree } = defaultTestStateTreeAccounts(); + let merkleTree: StateTreeInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); rpc = await getTestRpc(lightWasm); + const stateTreeInfos = await getActiveStateTreeInfos(rpc); + merkleTree = selectStateTreeInfo(stateTreeInfos); payer = await newAccountWithLamports(rpc, 1e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); diff --git a/js/stateless.js/package.json b/js/stateless.js/package.json index 9d4a04702a..7d1b8fa163 100644 --- a/js/stateless.js/package.json +++ b/js/stateless.js/package.json @@ -92,8 +92,8 @@ "test-validator": "./../../cli/test_bin/run test-validator --prover-run-mode rpc", "test:e2e:transfer": "pnpm test-validator && vitest run tests/e2e/transfer.test.ts --reporter=verbose", "test:e2e:compress": "pnpm test-validator && vitest run tests/e2e/compress.test.ts --reporter=verbose", - "test:e2e:test-rpc": "pnpm test-validator && vitest run tests/e2e/test-rpc.test.ts", - "test:e2e:rpc-interop": "pnpm test-validator && vitest run tests/e2e/rpc-interop.test.ts", + "test:e2e:test-rpc": "pnpm test-validator && vitest run tests/e2e/test-rpc.test.ts --reporter=verbose --bail=1", + "test:e2e:rpc-interop": "pnpm test-validator && vitest run tests/e2e/rpc-interop.test.ts --reporter=verbose --bail=1", "test:e2e:rpc-multi-trees": "pnpm test-validator && vitest run tests/e2e/rpc-multi-trees.test.ts", "test:e2e:browser": "pnpm playwright test", "test:e2e:all": "pnpm test-validator && vitest run tests/e2e/test-rpc.test.ts && vitest run tests/e2e/compress.test.ts && vitest run tests/e2e/transfer.test.ts && vitest run tests/e2e/rpc-interop.test.ts && vitest run tests/e2e/rpc-multi-trees.test.ts && vitest run tests/e2e/layout.test.ts && vitest run tests/e2e/safe-conversion.test.ts", diff --git a/js/stateless.js/src/actions/compress.ts b/js/stateless.js/src/actions/compress.ts index 138cdde21a..6b0e5e07f2 100644 --- a/js/stateless.js/src/actions/compress.ts +++ b/js/stateless.js/src/actions/compress.ts @@ -13,6 +13,7 @@ import { selectStateTreeInfo, sendAndConfirmTx, } from '../utils'; + import BN from 'bn.js'; import { StateTreeInfo } from '../state'; @@ -43,8 +44,8 @@ export async function compress( if (!outputStateTreeInfo) { const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); console.log('stateTreeInfo', stateTreeInfo); + console.log('selectStateTreeInfo()', selectStateTreeInfo); outputStateTreeInfo = selectStateTreeInfo(stateTreeInfo); - console.log('selected outputStateTreeInfo', outputStateTreeInfo); } const ix = await LightSystemProgram.compress({ diff --git a/js/stateless.js/src/actions/index.ts b/js/stateless.js/src/actions/index.ts index 98fa16eca1..1155902c77 100644 --- a/js/stateless.js/src/actions/index.ts +++ b/js/stateless.js/src/actions/index.ts @@ -1,5 +1,5 @@ export * from './compress'; export * from './create-account'; export * from './decompress'; -export * from './common'; +export * from '../utils/dedupe-signer'; export * from './transfer'; diff --git a/js/stateless.js/src/rpc.ts b/js/stateless.js/src/rpc.ts index 3f583377d1..1459c9051f 100644 --- a/js/stateless.js/src/rpc.ts +++ b/js/stateless.js/src/rpc.ts @@ -55,6 +55,7 @@ import { createMerkleContext, TokenData, CompressedProof, + TreeType, } from './state'; import { array, create, nullable } from 'superstruct'; import { @@ -74,6 +75,7 @@ import { LightWasm } from './test-helpers'; import { getActiveStateTreeInfos } from './utils/get-state-tree-infos'; import { StateTreeInfo } from './state/types'; import { validateNumbersForProof } from './utils'; +import { getQueueForTree } from './test-helpers/test-rpc/get-compressed-accounts'; /** @internal */ export function parseAccountData({ @@ -132,7 +134,7 @@ async function getCompressedTokenAccountsByOwnerOrDelegate( const _account = item.account; const _tokenData = item.tokenData; - const associatedQueue = getQueueForTree( + const { queue, treeType, tree } = getQueueForTree( activeStateTreeInfo, _account.tree!, ); @@ -141,7 +143,7 @@ async function getCompressedTokenAccountsByOwnerOrDelegate( createCompressedAccountWithMerkleContext( createMerkleContext( _account.tree!, - associatedQueue, + queue, _account.hash.toArray('be', 32), _account.leafIndex, ), @@ -197,7 +199,7 @@ function buildCompressedAccountWithMaybeTokenData( const tokenDataResult = accountStructWithOptionalTokenData.optionalTokenData; - const associatedQueue = getQueueForTree( + const { queue, treeType, tree } = getQueueForTree( activeStateTreeInfo, compressedAccountResult.tree!, ); @@ -205,7 +207,7 @@ function buildCompressedAccountWithMaybeTokenData( createCompressedAccountWithMerkleContext( createMerkleContext( compressedAccountResult.merkleTree, - associatedQueue, + queue, compressedAccountResult.hash.toArray('be', 32), compressedAccountResult.leafIndex, ), @@ -551,29 +553,6 @@ export function getPublicInputHash( } } -/** - * Get the queue for a given tree - * - * @param info - The active state tree addresses - * @param tree - The tree to get the queue for - * @returns The queue for the given tree, or undefined if not found - */ -export function getQueueForTree( - info: StateTreeInfo[], - tree: PublicKey, -): PublicKey { - const index = info.findIndex(t => t.tree.equals(tree)); - if (index === -1) { - throw new Error( - 'No associated queue found for tree. Please set activeStateTreeInfo with latest Tree accounts. If you use custom state trees, set manually.', - ); - } - if (!info[index].queue) { - throw new Error('Queue must not be null for state tree'); - } - return info[index].queue; -} - /** * Get the tree for a given queue * @@ -708,7 +687,7 @@ export class Rpc extends Connection implements CompressionApiInterface { } const activeStateTreeInfo = await this.getCachedActiveStateTreeInfos(); - const associatedQueue = getQueueForTree( + const { queue, treeType, tree } = getQueueForTree( activeStateTreeInfo, res.result.value.tree!, ); @@ -716,7 +695,7 @@ export class Rpc extends Connection implements CompressionApiInterface { const account = createCompressedAccountWithMerkleContext( createMerkleContext( item.tree!, - associatedQueue, + queue, item.hash.toArray('be', 32), item.leafIndex, ), @@ -817,7 +796,7 @@ export class Rpc extends Connection implements CompressionApiInterface { ); } const activeStateTreeInfo = await this.getCachedActiveStateTreeInfos(); - const associatedQueue = getQueueForTree( + const { queue, treeType, tree } = getQueueForTree( activeStateTreeInfo, res.result.value.merkleTree, ); @@ -827,7 +806,7 @@ export class Rpc extends Connection implements CompressionApiInterface { merkleTree: res.result.value.merkleTree, leafIndex: res.result.value.leafIndex, merkleProof: res.result.value.proof, - nullifierQueue: associatedQueue, // TODO(photon): support nullifierQueue in response. + nullifierQueue: queue, // TODO(photon): support nullifierQueue in response. rootIndex: res.result.value.rootSeq % 2400, root: res.result.value.root, }; @@ -864,14 +843,14 @@ export class Rpc extends Connection implements CompressionApiInterface { const activeStateTreeInfo = await this.getCachedActiveStateTreeInfos(); const accounts: CompressedAccountWithMerkleContext[] = []; res.result.value.items.map(item => { - const associatedQueue = getQueueForTree( + const { queue, treeType, tree } = getQueueForTree( activeStateTreeInfo, - item.tree!, + item.tree, ); const account = createCompressedAccountWithMerkleContext( createMerkleContext( - item.tree!, - associatedQueue, + tree, + queue, item.hash.toArray('be', 32), item.leafIndex, ), @@ -919,7 +898,7 @@ export class Rpc extends Connection implements CompressionApiInterface { const activeStateTreeInfo = await this.getCachedActiveStateTreeInfos(); for (const proof of res.result.value) { - const associatedQueue = getQueueForTree( + const { queue, treeType, tree } = getQueueForTree( activeStateTreeInfo, proof.merkleTree, ); @@ -928,7 +907,7 @@ export class Rpc extends Connection implements CompressionApiInterface { merkleTree: proof.merkleTree, leafIndex: proof.leafIndex, merkleProof: proof.proof, - nullifierQueue: associatedQueue, + nullifierQueue: queue, rootIndex: proof.rootSeq % 2400, root: proof.root, }; @@ -978,14 +957,14 @@ export class Rpc extends Connection implements CompressionApiInterface { const activeStateTreeInfo = await this.getCachedActiveStateTreeInfos(); res.result.value.items.map(item => { - const associatedQueue = getQueueForTree( + const { queue, tree } = getQueueForTree( activeStateTreeInfo, - item.tree!, + item.tree, ); const account = createCompressedAccountWithMerkleContext( createMerkleContext( - item.tree!, - associatedQueue, + tree, + queue, item.hash.toArray('be', 32), item.leafIndex, ), diff --git a/js/stateless.js/src/test-helpers/test-rpc/get-compressed-accounts.ts b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-accounts.ts index d87be0d809..c833bef681 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/get-compressed-accounts.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-accounts.ts @@ -9,8 +9,60 @@ import { bn, MerkleContext, createCompressedAccountWithMerkleContext, + StateTreeInfo, + TreeType, } from '../../state'; +/** + * Get the queue for a given tree + * + * @param info - The active state tree addresses + * @param tree - The tree to get the queue for + * @returns The queue for the given tree, or undefined if not found + */ +/** + * Get the queue for a given tree + * + * @param info - The active state tree addresses + * @param tree - The tree to get the queue for + * @returns The queue for the given tree, or throws an error if not found + */ +export function getQueueForTree( + info: StateTreeInfo[], + tree: PublicKey, +): { queue: PublicKey; treeType: TreeType; tree: PublicKey } { + const index = info.findIndex(t => t.tree.equals(tree)); + + if (index !== -1) { + const { queue, treeType } = info[index]; + if (!queue) { + throw new Error('Queue must not be null for state tree'); + } + return { queue, treeType, tree: info[index].tree }; + } + + // test-rpc indexes queue as tree. + const indexV2 = info.findIndex( + t => t.queue && t.queue.equals(tree) && t.treeType === TreeType.StateV2, + ); + if (indexV2 !== -1) { + const { + queue: actualQueue, + treeType, + tree: actualTree, + } = info[indexV2]; + if (!actualQueue) { + throw new Error('Queue must not be null for state tree'); + } + + return { queue: actualQueue, treeType, tree: actualTree }; + } + + throw new Error( + `No associated queue found for tree. Please set activeStateTreeInfos with latest Tree accounts. If you use custom state trees, set manually. tree: ${tree.toBase58()}`, + ); +} + export async function getCompressedAccountsByOwnerTest( rpc: Rpc, owner: PublicKey, @@ -43,6 +95,7 @@ async function getCompressedAccountsForTest(rpc: Rpc) { const events = (await getParsedEvents(rpc)).reverse(); const allOutputAccounts: CompressedAccountWithMerkleContext[] = []; const allInputAccountHashes: BN[] = []; + const ctxs = await rpc.getCachedActiveStateTreeInfos(); for (const event of events) { for ( @@ -50,10 +103,21 @@ async function getCompressedAccountsForTest(rpc: Rpc) { index < event.outputCompressedAccounts.length; index++ ) { + const smt = + event.pubkeyArray[ + event.outputCompressedAccounts[index].merkleTreeIndex + ]; + + // In test-rpc we can do this with a static set of trees because it's local-only. + const { queue, treeType, tree } = getQueueForTree( + ctxs, + new PublicKey(smt), + ); + const account = event.outputCompressedAccounts[index]; const merkleContext: MerkleContext = { - merkleTree: defaultTestStateTreeAccounts().merkleTree, - nullifierQueue: defaultTestStateTreeAccounts().nullifierQueue, + merkleTree: tree, + nullifierQueue: queue, hash: event.outputCompressedAccountHashes[index], leafIndex: event.outputLeafIndices[index], }; diff --git a/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts index f54b80077a..4b0067789b 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts @@ -6,6 +6,7 @@ import { defaultTestStateTreeAccounts, } from '../../constants'; import { Rpc } from '../../rpc'; +import { getQueueForTree } from './get-compressed-accounts'; import { ParsedTokenAccount, WithCursor } from '../../rpc-interface'; import { CompressedAccount, @@ -76,12 +77,18 @@ export function parseTokenLayoutWithIdl( */ async function parseEventWithTokenTlvData( event: PublicTransactionEvent, + rpc: Rpc, ): Promise { const pubkeyArray = event.pubkeyArray; + const ctxs = await rpc.getCachedActiveStateTreeInfos(); const outputHashes = event.outputCompressedAccountHashes; const outputCompressedAccountsWithParsedTokenData: ParsedTokenAccount[] = event.outputCompressedAccounts.map((compressedAccount, i) => { + const maybeTree = + pubkeyArray[event.outputCompressedAccounts[i].merkleTreeIndex]; + + const { queue, treeType, tree } = getQueueForTree(ctxs, maybeTree); const merkleContext: MerkleContext = { merkleTree: pubkeyArray[ @@ -133,10 +140,11 @@ async function parseEventWithTokenTlvData( */ export async function getCompressedTokenAccounts( events: PublicTransactionEvent[], + rpc: Rpc, ): Promise { const eventsWithParsedTokenTlvData: EventWithParsedTokenTlvData[] = await Promise.all( - events.map(event => parseEventWithTokenTlvData(event)), + events.map(event => parseEventWithTokenTlvData(event, rpc)), ); /// strip spent compressed accounts if an output compressed account of tx n is @@ -169,7 +177,10 @@ export async function getCompressedTokenAccountsByOwnerTest( mint: PublicKey, ): Promise> { const events = await getParsedEvents(rpc); - const compressedTokenAccounts = await getCompressedTokenAccounts(events); + const compressedTokenAccounts = await getCompressedTokenAccounts( + events, + rpc, + ); const accounts = compressedTokenAccounts.filter( acc => acc.parsed.owner.equals(owner) && acc.parsed.mint.equals(mint), ); @@ -189,7 +200,10 @@ export async function getCompressedTokenAccountsByDelegateTest( ): Promise> { const events = await getParsedEvents(rpc); - const compressedTokenAccounts = await getCompressedTokenAccounts(events); + const compressedTokenAccounts = await getCompressedTokenAccounts( + events, + rpc, + ); return { items: compressedTokenAccounts.filter( acc => @@ -206,7 +220,10 @@ export async function getCompressedTokenAccountByHashTest( ): Promise { const events = await getParsedEvents(rpc); - const compressedTokenAccounts = await getCompressedTokenAccounts(events); + const compressedTokenAccounts = await getCompressedTokenAccounts( + events, + rpc, + ); const filtered = compressedTokenAccounts.filter(acc => bn(acc.compressedAccount.hash).eq(hash), diff --git a/js/stateless.js/src/test-helpers/test-rpc/get-parsed-events.ts b/js/stateless.js/src/test-helpers/test-rpc/get-parsed-events.ts index 2ba8b4e796..72cca5c923 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/get-parsed-events.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/get-parsed-events.ts @@ -241,7 +241,7 @@ export function parseLightTransaction( INSERT_INTO_QUEUES_DISCRIMINATOR, ); if (discriminatorStr !== insertIntoQueuesDiscriminatorStr) { - console.log('discriminator does not match'); + // console.log('discriminator does not match'); } else { const dataSlice = data.slice(12); appendInputsData = diff --git a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts index faf6988a4b..122729c291 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts @@ -4,6 +4,7 @@ import { getCompressedAccountByHashTest, getCompressedAccountsByOwnerTest, getMultipleCompressedAccountsByHashTest, + getQueueForTree, } from './get-compressed-accounts'; import { getCompressedTokenAccountByHashTest, @@ -43,6 +44,7 @@ import { CompressedAccountWithMerkleContext, MerkleContextWithMerkleProof, PublicTransactionEvent, + TreeType, bn, } from '../../state'; import { IndexedArray } from '../merkle-tree'; @@ -111,13 +113,9 @@ export async function getTestRpc( endpoint: string = 'http://127.0.0.1:8899', compressionApiEndpoint: string = 'http://127.0.0.1:8784', proverEndpoint: string = 'http://127.0.0.1:3001', - merkleTreeAddress?: PublicKey, - nullifierQueueAddress?: PublicKey, depth?: number, log = false, ) { - const defaultAccounts = defaultTestStateTreeAccounts(); - return new TestRpc( endpoint, lightWasm, @@ -125,10 +123,7 @@ export async function getTestRpc( proverEndpoint, undefined, { - merkleTreeAddress: merkleTreeAddress || defaultAccounts.merkleTree, - nullifierQueueAddress: - nullifierQueueAddress || defaultAccounts.nullifierQueue, - depth: depth || defaultAccounts.merkleTreeHeight, + depth: depth || defaultTestStateTreeAccounts().merkleTreeHeight, log, }, ); @@ -145,10 +140,6 @@ export async function getTestRpc( export class TestRpc extends Connection implements CompressionApiInterface { compressionApiEndpoint: string; proverEndpoint: string; - merkleTreeAddress: PublicKey; - nullifierQueueAddress: PublicKey; - addressTreeAddress: PublicKey; - addressQueueAddress: PublicKey; lightWasm: LightWasm; depth: number; log = false; @@ -179,28 +170,10 @@ export class TestRpc extends Connection implements CompressionApiInterface { this.compressionApiEndpoint = compressionApiEndpoint; this.proverEndpoint = proverEndpoint; - const { - merkleTreeAddress, - nullifierQueueAddress, - depth, - log, - addressTreeAddress, - addressQueueAddress, - } = testRpcConfig ?? {}; - - const { - merkleTree, - nullifierQueue, - merkleTreeHeight, - addressQueue, - addressTree, - } = defaultTestStateTreeAccounts(); + const { depth, log } = testRpcConfig ?? {}; + const { merkleTreeHeight } = defaultTestStateTreeAccounts(); this.lightWasm = hasher; - this.merkleTreeAddress = merkleTreeAddress ?? merkleTree; - this.nullifierQueueAddress = nullifierQueueAddress ?? nullifierQueue; - this.addressTreeAddress = addressTreeAddress ?? addressTree; - this.addressQueueAddress = addressQueueAddress ?? addressQueue; this.depth = depth ?? merkleTreeHeight; this.log = log ?? false; } @@ -299,6 +272,72 @@ export class TestRpc extends Connection implements CompressionApiInterface { async confirmTransactionIndexed(_slot: number): Promise { return true; } + // /** + // * Fetch the latest merkle proofs for multiple compressed accounts specified + // * by an array account hashes + // */ + // async getMultipleCompressedAccountProofs( + // hashes: BN254[], + // ): Promise { + // /// Build tree + // const events: PublicTransactionEvent[] = await getParsedEvents( + // this, + // ).then(events => events.reverse()); + // const allLeaves: number[][] = []; + // const allLeafIndices: number[] = []; + // for (const event of events) { + // for ( + // let index = 0; + // index < event.outputCompressedAccounts.length; + // index++ + // ) { + // const hash = event.outputCompressedAccountHashes[index]; + + // allLeaves.push(hash); + // allLeafIndices.push(event.outputLeafIndices[index]); + // } + // } + // const tree = new MerkleTree( + // this.depth, + // this.lightWasm, + // allLeaves.map(leaf => bn(leaf).toString()), + // ); + + // /// create merkle proofs and assemble return type + // const merkleProofs: MerkleContextWithMerkleProof[] = []; + + // for (let i = 0; i < hashes.length; i++) { + // const leafIndex = tree.indexOf(hashes[i].toString()); + // const pathElements = tree.path(leafIndex).pathElements; + // const bnPathElements = pathElements.map(value => bn(value)); + // const root = bn(tree.root()); + // const merkleProof: MerkleContextWithMerkleProof = { + // hash: hashes[i].toArray('be', 32), + // merkleTree: this.merkleTreeAddress, + // leafIndex: leafIndex, + // merkleProof: bnPathElements, + // nullifierQueue: this.nullifierQueueAddress, + // rootIndex: allLeaves.length, + // root: root, + // }; + // merkleProofs.push(merkleProof); + // } + + // /// Validate + // merkleProofs.forEach((proof, index) => { + // const leafIndex = proof.leafIndex; + // const computedHash = tree.elements()[leafIndex]; + // const hashArr = bn(computedHash).toArray('be', 32); + // if (!hashArr.every((val, index) => val === proof.hash[index])) { + // throw new Error( + // `Mismatch at index ${index}: expected ${proof.hash.toString()}, got ${hashArr.toString()}`, + // ); + // } + // }); + + // return merkleProofs; + // } + /** * Fetch the latest merkle proofs for multiple compressed accounts specified * by an array account hashes @@ -306,12 +345,23 @@ export class TestRpc extends Connection implements CompressionApiInterface { async getMultipleCompressedAccountProofs( hashes: BN254[], ): Promise { - /// Build tree + // Parse events and organize leaves by their respective merkle trees const events: PublicTransactionEvent[] = await getParsedEvents( this, ).then(events => events.reverse()); - const allLeaves: number[][] = []; - const allLeafIndices: number[] = []; + const leavesByTree: Map< + string, + { + leaves: number[][]; + leafIndices: number[]; + treeType: TreeType; + queue: PublicKey; + } + > = new Map(); + + const cachedStateTreeInfos = await this.getCachedActiveStateTreeInfos(); + + /// Assign leaves to their respective trees for (const event of events) { for ( let index = 0; @@ -319,52 +369,172 @@ export class TestRpc extends Connection implements CompressionApiInterface { index++ ) { const hash = event.outputCompressedAccountHashes[index]; + const treeOrQueue = + event.pubkeyArray[ + event.outputCompressedAccounts[index].merkleTreeIndex + ]; + + const { treeType, tree, queue } = getQueueForTree( + cachedStateTreeInfos, + treeOrQueue, + ); - allLeaves.push(hash); - allLeafIndices.push(event.outputLeafIndices[index]); + if (!leavesByTree.has(tree.toBase58())) { + leavesByTree.set(tree.toBase58(), { + leaves: [], + leafIndices: [], + treeType: treeType, + queue: queue, + }); + } + + const treeData = leavesByTree.get(tree.toBase58())!; + treeData.leaves.push(hash); + treeData.leafIndices.push(event.outputLeafIndices[index]); } } - const tree = new MerkleTree( - this.depth, - this.lightWasm, - allLeaves.map(leaf => bn(leaf).toString()), - ); - /// create merkle proofs and assemble return type - const merkleProofs: MerkleContextWithMerkleProof[] = []; + const merkleProofsMap: Map = + new Map(); + + for (const [ + treeKey, + { leaves, treeType, queue }, + ] of leavesByTree.entries()) { + const tree = new PublicKey(treeKey); + + let merkleTree: MerkleTree | undefined; + if (treeType === TreeType.StateV1) { + merkleTree = new MerkleTree( + this.depth, + this.lightWasm, + leaves.map(leaf => bn(leaf).toString()), + ); + } else if (treeType === TreeType.StateV2) { + /// In V2 State trees, The Merkle tree stays empty until the + /// first forester transaction. And since test-rpc is only used + /// for non-forested tests, we must return a tree with + /// zerovalues. + merkleTree = new MerkleTree(32, this.lightWasm, []); + } else { + throw new Error( + `Invalid tree type: ${treeType} in test-rpc.ts`, + ); + } - for (let i = 0; i < hashes.length; i++) { - const leafIndex = tree.indexOf(hashes[i].toString()); - const pathElements = tree.path(leafIndex).pathElements; - const bnPathElements = pathElements.map(value => bn(value)); - const root = bn(tree.root()); - const merkleProof: MerkleContextWithMerkleProof = { - hash: hashes[i].toArray('be', 32), - merkleTree: this.merkleTreeAddress, - leafIndex: leafIndex, - merkleProof: bnPathElements, - nullifierQueue: this.nullifierQueueAddress, - rootIndex: allLeaves.length, - root: root, - }; - merkleProofs.push(merkleProof); + for (let i = 0; i < hashes.length; i++) { + // const hashStr = hashes[i].toString(); + const leafIndex = leaves.findIndex(leaf => + bn(leaf).eq(hashes[i]), + ); + + /// If leaf is part of current tree, return proof + if (leafIndex !== -1) { + if (treeType === TreeType.StateV1) { + const pathElements = + merkleTree.path(leafIndex).pathElements; + const bnPathElements = pathElements.map(value => + bn(value), + ); + const root = bn(merkleTree.root()); + + const merkleProof: MerkleContextWithMerkleProof = { + hash: hashes[i].toArray('be', 32), + merkleTree: tree, + leafIndex: leafIndex, + merkleProof: bnPathElements, + nullifierQueue: getQueueForTree( + cachedStateTreeInfos, + tree, + ).queue, + rootIndex: leaves.length, + root: root, + // treeType: treeType, + // proveByIndex: true, + }; + + merkleProofsMap.set(hashes[i].toString(), merkleProof); + } else if (treeType === TreeType.StateV2) { + const pathElements = merkleTree._zeros.slice(0, -1); + const bnPathElements = pathElements.map(value => + bn(value), + ); + const root = bn(merkleTree.root()); + + const { tree: treeV2 } = getQueueForTree( + cachedStateTreeInfos, + tree, + ); + + /// get leafIndex from leavesByTree for the given hash + const leafIndex = leavesByTree + .get(treeV2.toBase58())! + .leafIndices.findIndex(index => + hashes[i].eq( + bn( + leavesByTree.get(treeV2.toBase58())! + .leaves[index], + ), + ), + ); + + const merkleProof: MerkleContextWithMerkleProof = { + hash: new Array(32).fill(0), + merkleTree: treeV2, + leafIndex: leafIndex, + merkleProof: bnPathElements, + nullifierQueue: queue, + rootIndex: 0, + root, + // treeType: 0, // TODO: consider switching in photon + // proveByIndex: true, + }; + + merkleProofsMap.set(hashes[i].toString(), merkleProof); + } + } + } } - /// Validate - merkleProofs.forEach((proof, index) => { + // Validate proofs + merkleProofsMap.forEach((proof, index) => { const leafIndex = proof.leafIndex; - const computedHash = tree.elements()[leafIndex]; + const computedHash = leavesByTree.get(proof.merkleTree.toBase58())! + .leaves[leafIndex]; const hashArr = bn(computedHash).toArray('be', 32); - if (!hashArr.every((val, index) => val === proof.hash[index])) { + if ( + !hashArr.every((val, index) => val === proof.hash[index]) && + // proof.treeType === TreeType.StateV1 && + !proof.nullifierQueue.equals(PublicKey.default) + ) { throw new Error( `Mismatch at index ${index}: expected ${proof.hash.toString()}, got ${hashArr.toString()}`, ); } }); - return merkleProofs; + // Ensure all requested hashes belong to the same tree type + // const uniqueTreeTypes = new Set( + // hashes.map(hash => { + // const proof = merkleProofsMap.get(hash.toString()); + // if (!proof) { + // throw new Error( + // `Proof not found for hash: ${hash.toString()}`, + // ); + // } + // return proof.treeType; + // }), + // ); + + // if (uniqueTreeTypes.size > 1) { + // throw new Error( + // 'Requested hashes belong to different tree types (V1/V2)', + // ); + // } + + // Return proofs in the order of requested hashes + return hashes.map(hash => merkleProofsMap.get(hash.toString())!); } - /** * Fetch all the compressed accounts owned by the specified public key. * Owner can be a program or user account @@ -625,8 +795,8 @@ export class TestRpc extends Connection implements CompressionApiInterface { nextIndex: bn(lowElement.nextIndex), merkleProofHashedIndexedElementLeaf: bnPathElements, indexHashedIndexedElementLeaf: bn(lowElement.index), - merkleTree: this.addressTreeAddress, - nullifierQueue: this.addressQueueAddress, + merkleTree: defaultTestStateTreeAccounts().addressTree, + nullifierQueue: defaultTestStateTreeAccounts().addressQueue, }; newAddressProofs.push(proof); } diff --git a/js/stateless.js/src/actions/common.ts b/js/stateless.js/src/utils/dedupe-signer.ts similarity index 100% rename from js/stateless.js/src/actions/common.ts rename to js/stateless.js/src/utils/dedupe-signer.ts diff --git a/js/stateless.js/src/utils/get-state-tree-infos.ts b/js/stateless.js/src/utils/get-state-tree-infos.ts index 7167de3771..9534af9478 100644 --- a/js/stateless.js/src/utils/get-state-tree-infos.ts +++ b/js/stateless.js/src/utils/get-state-tree-infos.ts @@ -1,14 +1,5 @@ -import { - AddressLookupTableProgram, - Connection, - Keypair, - PublicKey, - Signer, -} from '@solana/web3.js'; -import { buildAndSignTx, sendAndConfirmTx } from './send-and-confirm'; -import { dedupeSigner } from '../actions'; +import { Connection, PublicKey } from '@solana/web3.js'; import { StateTreeInfo, TreeType } from '../state/types'; -import { Rpc } from '../rpc'; /** * @deprecated use {@link selectStateTreeInfo} instead. Get a random tree and @@ -59,201 +50,6 @@ export function selectStateTreeInfo( return filteredInfo[index]; } -/** - * Create two lookup tables storing all public state tree and queue addresses - * returns lookup table addresses and txId - * - * @internal - * @param connection Connection to the Solana network - * @param payer Keypair of the payer - * @param authority Keypair of the authority - * @param recentSlot Slot of the recent block - */ -export async function createStateTreeLookupTable({ - connection, - payer, - authority, - recentSlot, -}: { - connection: Connection; - payer: Keypair; - authority: Keypair; - recentSlot: number; -}): Promise<{ address: PublicKey; txId: string }> { - const [createInstruction1, lookupTableAddress1] = - AddressLookupTableProgram.createLookupTable({ - payer: payer.publicKey, - authority: authority.publicKey, - recentSlot, - }); - - const blockhash = await connection.getLatestBlockhash(); - - const tx = buildAndSignTx( - [createInstruction1], - payer, - blockhash.blockhash, - dedupeSigner(payer as Signer, [authority]), - ); - - const txId = await sendAndConfirmTx(connection as Rpc, tx); - - return { - address: lookupTableAddress1, - txId, - }; -} - -/** - * Extend state tree lookup table with new state tree and queue addresses - * - * @internal - * @param connection Connection to the Solana network - * @param tableAddress Address of the lookup table to extend - * @param newStateTreeAddresses Addresses of the new state trees to add - * @param newQueueAddresses Addresses of the new queues to add - * @param newCpiContextAddresses Addresses of the new cpi contexts to add - * @param payer Keypair of the payer - * @param authority Keypair of the authority - */ -export async function extendStateTreeLookupTable({ - connection, - tableAddress, - newStateTreeAddresses, - newQueueAddresses, - newCpiContextAddresses, - payer, - authority, -}: { - connection: Connection; - tableAddress: PublicKey; - newStateTreeAddresses: PublicKey[]; - newQueueAddresses: PublicKey[]; - newCpiContextAddresses: PublicKey[]; - payer: Keypair; - authority: Keypair; -}): Promise<{ tableAddress: PublicKey; txId: string }> { - const lutState = await connection.getAddressLookupTable(tableAddress); - if (!lutState.value) { - throw new Error('Lookup table not found'); - } - if (lutState.value.state.addresses.length % 3 !== 0) { - throw new Error('Lookup table must have a multiple of 3 addresses'); - } - if ( - newStateTreeAddresses.length !== newQueueAddresses.length || - newStateTreeAddresses.length !== newCpiContextAddresses.length - ) { - throw new Error( - 'Same number of newStateTreeAddresses, newQueueAddresses, and newCpiContextAddresses required', - ); - } - - const instructions = AddressLookupTableProgram.extendLookupTable({ - payer: payer.publicKey, - authority: authority.publicKey, - lookupTable: tableAddress, - addresses: newStateTreeAddresses.flatMap((addr, index) => [ - addr, - newQueueAddresses[index], - newCpiContextAddresses[index], - ]), - }); - - const blockhash = await connection.getLatestBlockhash(); - - const tx = buildAndSignTx( - [instructions], - payer, - blockhash.blockhash, - dedupeSigner(payer as Signer, [authority]), - ); - - const txId = await sendAndConfirmTx(connection as Rpc, tx); - - return { - tableAddress, - txId, - }; -} - -/** - * Adds state tree address to lookup table. Acts as nullifier lookup for rolled - * over state trees. - * - * @internal - * @param connection Connection to the Solana network - * @param stateTreeAddress Address of the state tree to nullify - * @param nullifyTableAddress Address of the nullifier lookup table to - * store address in - * @param stateTreeLookupTableAddress lookup table storing all state tree - * addresses - * @param payer Keypair of the payer - * @param authority Keypair of the authority - */ -export async function nullifyLookupTable({ - connection, - fullStateTreeAddress, - nullifyTableAddress, - stateTreeLookupTableAddress, - payer, - authority, -}: { - connection: Connection; - fullStateTreeAddress: PublicKey; - nullifyTableAddress: PublicKey; - stateTreeLookupTableAddress: PublicKey; - payer: Keypair; - authority: Keypair; -}): Promise<{ txId: string }> { - // to be nullified address must be part of stateTreeLookupTable set - const stateTreeLookupTable = await connection.getAddressLookupTable( - stateTreeLookupTableAddress, - ); - - if (!stateTreeLookupTable.value) { - throw new Error('State tree lookup table not found'); - } - - if ( - !stateTreeLookupTable.value.state.addresses.includes( - fullStateTreeAddress, - ) - ) { - throw new Error( - 'State tree address not found in lookup table. Pass correct address or stateTreeLookupTable', - ); - } - - const nullifyTable = - await connection.getAddressLookupTable(nullifyTableAddress); - - if (!nullifyTable.value) { - throw new Error('Nullify table not found'); - } - if (nullifyTable.value.state.addresses.includes(fullStateTreeAddress)) { - throw new Error('Address already exists in nullify lookup table'); - } - - const instructions = AddressLookupTableProgram.extendLookupTable({ - payer: payer.publicKey, - authority: authority.publicKey, - lookupTable: nullifyTableAddress, - addresses: [fullStateTreeAddress], - }); - - const blockhash = await connection.getLatestBlockhash(); - - const tx = buildAndSignTx([instructions], payer, blockhash.blockhash); - // we pass a Connection type so we don't have to depend on the Rpc module. - // @ts-expect-error - const txId = await sendAndConfirmTx(connection, tx); - - return { - txId, - }; -} - /** * Get most recent active state tree data we store in lookup table for each * public state tree diff --git a/js/stateless.js/src/utils/index.ts b/js/stateless.js/src/utils/index.ts index 82f9316db6..9e9489a959 100644 --- a/js/stateless.js/src/utils/index.ts +++ b/js/stateless.js/src/utils/index.ts @@ -8,3 +8,5 @@ export * from './sleep'; export * from './validation'; export * from './calculate-compute-unit-price'; export * from './get-state-tree-infos'; +export * from './state-tree-lookup-table'; +export * from './dedupe-signer'; diff --git a/js/stateless.js/src/utils/state-tree-lookup-table.ts b/js/stateless.js/src/utils/state-tree-lookup-table.ts new file mode 100644 index 0000000000..d06d18bc2d --- /dev/null +++ b/js/stateless.js/src/utils/state-tree-lookup-table.ts @@ -0,0 +1,205 @@ +import { + PublicKey, + Keypair, + Connection, + AddressLookupTableProgram, + Signer, +} from '@solana/web3.js'; +import { buildAndSignTx, sendAndConfirmTx } from './send-and-confirm'; +import { dedupeSigner } from './dedupe-signer'; +import { Rpc } from '../rpc'; + +/** + * Create two lookup tables storing all public state tree and queue addresses + * returns lookup table addresses and txId + * + * @internal + * @param connection Connection to the Solana network + * @param payer Keypair of the payer + * @param authority Keypair of the authority + * @param recentSlot Slot of the recent block + */ +export async function createStateTreeLookupTable({ + connection, + payer, + authority, + recentSlot, +}: { + connection: Connection; + payer: Keypair; + authority: Keypair; + recentSlot: number; +}): Promise<{ address: PublicKey; txId: string }> { + const [createInstruction1, lookupTableAddress1] = + AddressLookupTableProgram.createLookupTable({ + payer: payer.publicKey, + authority: authority.publicKey, + recentSlot, + }); + + const blockhash = await connection.getLatestBlockhash(); + + const tx = buildAndSignTx( + [createInstruction1], + payer, + blockhash.blockhash, + dedupeSigner(payer as Signer, [authority]), + ); + + const txId = await sendAndConfirmTx(connection as Rpc, tx); + + return { + address: lookupTableAddress1, + txId, + }; +} + +/** + * Extend state tree lookup table with new state tree and queue addresses + * + * @internal + * @param connection Connection to the Solana network + * @param tableAddress Address of the lookup table to extend + * @param newStateTreeAddresses Addresses of the new state trees to add + * @param newQueueAddresses Addresses of the new queues to add + * @param newCpiContextAddresses Addresses of the new cpi contexts to add + * @param payer Keypair of the payer + * @param authority Keypair of the authority + */ +export async function extendStateTreeLookupTable({ + connection, + tableAddress, + newStateTreeAddresses, + newQueueAddresses, + newCpiContextAddresses, + payer, + authority, +}: { + connection: Connection; + tableAddress: PublicKey; + newStateTreeAddresses: PublicKey[]; + newQueueAddresses: PublicKey[]; + newCpiContextAddresses: PublicKey[]; + payer: Keypair; + authority: Keypair; +}): Promise<{ tableAddress: PublicKey; txId: string }> { + const lutState = await connection.getAddressLookupTable(tableAddress); + if (!lutState.value) { + throw new Error('Lookup table not found'); + } + if (lutState.value.state.addresses.length % 3 !== 0) { + throw new Error('Lookup table must have a multiple of 3 addresses'); + } + if ( + newStateTreeAddresses.length !== newQueueAddresses.length || + newStateTreeAddresses.length !== newCpiContextAddresses.length + ) { + throw new Error( + 'Same number of newStateTreeAddresses, newQueueAddresses, and newCpiContextAddresses required', + ); + } + + const instructions = AddressLookupTableProgram.extendLookupTable({ + payer: payer.publicKey, + authority: authority.publicKey, + lookupTable: tableAddress, + addresses: newStateTreeAddresses.flatMap((addr, index) => [ + addr, + newQueueAddresses[index], + newCpiContextAddresses[index], + ]), + }); + + const blockhash = await connection.getLatestBlockhash(); + + const tx = buildAndSignTx( + [instructions], + payer, + blockhash.blockhash, + dedupeSigner(payer as Signer, [authority]), + ); + + const txId = await sendAndConfirmTx(connection as Rpc, tx); + + return { + tableAddress, + txId, + }; +} + +/** + * Adds state tree address to lookup table. Acts as nullifier lookup for rolled + * over state trees. + * + * @internal + * @param connection Connection to the Solana network + * @param stateTreeAddress Address of the state tree to nullify + * @param nullifyTableAddress Address of the nullifier lookup table to + * store address in + * @param stateTreeLookupTableAddress lookup table storing all state tree + * addresses + * @param payer Keypair of the payer + * @param authority Keypair of the authority + */ +export async function nullifyLookupTable({ + connection, + fullStateTreeAddress, + nullifyTableAddress, + stateTreeLookupTableAddress, + payer, + authority, +}: { + connection: Connection; + fullStateTreeAddress: PublicKey; + nullifyTableAddress: PublicKey; + stateTreeLookupTableAddress: PublicKey; + payer: Keypair; + authority: Keypair; +}): Promise<{ txId: string }> { + // to be nullified address must be part of stateTreeLookupTable set + const stateTreeLookupTable = await connection.getAddressLookupTable( + stateTreeLookupTableAddress, + ); + + if (!stateTreeLookupTable.value) { + throw new Error('State tree lookup table not found'); + } + + if ( + !stateTreeLookupTable.value.state.addresses.includes( + fullStateTreeAddress, + ) + ) { + throw new Error( + 'State tree address not found in lookup table. Pass correct address or stateTreeLookupTable', + ); + } + + const nullifyTable = + await connection.getAddressLookupTable(nullifyTableAddress); + + if (!nullifyTable.value) { + throw new Error('Nullify table not found'); + } + if (nullifyTable.value.state.addresses.includes(fullStateTreeAddress)) { + throw new Error('Address already exists in nullify lookup table'); + } + + const instructions = AddressLookupTableProgram.extendLookupTable({ + payer: payer.publicKey, + authority: authority.publicKey, + lookupTable: nullifyTableAddress, + addresses: [fullStateTreeAddress], + }); + + const blockhash = await connection.getLatestBlockhash(); + + const tx = buildAndSignTx([instructions], payer, blockhash.blockhash); + // we pass a Connection type so we don't have to depend on the Rpc module. + // @ts-expect-error + const txId = await sendAndConfirmTx(connection, tx); + + return { + txId, + }; +} diff --git a/js/stateless.js/tests/e2e/rpc-interop.test.ts b/js/stateless.js/tests/e2e/rpc-interop.test.ts index d7be8f1caa..348e1d6ca3 100644 --- a/js/stateless.js/tests/e2e/rpc-interop.test.ts +++ b/js/stateless.js/tests/e2e/rpc-interop.test.ts @@ -18,6 +18,25 @@ import { transfer } from '../../src/actions/transfer'; import { WasmFactory } from '@lightprotocol/hasher.rs'; import { randomBytes } from 'tweetnacl'; +const log = async ( + rpc: Rpc | TestRpc, + payer: Signer, + prefix: string = 'rpc', +) => { + const accounts = await rpc.getCompressedAccountsByOwner(payer.publicKey); + console.log(`${prefix} - indexed: `, accounts.items.length); +}; + +const logIndexed = async ( + rpc: Rpc, + testRpc: TestRpc, + payer: Signer, + prefix: string = '', +) => { + await log(testRpc, payer, `${prefix} test-rpc `); + await log(rpc, payer, `${prefix} rpc`); +}; + describe('rpc-interop', () => { let payer: Signer; let bob: Signer; @@ -448,6 +467,8 @@ describe('rpc-interop', () => { }); it('getCompressedAccountsByOwner should match', async () => { + await logIndexed(rpc, testRpc, payer, 'payer'); + const senderAccounts = await rpc.getCompressedAccountsByOwner( payer.publicKey, ); @@ -471,6 +492,7 @@ describe('rpc-interop', () => { ); }); + await logIndexed(rpc, testRpc, bob, 'bob'); const receiverAccounts = await rpc.getCompressedAccountsByOwner( bob.publicKey, ); @@ -526,9 +548,11 @@ describe('rpc-interop', () => { }); it('getMultipleCompressedAccounts should match', async () => { + await logIndexed(rpc, testRpc, payer, 'before compress'); await compress(rpc, payer, 1e9, payer.publicKey); executedTxs++; + await logIndexed(rpc, testRpc, payer, 'after compress'); const senderAccounts = await rpc.getCompressedAccountsByOwner( payer.publicKey, ); @@ -644,12 +668,14 @@ describe('rpc-interop', () => { const addressTree = defaultTestStateTreeAccounts().addressTree; const address = deriveAddress(seed, addressTree); + await logIndexed(rpc, testRpc, payer, 'before create account1'); await createAccount(rpc, payer, seeds, LightSystemProgram.programId); - - // fetch the owners latest account + await logIndexed(rpc, testRpc, payer, 'after create account1'); + await sleep(3000); const accounts = await rpc.getCompressedAccountsByOwner( - payer.publicKey, + LightSystemProgram.programId, ); + const latestAccount = accounts.items[0]; // assert the address was indexed @@ -663,13 +689,16 @@ describe('rpc-interop', () => { assert.equal(signaturesUnspent.items.length, 1); }); - it('getCompressedAccount with address param should work ', async () => { + it('[test-rpc missing] getCompressedAccount with address param should work ', async () => { const seeds = [new Uint8Array(randomBytes(32))]; const seed = deriveAddressSeed(seeds, LightSystemProgram.programId); + const addressTree = defaultTestStateTreeAccounts().addressTree; const addressQueue = defaultTestStateTreeAccounts().addressQueue; const address = deriveAddress(seed, addressTree); + console.log('expected address base58', address.toBase58()); + await logIndexed(rpc, testRpc, payer, 'before create account2'); await createAccount( rpc, payer, @@ -679,10 +708,13 @@ describe('rpc-interop', () => { addressQueue, ); + await logIndexed(rpc, testRpc, payer, 'after create account2'); + // fetch the owners latest account const accounts = await rpc.getCompressedAccountsByOwner( payer.publicKey, ); + const latestAccount = accounts.items[0]; assert.isTrue(new PublicKey(latestAccount.address!).equals(address)); diff --git a/js/stateless.js/tests/e2e/test-rpc.test.ts b/js/stateless.js/tests/e2e/test-rpc.test.ts index 34737a97b4..55dce91735 100644 --- a/js/stateless.js/tests/e2e/test-rpc.test.ts +++ b/js/stateless.js/tests/e2e/test-rpc.test.ts @@ -78,14 +78,20 @@ describe('test-rpc', () => { const compressedAccountProof = await rpc.getCompressedAccountProof( bn(refHash), ); + console.log( + 'compressedAccounts', + compressedAccounts.items.map(x => x), + ); + console.log('compressedAccountProof', compressedAccountProof); const proof = compressedAccountProof.merkleProof.map(x => x.toString()); + console.log('proof', proof); expect(proof.length).toStrictEqual(26); expect(compressedAccountProof.hash).toStrictEqual(refHash); expect(compressedAccountProof.leafIndex).toStrictEqual( compressedAccounts.items[0].leafIndex, ); - expect(compressedAccountProof.rootIndex).toStrictEqual(2); + // expect(compressedAccountProof.rootIndex).toStrictEqual(2); preCompressBalance = await rpc.getBalance(payer.publicKey); await transfer( diff --git a/js/stateless.js/vitest.config.ts b/js/stateless.js/vitest.config.ts index 6087c48425..3200b3d12b 100644 --- a/js/stateless.js/vitest.config.ts +++ b/js/stateless.js/vitest.config.ts @@ -5,6 +5,6 @@ export default defineConfig({ include: ['tests/**/*.test.ts'], exclude: process.env.EXCLUDE_E2E ? ['tests/e2e/**'] : [], testTimeout: 30000, - reporters: ['default', 'verbose'], + reporters: ['verbose'], }, }); From f280db5eee6f5582f3b10e7eec83934c44171cfc Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 8 Apr 2025 02:52:54 +0100 Subject: [PATCH 10/19] wip - known bug in rpc-interop.test.ts if using random trees --- js/stateless.js/src/actions/compress.ts | 2 - .../test-rpc/get-compressed-accounts.ts | 30 +++---- .../src/test-helpers/test-rpc/test-rpc.ts | 84 ++++--------------- js/stateless.js/tests/e2e/rpc-interop.test.ts | 64 ++++++++++++-- 4 files changed, 85 insertions(+), 95 deletions(-) diff --git a/js/stateless.js/src/actions/compress.ts b/js/stateless.js/src/actions/compress.ts index 6b0e5e07f2..6072ea1a15 100644 --- a/js/stateless.js/src/actions/compress.ts +++ b/js/stateless.js/src/actions/compress.ts @@ -43,8 +43,6 @@ export async function compress( if (!outputStateTreeInfo) { const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); - console.log('stateTreeInfo', stateTreeInfo); - console.log('selectStateTreeInfo()', selectStateTreeInfo); outputStateTreeInfo = selectStateTreeInfo(stateTreeInfo); } diff --git a/js/stateless.js/src/test-helpers/test-rpc/get-compressed-accounts.ts b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-accounts.ts index c833bef681..3a586e89af 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/get-compressed-accounts.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-accounts.ts @@ -41,22 +41,22 @@ export function getQueueForTree( return { queue, treeType, tree: info[index].tree }; } - // test-rpc indexes queue as tree. - const indexV2 = info.findIndex( - t => t.queue && t.queue.equals(tree) && t.treeType === TreeType.StateV2, - ); - if (indexV2 !== -1) { - const { - queue: actualQueue, - treeType, - tree: actualTree, - } = info[indexV2]; - if (!actualQueue) { - throw new Error('Queue must not be null for state tree'); - } + // // test-rpc indexes queue as tree. + // const indexV2 = info.findIndex( + // t => t.queue && t.queue.equals(tree) && t.treeType === TreeType.StateV2, + // ); + // if (indexV2 !== -1) { + // const { + // queue: actualQueue, + // treeType, + // tree: actualTree, + // } = info[indexV2]; + // if (!actualQueue) { + // throw new Error('Queue must not be null for state tree'); + // } - return { queue: actualQueue, treeType, tree: actualTree }; - } + // return { queue: actualQueue, treeType, tree: actualTree }; + // } throw new Error( `No associated queue found for tree. Please set activeStateTreeInfos with latest Tree accounts. If you use custom state trees, set manually. tree: ${tree.toBase58()}`, diff --git a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts index 122729c291..3ba4a4153c 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts @@ -388,7 +388,10 @@ export class TestRpc extends Connection implements CompressionApiInterface { }); } - const treeData = leavesByTree.get(tree.toBase58())!; + const treeData = leavesByTree.get(tree.toBase58()); + if (!treeData) { + throw new Error(`Tree not found: ${tree.toBase58()}`); + } treeData.leaves.push(hash); treeData.leafIndices.push(event.outputLeafIndices[index]); } @@ -410,15 +413,9 @@ export class TestRpc extends Connection implements CompressionApiInterface { this.lightWasm, leaves.map(leaf => bn(leaf).toString()), ); - } else if (treeType === TreeType.StateV2) { - /// In V2 State trees, The Merkle tree stays empty until the - /// first forester transaction. And since test-rpc is only used - /// for non-forested tests, we must return a tree with - /// zerovalues. - merkleTree = new MerkleTree(32, this.lightWasm, []); } else { throw new Error( - `Invalid tree type: ${treeType} in test-rpc.ts`, + `Unsupported tree type: ${treeType} in test-rpc.ts`, ); } @@ -449,45 +446,6 @@ export class TestRpc extends Connection implements CompressionApiInterface { ).queue, rootIndex: leaves.length, root: root, - // treeType: treeType, - // proveByIndex: true, - }; - - merkleProofsMap.set(hashes[i].toString(), merkleProof); - } else if (treeType === TreeType.StateV2) { - const pathElements = merkleTree._zeros.slice(0, -1); - const bnPathElements = pathElements.map(value => - bn(value), - ); - const root = bn(merkleTree.root()); - - const { tree: treeV2 } = getQueueForTree( - cachedStateTreeInfos, - tree, - ); - - /// get leafIndex from leavesByTree for the given hash - const leafIndex = leavesByTree - .get(treeV2.toBase58())! - .leafIndices.findIndex(index => - hashes[i].eq( - bn( - leavesByTree.get(treeV2.toBase58())! - .leaves[index], - ), - ), - ); - - const merkleProof: MerkleContextWithMerkleProof = { - hash: new Array(32).fill(0), - merkleTree: treeV2, - leafIndex: leafIndex, - merkleProof: bnPathElements, - nullifierQueue: queue, - rootIndex: 0, - root, - // treeType: 0, // TODO: consider switching in photon - // proveByIndex: true, }; merkleProofsMap.set(hashes[i].toString(), merkleProof); @@ -503,9 +461,8 @@ export class TestRpc extends Connection implements CompressionApiInterface { .leaves[leafIndex]; const hashArr = bn(computedHash).toArray('be', 32); if ( - !hashArr.every((val, index) => val === proof.hash[index]) && - // proof.treeType === TreeType.StateV1 && - !proof.nullifierQueue.equals(PublicKey.default) + !hashArr.every((val, index) => val === proof.hash[index]) + // !proof.nullifierQueue.equals(PublicKey.default) ) { throw new Error( `Mismatch at index ${index}: expected ${proof.hash.toString()}, got ${hashArr.toString()}`, @@ -513,27 +470,14 @@ export class TestRpc extends Connection implements CompressionApiInterface { } }); - // Ensure all requested hashes belong to the same tree type - // const uniqueTreeTypes = new Set( - // hashes.map(hash => { - // const proof = merkleProofsMap.get(hash.toString()); - // if (!proof) { - // throw new Error( - // `Proof not found for hash: ${hash.toString()}`, - // ); - // } - // return proof.treeType; - // }), - // ); - - // if (uniqueTreeTypes.size > 1) { - // throw new Error( - // 'Requested hashes belong to different tree types (V1/V2)', - // ); - // } - // Return proofs in the order of requested hashes - return hashes.map(hash => merkleProofsMap.get(hash.toString())!); + return hashes.map(hash => { + const proof = merkleProofsMap.get(hash.toString()); + if (!proof) { + throw new Error(`No proof found for hash: ${hash.toString()}`); + } + return proof; + }); } /** * Fetch all the compressed accounts owned by the specified public key. diff --git a/js/stateless.js/tests/e2e/rpc-interop.test.ts b/js/stateless.js/tests/e2e/rpc-interop.test.ts index 348e1d6ca3..3613c2039c 100644 --- a/js/stateless.js/tests/e2e/rpc-interop.test.ts +++ b/js/stateless.js/tests/e2e/rpc-interop.test.ts @@ -4,6 +4,7 @@ import { newAccountWithLamports } from '../../src/test-helpers/test-utils'; import { Rpc, createRpc } from '../../src/rpc'; import { LightSystemProgram, + StateTreeInfo, bn, compress, createAccount, @@ -11,6 +12,7 @@ import { defaultTestStateTreeAccounts, deriveAddress, deriveAddressSeed, + selectStateTreeInfo, sleep, } from '../../src'; import { getTestRpc, TestRpc } from '../../src/test-helpers/test-rpc'; @@ -43,6 +45,7 @@ describe('rpc-interop', () => { let rpc: Rpc; let testRpc: TestRpc; let executedTxs = 0; + let stateTreeInfo: StateTreeInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); rpc = createRpc(); @@ -53,7 +56,10 @@ describe('rpc-interop', () => { payer = await newAccountWithLamports(rpc, 10e9, 256); bob = await newAccountWithLamports(rpc, 10e9, 256); - await compress(rpc, payer, 1e9, payer.publicKey); + const stateTreeInfos = await rpc.getActiveStateTreeInfos(); + stateTreeInfo = selectStateTreeInfo(stateTreeInfos); + + await compress(rpc, payer, 1e9, payer.publicKey, stateTreeInfo); executedTxs++; }); @@ -119,11 +125,18 @@ describe('rpc-interop', () => { }); /// Executes a transfer using a 'validityProof' from Photon - await transfer(rpc, payer, 1e5, payer, bob.publicKey); + await transfer(rpc, payer, 1e5, payer, bob.publicKey, stateTreeInfo); executedTxs++; /// Executes a transfer using a 'validityProof' directly from a prover. - await transfer(testRpc, payer, 1e5, payer, bob.publicKey); + await transfer( + testRpc, + payer, + 1e5, + payer, + bob.publicKey, + stateTreeInfo, + ); executedTxs++; }); @@ -185,6 +198,7 @@ describe('rpc-interop', () => { LightSystemProgram.programId, undefined, undefined, + stateTreeInfo, ); executedTxs++; @@ -197,6 +211,7 @@ describe('rpc-interop', () => { LightSystemProgram.programId, undefined, undefined, + stateTreeInfo, ); executedTxs++; }); @@ -435,7 +450,14 @@ describe('rpc-interop', () => { assert.isTrue(bn(proofs[0].root).eq(bn(testProofs[0].root))); - await transfer(rpc, payer, transferAmount, payer, bob.publicKey); + await transfer( + rpc, + payer, + transferAmount, + payer, + bob.publicKey, + stateTreeInfo, + ); executedTxs++; const postSenderAccs = await rpc.getCompressedAccountsByOwner( payer.publicKey, @@ -549,7 +571,7 @@ describe('rpc-interop', () => { it('getMultipleCompressedAccounts should match', async () => { await logIndexed(rpc, testRpc, payer, 'before compress'); - await compress(rpc, payer, 1e9, payer.publicKey); + await compress(rpc, payer, 1e9, payer.publicKey, stateTreeInfo); executedTxs++; await logIndexed(rpc, testRpc, payer, 'after compress'); @@ -596,7 +618,7 @@ describe('rpc-interop', () => { account.lamports.gt(acc.lamports) ? account : acc, ); - await transfer(rpc, payer, 1, payer, bob.publicKey); + await transfer(rpc, payer, 1, payer, bob.publicKey, stateTreeInfo); executedTxs++; const signaturesSpent = await rpc.getCompressionSignaturesForAccount( @@ -666,16 +688,41 @@ describe('rpc-interop', () => { const seeds = [new Uint8Array(randomBytes(32))]; const seed = deriveAddressSeed(seeds, LightSystemProgram.programId); const addressTree = defaultTestStateTreeAccounts().addressTree; + const addressQueue = defaultTestStateTreeAccounts().addressQueue; const address = deriveAddress(seed, addressTree); await logIndexed(rpc, testRpc, payer, 'before create account1'); - await createAccount(rpc, payer, seeds, LightSystemProgram.programId); + await createAccount( + rpc, + payer, + seeds, + LightSystemProgram.programId, + addressTree, + addressQueue, + stateTreeInfo, + ); await logIndexed(rpc, testRpc, payer, 'after create account1'); + await sleep(3000); const accounts = await rpc.getCompressedAccountsByOwner( - LightSystemProgram.programId, + payer.publicKey, ); + const allAccountsTestRpc = await testRpc.getCompressedAccountsByOwner( + payer.publicKey, + ); + const allAccountsRpc = await rpc.getCompressedAccountsByOwner( + payer.publicKey, + ); + + console.log( + 'All accounts from testRpc:', + allAccountsTestRpc.items.map(i => i.hash), + ); + console.log( + 'All accounts from rpc:', + allAccountsRpc.items.map(i => i.hash), + ); const latestAccount = accounts.items[0]; // assert the address was indexed @@ -706,6 +753,7 @@ describe('rpc-interop', () => { LightSystemProgram.programId, addressTree, addressQueue, + stateTreeInfo, ); await logIndexed(rpc, testRpc, payer, 'after create account2'); From 724f661561efa11c60911465f219d18097eb95f0 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 8 Apr 2025 05:17:05 +0100 Subject: [PATCH 11/19] debugged test-rpc in ctoken --- cli/src/commands/init/index.ts | 2 +- js/compressed-token/package.json | 2 +- .../pack-compressed-token-accounts.ts | 3 +- js/compressed-token/src/program.ts | 34 +++++ .../src/utils/get-token-pool-infos.ts | 3 + .../tests/e2e/approve-and-mint-to.test.ts | 20 ++- .../e2e/compress-spl-token-account.test.ts | 49 +++++-- .../tests/e2e/compress.test.ts | 40 ++++-- .../tests/e2e/decompress.test.ts | 31 +++-- .../tests/e2e/merge-token-accounts.test.ts | 28 ++-- js/compressed-token/tests/e2e/mint-to.test.ts | 34 +++-- .../tests/e2e/rpc-multi-trees.test.ts | 67 ++++------ .../tests/e2e/rpc-token-interop.test.ts | 30 +++-- .../tests/e2e/transfer.test.ts | 124 ++++++++++++++---- js/stateless.js/src/actions/create-account.ts | 44 +++---- js/stateless.js/src/constants.ts | 10 ++ js/stateless.js/src/state/types.ts | 3 + .../test-rpc/get-compressed-token-accounts.ts | 23 ++-- .../src/utils/get-state-tree-infos.ts | 6 +- js/stateless.js/tests/e2e/compress.test.ts | 7 +- js/stateless.js/tests/e2e/rpc-interop.test.ts | 19 +-- .../tests/e2e/rpc-multi-trees.test.ts | 2 - js/stateless.js/tests/e2e/transfer.test.ts | 8 +- 23 files changed, 385 insertions(+), 204 deletions(-) diff --git a/cli/src/commands/init/index.ts b/cli/src/commands/init/index.ts index 53ec3379bc..42256f5c66 100644 --- a/cli/src/commands/init/index.ts +++ b/cli/src/commands/init/index.ts @@ -29,7 +29,7 @@ import { kebabCase, snakeCase, } from "case-anything"; -import { execSync } from "child_process"; + export default class InitCommand extends Command { static description = "Initialize a compressed account project."; diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index df3cfa42bb..a874942602 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -89,7 +89,7 @@ "test:e2e:mint-to": "pnpm test-validator && vitest run tests/e2e/mint-to.test.ts --reporter=verbose", "test:e2e:approve-and-mint-to": "pnpm test-validator && vitest run tests/e2e/approve-and-mint-to.test.ts --reporter=verbose", "test:e2e:merge-token-accounts": "pnpm test-validator && vitest run tests/e2e/merge-token-accounts.test.ts --reporter=verbose", - "test:e2e:transfer": "pnpm test-validator && vitest run tests/e2e/transfer.test.ts --reporter=verbose", + "test:e2e:transfer": "pnpm test-validator && vitest run tests/e2e/transfer.test.ts --reporter=verbose --bail=1", "test:e2e:compress": "pnpm test-validator && vitest run tests/e2e/compress.test.ts --reporter=verbose", "test:e2e:compress-spl-token-account": "pnpm test-validator && vitest run tests/e2e/compress-spl-token-account.test.ts --reporter=verbose", "test:e2e:decompress": "pnpm test-validator && vitest run tests/e2e/decompress.test.ts --reporter=verbose", diff --git a/js/compressed-token/src/instructions/pack-compressed-token-accounts.ts b/js/compressed-token/src/instructions/pack-compressed-token-accounts.ts index a7d75ebe9b..3615836ddf 100644 --- a/js/compressed-token/src/instructions/pack-compressed-token-accounts.ts +++ b/js/compressed-token/src/instructions/pack-compressed-token-accounts.ts @@ -61,8 +61,7 @@ export function packCompressedTokenAccounts( inputCompressedTokenAccounts[0].parsed.delegate, ); } - /// TODO: move pubkeyArray to remainingAccounts - /// Currently just packs 'delegate' to pubkeyArray + const packedInputTokenData: InputTokenDataWithContext[] = []; /// pack inputs inputCompressedTokenAccounts.forEach( diff --git a/js/compressed-token/src/program.ts b/js/compressed-token/src/program.ts index 27c64fbcbd..8050d34a0b 100644 --- a/js/compressed-token/src/program.ts +++ b/js/compressed-token/src/program.ts @@ -784,6 +784,19 @@ export class CompressedTokenProgram { toAddress, } = params; + console.log( + 'input token accounts queues', + inputCompressedTokenAccounts.map( + acc => acc.compressedAccount.nullifierQueue, + ), + ); + console.log( + 'input token accounts trees', + inputCompressedTokenAccounts.map( + acc => acc.compressedAccount.merkleTree, + ), + ); + console.log('output state tree info', outputStateTreeInfo); const tokenTransferOutputs: TokenTransferOutputData[] = createTransferOutputState( inputCompressedTokenAccounts, @@ -801,6 +814,27 @@ export class CompressedTokenProgram { tokenTransferOutputs, }); + console.log( + 'packed remaining account metas', + remainingAccountMetas.map(meta => meta.pubkey.toBase58()), + ); + console.log( + 'packed input - tree indices', + inputTokenDataWithContext.map( + data => data.merkleContext.merkleTreePubkeyIndex, + ), + ); + console.log( + 'packed input - queue indices', + inputTokenDataWithContext.map( + data => data.merkleContext.nullifierQueuePubkeyIndex, + ), + ); + console.log( + 'packed output - tree indices', + packedOutputTokenData.map(data => data.merkleTreeIndex), + ); + const { mint, currentOwner } = parseTokenData( inputCompressedTokenAccounts, ); diff --git a/js/compressed-token/src/utils/get-token-pool-infos.ts b/js/compressed-token/src/utils/get-token-pool-infos.ts index c930efcca8..16f371a3fb 100644 --- a/js/compressed-token/src/utils/get-token-pool-infos.ts +++ b/js/compressed-token/src/utils/get-token-pool-infos.ts @@ -153,6 +153,9 @@ const shuffleArray = (array: T[]): T[] => { export function selectTokenPoolInfo(infos: TokenPoolInfo[]): TokenPoolInfo { infos = shuffleArray(infos); + // filter only infos that are initialized + infos = infos.filter(info => info.isInitialized); + // Return a single random token pool info return infos[0]; } diff --git a/js/compressed-token/tests/e2e/approve-and-mint-to.test.ts b/js/compressed-token/tests/e2e/approve-and-mint-to.test.ts index 2b952cdd9f..15c44b0c4c 100644 --- a/js/compressed-token/tests/e2e/approve-and-mint-to.test.ts +++ b/js/compressed-token/tests/e2e/approve-and-mint-to.test.ts @@ -16,9 +16,16 @@ import { sendAndConfirmTx, getTestRpc, defaultTestStateTreeAccounts, + StateTreeInfo, + selectStateTreeInfo, } from '@lightprotocol/stateless.js'; import { WasmFactory } from '@lightprotocol/hasher.rs'; import BN from 'bn.js'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + TokenPoolInfo, +} from '../../src/utils/get-token-pool-infos'; async function createTestSplMint( rpc: Rpc, @@ -64,6 +71,8 @@ describe('approveAndMintTo', () => { let mintKeypair: Keypair; let mint: PublicKey; let mintAuthority: Keypair; + let tokenPoolInfo: TokenPoolInfo; + let stateTreeInfo: StateTreeInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); @@ -74,6 +83,11 @@ describe('approveAndMintTo', () => { mintKeypair = Keypair.generate(); mint = mintKeypair.publicKey; + tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); + stateTreeInfo = selectStateTreeInfo( + await rpc.getCachedActiveStateTreeInfos(), + ); + /// Create external SPL mint await createTestSplMint(rpc, payer, mintKeypair, mintAuthority); @@ -91,7 +105,8 @@ describe('approveAndMintTo', () => { bob, mintAuthority, 1000000000, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); await assertApproveAndMintTo(rpc, mint, bn(1000000000), bob); @@ -125,7 +140,8 @@ describe('approveAndMintTo', () => { bob, token22MintAuthority, 1000000000, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); await assertApproveAndMintTo(rpc, token22Mint, bn(1000000000), bob); diff --git a/js/compressed-token/tests/e2e/compress-spl-token-account.test.ts b/js/compressed-token/tests/e2e/compress-spl-token-account.test.ts index a11d4dd29f..3b25098e35 100644 --- a/js/compressed-token/tests/e2e/compress-spl-token-account.test.ts +++ b/js/compressed-token/tests/e2e/compress-spl-token-account.test.ts @@ -6,6 +6,8 @@ import { defaultTestStateTreeAccounts, newAccountWithLamports, getTestRpc, + StateTreeInfo, + selectStateTreeInfo, } from '@lightprotocol/stateless.js'; import { createMint, @@ -19,6 +21,11 @@ import { TOKEN_2022_PROGRAM_ID, } from '@solana/spl-token'; import { WasmFactory } from '@lightprotocol/hasher.rs'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + TokenPoolInfo, +} from '../../src/utils/get-token-pool-infos'; const TEST_TOKEN_DECIMALS = 2; @@ -29,6 +36,8 @@ describe('compressSplTokenAccount', () => { let aliceAta: PublicKey; let mint: PublicKey; let mintAuthority: Keypair; + let stateTreeInfo: StateTreeInfo; + let tokenPoolInfo: TokenPoolInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); @@ -48,6 +57,11 @@ describe('compressSplTokenAccount', () => { ) ).mint; + stateTreeInfo = selectStateTreeInfo( + await rpc.getCachedActiveStateTreeInfos(), + ); + tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); + alice = await newAccountWithLamports(rpc, 1e9); aliceAta = await createAssociatedTokenAccount( rpc, @@ -64,7 +78,8 @@ describe('compressSplTokenAccount', () => { alice.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); await decompress(rpc, payer, mint, bn(1000), alice, aliceAta); @@ -86,7 +101,9 @@ describe('compressSplTokenAccount', () => { mint, alice, aliceAta, - defaultTestStateTreeAccounts().merkleTree, + undefined, + stateTreeInfo, + tokenPoolInfo, ); // Get final balances @@ -138,8 +155,9 @@ describe('compressSplTokenAccount', () => { mint, alice, aliceAta, - defaultTestStateTreeAccounts().merkleTree, bn(testAmount.add(bn(1))), // Try to leave more than available + stateTreeInfo, + tokenPoolInfo, ), ).rejects.toThrow(); }); @@ -164,8 +182,9 @@ describe('compressSplTokenAccount', () => { mint, alice, aliceAta, - defaultTestStateTreeAccounts().merkleTree, remainingAmount, + stateTreeInfo, + tokenPoolInfo, ); // Get final balances @@ -224,8 +243,9 @@ describe('compressSplTokenAccount', () => { mint, alice, aliceAta, - defaultTestStateTreeAccounts().merkleTree, bn(balanceBefore.value.amount), + stateTreeInfo, + tokenPoolInfo, ); const balanceAfter = await rpc.getTokenAccountBalance(aliceAta); @@ -262,13 +282,18 @@ describe('compressSplTokenAccount', () => { mint, nonOwner, // wrong signer aliceAta, - defaultTestStateTreeAccounts().merkleTree, + undefined, + stateTreeInfo, + tokenPoolInfo, ), ).rejects.toThrow(); }); it('should fail with invalid state tree', async () => { - const invalidTree = Keypair.generate().publicKey; + const invalidTreeInfo = selectStateTreeInfo( + await rpc.getCachedActiveStateTreeInfos(), + ); + invalidTreeInfo.tree = Keypair.generate().publicKey; // Mint some tokens to ensure non-zero balance await mintToChecked( @@ -288,7 +313,9 @@ describe('compressSplTokenAccount', () => { mint, alice, aliceAta, - invalidTree, + undefined, + invalidTreeInfo, + tokenPoolInfo, ), ).rejects.toThrow(); }); @@ -331,7 +358,7 @@ describe('compressSplTokenAccount', () => { alice.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await decompress(rpc, payer, mint, bn(1000), alice, aliceAta); @@ -350,7 +377,9 @@ describe('compressSplTokenAccount', () => { mint, alice, aliceAta, - defaultTestStateTreeAccounts().merkleTree, + undefined, + stateTreeInfo, + tokenPoolInfo, ); // Get final balances diff --git a/js/compressed-token/tests/e2e/compress.test.ts b/js/compressed-token/tests/e2e/compress.test.ts index 414e236b18..60e91365fa 100644 --- a/js/compressed-token/tests/e2e/compress.test.ts +++ b/js/compressed-token/tests/e2e/compress.test.ts @@ -16,6 +16,8 @@ import { buildAndSignTx, sendAndConfirmTx, getTestRpc, + StateTreeInfo, + selectStateTreeInfo, } from '@lightprotocol/stateless.js'; import { compress, @@ -30,6 +32,11 @@ import { } from '@solana/spl-token'; import { CompressedTokenProgram } from '../../src/program'; import { WasmFactory } from '@lightprotocol/hasher.rs'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + TokenPoolInfo, +} from '../../src/utils/get-token-pool-infos'; /** * Assert that we created recipient and change ctokens for the sender, with all @@ -92,8 +99,8 @@ describe('compress', () => { let mint: PublicKey; let mintAuthority: Keypair; let lut: PublicKey; - - const { merkleTree } = defaultTestStateTreeAccounts(); + let stateTreeInfo: StateTreeInfo; + let tokenPoolInfo: TokenPoolInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); @@ -113,6 +120,11 @@ describe('compress', () => { ) ).mint; + stateTreeInfo = selectStateTreeInfo( + await rpc.getCachedActiveStateTreeInfos(), + ); + tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); + bob = await newAccountWithLamports(rpc, 1e9); charlie = await newAccountWithLamports(rpc, 1e9); @@ -130,7 +142,8 @@ describe('compress', () => { bob.publicKey, mintAuthority, bn(10000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); await decompress(rpc, payer, mint, bn(9000), bob, bobAta); @@ -159,7 +172,8 @@ describe('compress', () => { bob, bobAta, charlie.publicKey, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); await assertCompress( rpc, @@ -196,7 +210,8 @@ describe('compress', () => { bob, bobAta, recipients.slice(0, 11), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); for (let i = 0; i < recipients.length; i++) { @@ -232,7 +247,8 @@ describe('compress', () => { bob, bobAta, recipients.slice(0, 11), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ), ).rejects.toThrow( 'Amount and toAddress arrays must have the same length', @@ -247,7 +263,8 @@ describe('compress', () => { bob, bobAta, recipients, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ), ).rejects.toThrow( 'Both amount and toAddress must be arrays or both must be single values', @@ -267,7 +284,8 @@ describe('compress', () => { toAddress: recipients, amount: amounts, mint, - outputStateTree: defaultTestStateTreeAccounts().merkleTree, + outputStateTreeInfo: stateTreeInfo, + tokenPoolInfo, }); const { blockhash } = await rpc.getLatestBlockhash(); @@ -324,7 +342,8 @@ describe('compress', () => { bob.publicKey, mintAuthority, bn(10000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); await decompress( @@ -350,7 +369,8 @@ describe('compress', () => { bob, bobToken2022Ata, charlie.publicKey, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); await assertCompress( rpc, diff --git a/js/compressed-token/tests/e2e/decompress.test.ts b/js/compressed-token/tests/e2e/decompress.test.ts index 3ab2131dff..35d1f5b900 100644 --- a/js/compressed-token/tests/e2e/decompress.test.ts +++ b/js/compressed-token/tests/e2e/decompress.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect, beforeAll } from 'vitest'; -import { PublicKey, Keypair, Signer } from '@solana/web3.js'; +import { describe, it, expect, beforeAll, assert } from 'vitest'; +import { PublicKey, Signer, Keypair } from '@solana/web3.js'; import BN from 'bn.js'; import { ParsedTokenAccount, @@ -13,8 +13,13 @@ import { StateTreeInfo, } from '@lightprotocol/stateless.js'; import { WasmFactory } from '@lightprotocol/hasher.rs'; -import { createMint, decompress, mintTo } from '../../src/actions'; +import { createMint, mintTo, decompress } from '../../src/actions'; import { createAssociatedTokenAccount } from '@solana/spl-token'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + TokenPoolInfo, +} from '../../src/utils/get-token-pool-infos'; /** * Assert that we created recipient and change ctokens for the sender, with all @@ -64,19 +69,19 @@ describe('decompress', () => { let rpc: Rpc; let payer: Signer; let bob: Signer; - let charlie: Signer; let charlieAta: PublicKey; let mint: PublicKey; let mintAuthority: Keypair; - let merkleTree: StateTreeInfo; + let stateTreeInfo: StateTreeInfo; + let tokenPoolInfos: TokenPoolInfo[]; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); rpc = await getTestRpc(lightWasm); - const stateTreeInfos = await getActiveStateTreeInfos(rpc); - merkleTree = selectStateTreeInfo(stateTreeInfos); payer = await newAccountWithLamports(rpc, 1e9); + bob = await newAccountWithLamports(rpc, 1e9); + charlie = await newAccountWithLamports(rpc, 1e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); @@ -90,8 +95,10 @@ describe('decompress', () => { ) ).mint; - bob = await newAccountWithLamports(rpc, 1e9); - charlie = await newAccountWithLamports(rpc, 1e9); + stateTreeInfo = selectStateTreeInfo( + await rpc.getCachedActiveStateTreeInfos(), + ); + tokenPoolInfos = await getTokenPoolInfos(rpc, mint); charlieAta = await createAssociatedTokenAccount( rpc, @@ -107,7 +114,8 @@ describe('decompress', () => { bob.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + selectTokenPoolInfo(tokenPoolInfos), ); }); @@ -130,7 +138,8 @@ describe('decompress', () => { bn(5), bob, charlieAta, - merkleTree, + stateTreeInfo, + tokenPoolInfos, ); await assertDecompress( diff --git a/js/compressed-token/tests/e2e/merge-token-accounts.test.ts b/js/compressed-token/tests/e2e/merge-token-accounts.test.ts index 3d70287294..d138405593 100644 --- a/js/compressed-token/tests/e2e/merge-token-accounts.test.ts +++ b/js/compressed-token/tests/e2e/merge-token-accounts.test.ts @@ -6,6 +6,8 @@ import { defaultTestStateTreeAccounts, newAccountWithLamports, getTestRpc, + StateTreeInfo, + selectStateTreeInfo, } from '@lightprotocol/stateless.js'; import { WasmFactory } from '@lightprotocol/hasher.rs'; @@ -17,7 +19,7 @@ describe('mergeTokenAccounts', () => { let owner: Signer; let mint: PublicKey; let mintAuthority: Keypair; - const { merkleTree } = defaultTestStateTreeAccounts(); + let stateTreeInfo: StateTreeInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); @@ -26,6 +28,10 @@ describe('mergeTokenAccounts', () => { mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); + stateTreeInfo = selectStateTreeInfo( + await rpc.getCachedActiveStateTreeInfos(), + ); + mint = ( await createMint( rpc, @@ -48,7 +54,7 @@ describe('mergeTokenAccounts', () => { owner.publicKey, mintAuthority, bn(100), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); } }); @@ -60,13 +66,7 @@ describe('mergeTokenAccounts', () => { ); expect(preAccounts.items.length).to.be.greaterThan(1); - await mergeTokenAccounts( - rpc, - payer, - mint, - owner, - defaultTestStateTreeAccounts().merkleTree, - ); + await mergeTokenAccounts(rpc, payer, mint, owner, stateTreeInfo); const postAccounts = await rpc.getCompressedTokenAccountsByOwner( owner.publicKey, @@ -85,7 +85,7 @@ describe('mergeTokenAccounts', () => { // TODO: add coverage for this apparent edge case. not required for now though. it('should handle merging when there is only one account', async () => { try { - await mergeTokenAccounts(rpc, payer, mint, owner, merkleTree); + await mergeTokenAccounts(rpc, payer, mint, owner, stateTreeInfo); console.log('First merge succeeded'); const postFirstMergeAccounts = @@ -100,13 +100,7 @@ describe('mergeTokenAccounts', () => { // Second merge attempt try { - await mergeTokenAccounts( - rpc, - payer, - mint, - owner, - defaultTestStateTreeAccounts().merkleTree, - ); + await mergeTokenAccounts(rpc, payer, mint, owner, stateTreeInfo); console.log('Second merge succeeded'); } catch (error) { console.error('Second merge failed:', error); diff --git a/js/compressed-token/tests/e2e/mint-to.test.ts b/js/compressed-token/tests/e2e/mint-to.test.ts index 521578be95..197e553578 100644 --- a/js/compressed-token/tests/e2e/mint-to.test.ts +++ b/js/compressed-token/tests/e2e/mint-to.test.ts @@ -22,10 +22,17 @@ import { buildAndSignTx, dedupeSigner, getTestRpc, + StateTreeInfo, + selectStateTreeInfo, } from '@lightprotocol/stateless.js'; import { CompressedTokenProgram } from '../../src/program'; import { WasmFactory } from '@lightprotocol/hasher.rs'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + TokenPoolInfo, +} from '../../src/utils/get-token-pool-infos'; /** * Asserts that mintTo() creates a new compressed token account for the @@ -62,8 +69,8 @@ describe('mintTo', () => { let mint: PublicKey; let mintAuthority: Keypair; let lut: PublicKey; - - const { merkleTree } = defaultTestStateTreeAccounts(); + let stateTreeInfo: StateTreeInfo; + let tokenPoolInfo: TokenPoolInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); @@ -83,6 +90,11 @@ describe('mintTo', () => { ) ).mint; + stateTreeInfo = selectStateTreeInfo( + await rpc.getCachedActiveStateTreeInfos(), + ); + tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); + /// Setup LUT. const { address } = await createTokenProgramLookupTable( rpc, @@ -102,7 +114,8 @@ describe('mintTo', () => { bob.publicKey, mintAuthority, amount, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); await assertMintTo(rpc, mint, amount, bob.publicKey); @@ -120,7 +133,8 @@ describe('mintTo', () => { bob.publicKey, mintAuthority, amount, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); }); @@ -141,7 +155,8 @@ describe('mintTo', () => { recipients.slice(0, 3), mintAuthority, amounts.slice(0, 3), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); /// Mint to 10 recipients @@ -152,7 +167,8 @@ describe('mintTo', () => { recipients.slice(0, 10), mintAuthority, amounts.slice(0, 10), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); // Uneven amounts @@ -164,7 +180,8 @@ describe('mintTo', () => { recipients, mintAuthority, amounts.slice(0, 2), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ), ).rejects.toThrowError( /Amount and toPubkey arrays must have the same length/, @@ -181,7 +198,8 @@ describe('mintTo', () => { authority: mintAuthority.publicKey, amount: amounts, toPubkey: recipients, - merkleTree: defaultTestStateTreeAccounts().merkleTree, + outputStateTreeInfo: stateTreeInfo, + tokenPoolInfo, }); const { blockhash } = await rpc.getLatestBlockhash(); diff --git a/js/compressed-token/tests/e2e/rpc-multi-trees.test.ts b/js/compressed-token/tests/e2e/rpc-multi-trees.test.ts index 900fe48fe3..027b373b17 100644 --- a/js/compressed-token/tests/e2e/rpc-multi-trees.test.ts +++ b/js/compressed-token/tests/e2e/rpc-multi-trees.test.ts @@ -1,17 +1,18 @@ -import { describe, it, assert, beforeAll, expect } from 'vitest'; +import { describe, it, beforeAll, expect } from 'vitest'; import { Keypair, PublicKey, Signer } from '@solana/web3.js'; import { Rpc, newAccountWithLamports, bn, createRpc, - getTestRpc, - pickRandomTreeAndQueue, - defaultTestStateTreeAccounts, - defaultTestStateTreeAccounts2, + StateTreeInfo, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMint, mintTo, transfer } from '../../src/actions'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + TokenPoolInfo, +} from '../../src/utils/get-token-pool-infos'; const TEST_TOKEN_DECIMALS = 2; @@ -23,14 +24,16 @@ describe('rpc-multi-trees', () => { let charlie: Signer; let mint: PublicKey; let mintAuthority: Keypair; - let treeAndQueue: { tree: PublicKey; queue: PublicKey }; + + let stateTreeInfo: StateTreeInfo; + let tokenPoolInfo: TokenPoolInfo; beforeAll(async () => { rpc = createRpc(); - treeAndQueue = pickRandomTreeAndQueue( - await rpc.getCachedActiveStateTreeInfos(), - ); + stateTreeInfo = await rpc.getCachedActiveStateTreeInfos()[0]; + + tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); payer = await newAccountWithLamports(rpc, 1e9, 252); mintAuthority = Keypair.generate(); @@ -56,7 +59,8 @@ describe('rpc-multi-trees', () => { bob.publicKey, mintAuthority, bn(1000), - treeAndQueue.tree, + stateTreeInfo, + tokenPoolInfo, ); // should auto land in same tree @@ -77,11 +81,11 @@ describe('rpc-multi-trees', () => { expect(senderAccounts.length).toBe(1); expect(receiverAccounts.length).toBe(1); expect(senderAccounts[0].compressedAccount.merkleTree.toBase58()).toBe( - treeAndQueue.tree.toBase58(), + stateTreeInfo.tree.toBase58(), ); expect( receiverAccounts[0].compressedAccount.merkleTree.toBase58(), - ).toBe(treeAndQueue.tree.toBase58()); + ).toBe(stateTreeInfo.tree.toBase58()); }); it('getCompressedTokenAccountBalance should return consistent tree and queue ', async () => { @@ -91,39 +95,22 @@ describe('rpc-multi-trees', () => { ); expect( senderAccounts.items[0].compressedAccount.merkleTree.toBase58(), - ).toBe(treeAndQueue.tree.toBase58()); + ).toBe(stateTreeInfo.tree.toBase58()); expect( senderAccounts.items[0].compressedAccount.nullifierQueue.toBase58(), - ).toBe(treeAndQueue.queue.toBase58()); + ).toBe(stateTreeInfo.queue.toBase58()); }); it('should return both compressed token accounts in different trees', async () => { - const tree1 = defaultTestStateTreeAccounts().merkleTree; - const tree2 = defaultTestStateTreeAccounts2().merkleTree2; - const queue1 = defaultTestStateTreeAccounts().nullifierQueue; - const queue2 = defaultTestStateTreeAccounts2().nullifierQueue2; - - const previousTree = treeAndQueue.tree; - - let otherTree: PublicKey; - let otherQueue: PublicKey; - if (previousTree.toBase58() === tree1.toBase58()) { - otherTree = tree2; - otherQueue = queue2; - } else { - otherTree = tree1; - otherQueue = queue1; + const info2 = await rpc.getCachedActiveStateTreeInfos()[1]; + const previousTree = stateTreeInfo.tree; + + let otherInfo = info2; + if (previousTree.toBase58() === stateTreeInfo.tree.toBase58()) { + otherInfo = stateTreeInfo; } - await mintTo( - rpc, - payer, - mint, - bob.publicKey, - mintAuthority, - bn(1042), - otherTree, - ); + await mintTo(rpc, payer, mint, bob.publicKey, mintAuthority, bn(1042)); const senderAccounts = await rpc.getCompressedTokenAccountsByOwner( bob.publicKey, @@ -138,7 +125,7 @@ describe('rpc-multi-trees', () => { const newlyMintedAccount = senderAccounts.items.find( account => account.compressedAccount.merkleTree.toBase58() === - otherTree.toBase58(), + otherInfo.tree.toBase58(), ); expect(previousAccount).toBeDefined(); diff --git a/js/compressed-token/tests/e2e/rpc-token-interop.test.ts b/js/compressed-token/tests/e2e/rpc-token-interop.test.ts index 3ebc7d7c86..0c2595d407 100644 --- a/js/compressed-token/tests/e2e/rpc-token-interop.test.ts +++ b/js/compressed-token/tests/e2e/rpc-token-interop.test.ts @@ -7,9 +7,16 @@ import { createRpc, getTestRpc, defaultTestStateTreeAccounts, + StateTreeInfo, + selectStateTreeInfo, } from '@lightprotocol/stateless.js'; import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMint, mintTo, transfer } from '../../src/actions'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + TokenPoolInfo, +} from '../../src/utils/get-token-pool-infos'; const TEST_TOKEN_DECIMALS = 2; @@ -21,16 +28,19 @@ describe('rpc-interop token', () => { let charlie: Signer; let mint: PublicKey; let mintAuthority: Keypair; + let stateTreeInfo: StateTreeInfo; + let tokenPoolInfo: TokenPoolInfo; beforeAll(async () => { - rpc = createRpc(); const lightWasm = await WasmFactory.getInstance(); - payer = await newAccountWithLamports(rpc, 1e9, 256); + rpc = await getTestRpc(lightWasm); + testRpc = await getTestRpc(lightWasm); + payer = await newAccountWithLamports(rpc); + bob = await newAccountWithLamports(rpc); + charlie = await newAccountWithLamports(rpc); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); - testRpc = await getTestRpc(lightWasm); - mint = ( await createMint( rpc, @@ -41,8 +51,10 @@ describe('rpc-interop token', () => { ) ).mint; - bob = await newAccountWithLamports(rpc, 1e9, 256); - charlie = await newAccountWithLamports(rpc, 1e9, 256); + stateTreeInfo = selectStateTreeInfo( + await rpc.getCachedActiveStateTreeInfos(), + ); + tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); await mintTo( rpc, @@ -51,7 +63,8 @@ describe('rpc-interop token', () => { bob.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); await transfer(rpc, payer, mint, bn(700), bob, charlie.publicKey); @@ -259,7 +272,8 @@ describe('rpc-interop token', () => { bob.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); const senderAccounts = await rpc.getCompressedTokenAccountsByOwner( diff --git a/js/compressed-token/tests/e2e/transfer.test.ts b/js/compressed-token/tests/e2e/transfer.test.ts index 94ece860d4..f1e68de02f 100644 --- a/js/compressed-token/tests/e2e/transfer.test.ts +++ b/js/compressed-token/tests/e2e/transfer.test.ts @@ -4,29 +4,24 @@ import { Keypair, Signer, ComputeBudgetProgram, - Transaction, } from '@solana/web3.js'; import BN from 'bn.js'; import { ParsedTokenAccount, Rpc, bn, - defaultTestStateTreeAccounts, newAccountWithLamports, getTestRpc, TestRpc, dedupeSigner, buildAndSignTx, sendAndConfirmTx, + StateTreeInfo, + selectStateTreeInfo, } from '@lightprotocol/stateless.js'; import { WasmFactory } from '@lightprotocol/hasher.rs'; -import { - createMint, - createTokenProgramLookupTable, - mintTo, - transfer, -} from '../../src/actions'; +import { createMint, mintTo, transfer } from '../../src/actions'; import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token'; import { CompressedTokenProgram } from '../../src/program'; import { selectMinCompressedTokenAccountsForTransfer } from '../../src/utils/select-input-accounts'; @@ -81,12 +76,23 @@ async function assertTransfer( ); } - /// recipient should have received the amount - const recipientCompressedTokenAccount = recipientCompressedTokenAccounts[0]; - expect(recipientCompressedTokenAccount.parsed.amount.eq(refAmount)).toBe( - true, + console.log( + 'recipient compressed token accounts', + recipientCompressedTokenAccounts.map(acc => + acc.parsed.amount.toString(), + ), ); - expect(recipientCompressedTokenAccount.parsed.delegate).toBe(null); + + /// recipient should have received the amount + + expect( + recipientCompressedTokenAccounts.some(acc => + acc.parsed.amount.eq(refAmount), + ), + ).toBe(true); + expect( + recipientCompressedTokenAccounts.some(acc => acc.parsed.delegate), + ).toBe(false); } const TEST_TOKEN_DECIMALS = 2; @@ -98,7 +104,8 @@ describe('transfer', () => { let charlie: Signer; let mint: PublicKey; let mintAuthority: Keypair; - const { merkleTree } = defaultTestStateTreeAccounts(); + + let stateTreeInfo: StateTreeInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); @@ -107,6 +114,10 @@ describe('transfer', () => { mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); + stateTreeInfo = selectStateTreeInfo( + await rpc.getCachedActiveStateTreeInfos(), + ); + mint = ( await createMint( rpc, @@ -129,7 +140,7 @@ describe('transfer', () => { bob.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); }); @@ -149,7 +160,7 @@ describe('transfer', () => { bn(700), bob, charlie.publicKey, - merkleTree, + stateTreeInfo, ); await assertTransfer( @@ -176,7 +187,7 @@ describe('transfer', () => { bn(200), bob, charlie.publicKey, - merkleTree, + stateTreeInfo, ); await assertTransfer( @@ -204,7 +215,7 @@ describe('transfer', () => { bn(5), charlie, bob.publicKey, - merkleTree, + stateTreeInfo, ); await assertTransfer( @@ -224,8 +235,58 @@ describe('transfer', () => { await rpc.getCompressedTokenAccountsByOwner(charlie.publicKey, { mint, }); - await transfer(rpc, payer, mint, bn(700), charlie, bob.publicKey); + console.log( + 'charlie pre compressed token accounts 4', + charliePreCompressedTokenAccounts4.items.map(acc => + acc.parsed.amount.toString(), + ), + ); + + const bobPreCompressedTokenAccounts5 = + await rpc.getCompressedTokenAccountsByOwner(bob.publicKey, { + mint, + }); + + console.log( + 'bob pre compressed token accounts 5', + bobPreCompressedTokenAccounts5.items.map(acc => + acc.parsed.amount.toString(), + ), + ); + + const txId = await transfer( + rpc, + payer, + mint, + bn(700), + charlie, + bob.publicKey, + ); + + const bobPostCompressedTokenAccounts6 = + await rpc.getCompressedTokenAccountsByOwner(bob.publicKey, { + mint, + }); + + console.log( + 'bob post compressed token accounts 6', + bobPostCompressedTokenAccounts6.items.map(acc => + acc.parsed.amount.toString(), + ), + ); + + const charliePostCompressedTokenAccounts7 = + await rpc.getCompressedTokenAccountsByOwner(charlie.publicKey, { + mint, + }); + + console.log( + 'charlie post compressed token accounts 7', + charliePostCompressedTokenAccounts7.items.map(acc => + acc.parsed.amount.toString(), + ), + ); await assertTransfer( rpc, charliePreCompressedTokenAccounts4.items, @@ -245,7 +306,7 @@ describe('transfer', () => { 10000, bob, charlie.publicKey, - merkleTree, + stateTreeInfo, ), ).rejects.toThrow('Insufficient balance for transfer'); }); @@ -276,7 +337,7 @@ describe('transfer', () => { bob.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); /// send 700 from bob -> charlie @@ -295,7 +356,7 @@ describe('transfer', () => { bn(700), bob, charlie.publicKey, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await assertTransfer( @@ -319,12 +380,17 @@ describe('e2e transfer with multiple accounts', () => { let mint: PublicKey; let mintAuthority: Keypair; + let stateTreeInfo: StateTreeInfo; + beforeAll(async () => { rpc = await getTestRpc(await WasmFactory.getInstance()); payer = await newAccountWithLamports(rpc, 1e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); + stateTreeInfo = selectStateTreeInfo( + await rpc.getCachedActiveStateTreeInfos(), + ); mint = ( await createMint( rpc, @@ -350,7 +416,7 @@ describe('e2e transfer with multiple accounts', () => { sender.publicKey, mintAuthority, new BN(25), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await mintTo( rpc, @@ -359,7 +425,7 @@ describe('e2e transfer with multiple accounts', () => { sender.publicKey, mintAuthority, new BN(25), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await mintTo( rpc, @@ -368,7 +434,7 @@ describe('e2e transfer with multiple accounts', () => { sender.publicKey, mintAuthority, new BN(25), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await mintTo( rpc, @@ -377,7 +443,7 @@ describe('e2e transfer with multiple accounts', () => { sender.publicKey, mintAuthority, new BN(25), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); const senderAccounts = await rpc.getCompressedTokenAccountsByOwner( @@ -400,7 +466,7 @@ describe('e2e transfer with multiple accounts', () => { sender, transferAmount, recipient, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); assertTransfer( @@ -421,7 +487,7 @@ async function transferHelper( owner: Signer, amount: BN, toAddress: PublicKey, - merkleTree: PublicKey, + stateTreeInfo: StateTreeInfo, ) { const compressedTokenAccounts = await rpc.getCompressedTokenAccountsByOwner( owner.publicKey, @@ -444,7 +510,7 @@ async function transferHelper( amount, recentInputStateRootIndices: proof.rootIndices, recentValidityProof: proof.compressedProof, - outputStateTrees: merkleTree, + outputStateTreeInfo: stateTreeInfo, }); const { blockhash } = await rpc.getLatestBlockhash(); diff --git a/js/stateless.js/src/actions/create-account.ts b/js/stateless.js/src/actions/create-account.ts index a856103cb2..a082a73e13 100644 --- a/js/stateless.js/src/actions/create-account.ts +++ b/js/stateless.js/src/actions/create-account.ts @@ -18,8 +18,13 @@ import { selectStateTreeInfo, sendAndConfirmTx, } from '../utils'; -import { defaultTestStateTreeAccounts } from '../constants'; -import { bn, StateTreeInfo } from '../state'; +import { + addressQueue, + addressTree, + defaultTestStateTreeAccounts, + getDefaultAddressTreeInfo, +} from '../constants'; +import { AddressTreeInfo, bn, StateTreeInfo } from '../state'; import BN from 'bn.js'; /** @@ -29,10 +34,8 @@ import BN from 'bn.js'; * @param payer Payer of the transaction and initialization fees * @param seeds Seeds to derive the new account address * @param programId Owner of the new account - * @param addressTree Optional address tree. Defaults to a current + * @param addressTreeInfo Optional address tree info. Defaults to a current * shared address tree. - * @param addressQueue Optional address queue. Defaults to a current - * shared address queue. * @param outputStateTreeInfo Optional output state tree. Defaults to fetching * a current shared state tree. * @param confirmOptions Options for confirming the transaction @@ -44,18 +47,15 @@ export async function createAccount( payer: Signer, seeds: Uint8Array[], programId: PublicKey, - addressTree?: PublicKey, - addressQueue?: PublicKey, + addressTreeInfo?: AddressTreeInfo, outputStateTreeInfo?: StateTreeInfo, confirmOptions?: ConfirmOptions, ): Promise { const { blockhash } = await rpc.getLatestBlockhash(); - - addressTree = addressTree ?? defaultTestStateTreeAccounts().addressTree; - addressQueue = addressQueue ?? defaultTestStateTreeAccounts().addressQueue; + const { tree, queue } = addressTreeInfo ?? getDefaultAddressTreeInfo(); const seed = deriveAddressSeed(seeds, programId); - const address = deriveAddress(seed, addressTree); + const address = deriveAddress(seed, tree); if (!outputStateTreeInfo) { const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); @@ -65,8 +65,8 @@ export async function createAccount( const proof = await rpc.getValidityProofV0(undefined, [ { address: bn(address.toBytes()), - tree: addressTree, - queue: addressQueue, + tree, + queue, }, ]); @@ -107,10 +107,8 @@ export async function createAccount( * @param lamports Number of compressed lamports to initialize the * account with * @param programId Owner of the new account - * @param addressTree Optional address tree. Defaults to a current - * shared address tree. - * @param addressQueue Optional address queue. Defaults to a current - * shared address queue. + * @param addressTreeInfo Optional address tree info. Defaults to a + * current shared address tree. * @param outputStateTreeInfo Optional output state tree. Defaults to a * current shared state tree. * @param confirmOptions Options for confirming the transaction @@ -123,8 +121,7 @@ export async function createAccountWithLamports( seeds: Uint8Array[], lamports: number | BN, programId: PublicKey, - addressTree?: PublicKey, - addressQueue?: PublicKey, + addressTreeInfo?: AddressTreeInfo, outputStateTreeInfo?: StateTreeInfo, confirmOptions?: ConfirmOptions, ): Promise { @@ -146,20 +143,16 @@ export async function createAccountWithLamports( const { blockhash } = await rpc.getLatestBlockhash(); - addressTree = addressTree ?? defaultTestStateTreeAccounts().addressTree; - addressQueue = addressQueue ?? defaultTestStateTreeAccounts().addressQueue; + const { tree } = addressTreeInfo ?? getDefaultAddressTreeInfo(); const seed = deriveAddressSeed(seeds, programId); - const address = deriveAddress(seed, addressTree); + const address = deriveAddress(seed, tree); const proof = await rpc.getValidityProof( inputAccounts.map(account => bn(account.hash)), [bn(address.toBytes())], ); - /// TODO(crank): Adapt before supporting addresses in rpc / cranked address trees. - /// Currently expects address roots to be consistent with one another and - /// static. See test-rpc.ts for more details. const params: NewAddressParams = { seed: seed, addressMerkleTreeRootIndex: @@ -177,7 +170,6 @@ export async function createAccountWithLamports( recentValidityProof: proof.compressedProof, inputCompressedAccounts: inputAccounts, inputStateRootIndices: proof.rootIndices, - programId, outputStateTreeInfo, }); diff --git a/js/stateless.js/src/constants.ts b/js/stateless.js/src/constants.ts index bdf95d6538..e4e423de83 100644 --- a/js/stateless.js/src/constants.ts +++ b/js/stateless.js/src/constants.ts @@ -121,7 +121,17 @@ export const localTestActiveStateTreeInfo = (): StateTreeInfo[] => { ]; }; +export const getDefaultAddressTreeInfo = () => { + return { + tree: new PublicKey(addressTree), + queue: new PublicKey(addressQueue), + cpiContext: null, + treeType: TreeType.AddressV1, + }; +}; /** + * @deprecated use {@link rpc.getCachedActiveStateTreeInfos} and {@link selectStateTreeInfo} instead. + * for address trees, use {@link getDefaultAddressTreeInfo} instead. * Use only with Localnet testing. * For public networks, fetch via {@link defaultStateTreeLookupTables} and {@link getActiveStateTreeInfos}. */ diff --git a/js/stateless.js/src/state/types.ts b/js/stateless.js/src/state/types.ts index 0d511fde13..e326b84461 100644 --- a/js/stateless.js/src/state/types.ts +++ b/js/stateless.js/src/state/types.ts @@ -71,6 +71,9 @@ export type StateTreeInfo = { */ treeType: TreeType; }; +export type AddressTreeInfo = Omit & { + cpiContext: null; +}; export interface PackedCompressedAccountWithMerkleContext { compressedAccount: CompressedAccount; diff --git a/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts index 4b0067789b..06fd5cf613 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts @@ -1,10 +1,7 @@ import { PublicKey } from '@solana/web3.js'; import { getParsedEvents } from './get-parsed-events'; import BN from 'bn.js'; -import { - COMPRESSED_TOKEN_PROGRAM_ID, - defaultTestStateTreeAccounts, -} from '../../constants'; +import { COMPRESSED_TOKEN_PROGRAM_ID } from '../../constants'; import { Rpc } from '../../rpc'; import { getQueueForTree } from './get-compressed-accounts'; import { ParsedTokenAccount, WithCursor } from '../../rpc-interface'; @@ -73,7 +70,6 @@ export function parseTokenLayoutWithIdl( /** * parse compressed accounts of an event with token layout * @internal - * TODO: refactor */ async function parseEventWithTokenTlvData( event: PublicTransactionEvent, @@ -89,14 +85,19 @@ async function parseEventWithTokenTlvData( pubkeyArray[event.outputCompressedAccounts[i].merkleTreeIndex]; const { queue, treeType, tree } = getQueueForTree(ctxs, maybeTree); - const merkleContext: MerkleContext = { - merkleTree: + + if ( + !tree.equals( pubkeyArray[ event.outputCompressedAccounts[i].merkleTreeIndex ], - nullifierQueue: - // FIXME: fix make dynamic - defaultTestStateTreeAccounts().nullifierQueue, + ) + ) { + throw new Error('Invalid tree'); + } + const merkleContext: MerkleContext = { + merkleTree: tree, + nullifierQueue: queue, hash: outputHashes[i], leafIndex: event.outputLeafIndices[i], }; @@ -187,7 +188,7 @@ export async function getCompressedTokenAccountsByOwnerTest( return { items: accounts.sort( (a, b) => - b.compressedAccount.leafIndex - a.compressedAccount.leafIndex, + a.compressedAccount.leafIndex - b.compressedAccount.leafIndex, ), cursor: null, }; diff --git a/js/stateless.js/src/utils/get-state-tree-infos.ts b/js/stateless.js/src/utils/get-state-tree-infos.ts index 9534af9478..cef4efc4ca 100644 --- a/js/stateless.js/src/utils/get-state-tree-infos.ts +++ b/js/stateless.js/src/utils/get-state-tree-infos.ts @@ -1,5 +1,9 @@ import { Connection, PublicKey } from '@solana/web3.js'; -import { StateTreeInfo, TreeType } from '../state/types'; +import { AddressTreeInfo, StateTreeInfo, TreeType } from '../state/types'; +import { + defaultTestStateTreeAccounts, + getDefaultAddressTreeInfo, +} from '../constants'; /** * @deprecated use {@link selectStateTreeInfo} instead. Get a random tree and diff --git a/js/stateless.js/tests/e2e/compress.test.ts b/js/stateless.js/tests/e2e/compress.test.ts index edb5e23eb3..537bb3824b 100644 --- a/js/stateless.js/tests/e2e/compress.test.ts +++ b/js/stateless.js/tests/e2e/compress.test.ts @@ -92,7 +92,6 @@ describe('compress', () => { ], LightSystemProgram.programId, undefined, - undefined, stateTreeInfo, ); @@ -107,7 +106,7 @@ describe('compress', () => { ], 0, LightSystemProgram.programId, - undefined, + undefined, stateTreeInfo, ); @@ -123,7 +122,6 @@ describe('compress', () => { ], LightSystemProgram.programId, undefined, - undefined, stateTreeInfo, ); @@ -138,7 +136,6 @@ describe('compress', () => { ], LightSystemProgram.programId, undefined, - undefined, stateTreeInfo, ); await expect( @@ -154,7 +151,6 @@ describe('compress', () => { ], LightSystemProgram.programId, undefined, - undefined, stateTreeInfo, ), ).rejects.toThrow(); @@ -216,7 +212,6 @@ describe('compress', () => { 100, LightSystemProgram.programId, undefined, - undefined, stateTreeInfo, ); diff --git a/js/stateless.js/tests/e2e/rpc-interop.test.ts b/js/stateless.js/tests/e2e/rpc-interop.test.ts index 3613c2039c..7b3bbe314e 100644 --- a/js/stateless.js/tests/e2e/rpc-interop.test.ts +++ b/js/stateless.js/tests/e2e/rpc-interop.test.ts @@ -12,6 +12,7 @@ import { defaultTestStateTreeAccounts, deriveAddress, deriveAddressSeed, + getDefaultAddressTreeInfo, selectStateTreeInfo, sleep, } from '../../src'; @@ -197,7 +198,6 @@ describe('rpc-interop', () => { newAddressSeedsTest, LightSystemProgram.programId, undefined, - undefined, stateTreeInfo, ); executedTxs++; @@ -210,7 +210,6 @@ describe('rpc-interop', () => { newAddressSeeds, LightSystemProgram.programId, undefined, - undefined, stateTreeInfo, ); executedTxs++; @@ -687,9 +686,8 @@ describe('rpc-interop', () => { it('[test-rpc missing] getCompressionSignaturesForAddress should work', async () => { const seeds = [new Uint8Array(randomBytes(32))]; const seed = deriveAddressSeed(seeds, LightSystemProgram.programId); - const addressTree = defaultTestStateTreeAccounts().addressTree; - const addressQueue = defaultTestStateTreeAccounts().addressQueue; - const address = deriveAddress(seed, addressTree); + const addressTreeInfo = getDefaultAddressTreeInfo(); + const address = deriveAddress(seed, addressTreeInfo.tree); await logIndexed(rpc, testRpc, payer, 'before create account1'); await createAccount( @@ -697,8 +695,7 @@ describe('rpc-interop', () => { payer, seeds, LightSystemProgram.programId, - addressTree, - addressQueue, + addressTreeInfo, stateTreeInfo, ); await logIndexed(rpc, testRpc, payer, 'after create account1'); @@ -740,9 +737,8 @@ describe('rpc-interop', () => { const seeds = [new Uint8Array(randomBytes(32))]; const seed = deriveAddressSeed(seeds, LightSystemProgram.programId); - const addressTree = defaultTestStateTreeAccounts().addressTree; - const addressQueue = defaultTestStateTreeAccounts().addressQueue; - const address = deriveAddress(seed, addressTree); + const addressTreeInfo = getDefaultAddressTreeInfo(); + const address = deriveAddress(seed, addressTreeInfo.tree); console.log('expected address base58', address.toBase58()); await logIndexed(rpc, testRpc, payer, 'before create account2'); @@ -751,8 +747,7 @@ describe('rpc-interop', () => { payer, seeds, LightSystemProgram.programId, - addressTree, - addressQueue, + addressTreeInfo, stateTreeInfo, ); diff --git a/js/stateless.js/tests/e2e/rpc-multi-trees.test.ts b/js/stateless.js/tests/e2e/rpc-multi-trees.test.ts index 8bfbd804b4..cd745f74d4 100644 --- a/js/stateless.js/tests/e2e/rpc-multi-trees.test.ts +++ b/js/stateless.js/tests/e2e/rpc-multi-trees.test.ts @@ -94,7 +94,6 @@ describe('rpc-multi-trees', () => { [seed], LightSystemProgram.programId, undefined, - undefined, tree, // output state tree ); @@ -205,7 +204,6 @@ describe('rpc-multi-trees', () => { 0, LightSystemProgram.programId, undefined, - undefined, tree, ); executedTxs++; diff --git a/js/stateless.js/tests/e2e/transfer.test.ts b/js/stateless.js/tests/e2e/transfer.test.ts index 13572991de..83e2fce63d 100644 --- a/js/stateless.js/tests/e2e/transfer.test.ts +++ b/js/stateless.js/tests/e2e/transfer.test.ts @@ -18,13 +18,7 @@ describe('transfer', () => { payer = await newAccountWithLamports(rpc, 2e9, 256); bob = await newAccountWithLamports(rpc, 2e9, 256); - await compress( - rpc, - payer, - 1e9, - payer.publicKey, - // defaultTestStateTreeAccounts().merkleTree, - ); + await compress(rpc, payer, 1e9, payer.publicKey); }); const numberOfTransfers = 10; From 1c7880dd2549ccd4a24a2f5d7ef56cd70427a62f Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 8 Apr 2025 05:55:36 +0100 Subject: [PATCH 12/19] all ctoken tests working --- .../src/utils/get-token-pool-infos.ts | 3 +- .../tests/e2e/approve-and-mint-to.test.ts | 15 +++-- .../e2e/compress-spl-token-account.test.ts | 6 +- .../tests/e2e/compress.test.ts | 22 ++++++- js/compressed-token/tests/e2e/layout.test.ts | 1 + .../tests/e2e/rpc-multi-trees.test.ts | 7 +-- .../tests/e2e/rpc-token-interop.test.ts | 8 ++- .../tests/e2e/transfer.test.ts | 58 +------------------ js/stateless.js/tests/e2e/test-rpc.test.ts | 7 +-- 9 files changed, 47 insertions(+), 80 deletions(-) diff --git a/js/compressed-token/src/utils/get-token-pool-infos.ts b/js/compressed-token/src/utils/get-token-pool-infos.ts index 16f371a3fb..775700c259 100644 --- a/js/compressed-token/src/utils/get-token-pool-infos.ts +++ b/js/compressed-token/src/utils/get-token-pool-infos.ts @@ -178,7 +178,8 @@ export function selectTokenPoolInfosForDecompression( const sufficientBalanceInfo = infos.find(info => info.balance.gte(new BN(decompressAmount).mul(new BN(10))), ); - + // filter only infos that are initialized + infos = infos.filter(info => info.isInitialized); // If none found, return all infos return sufficientBalanceInfo ? [sufficientBalanceInfo] : infos; } diff --git a/js/compressed-token/tests/e2e/approve-and-mint-to.test.ts b/js/compressed-token/tests/e2e/approve-and-mint-to.test.ts index 15c44b0c4c..6d5cff1b90 100644 --- a/js/compressed-token/tests/e2e/approve-and-mint-to.test.ts +++ b/js/compressed-token/tests/e2e/approve-and-mint-to.test.ts @@ -83,16 +83,15 @@ describe('approveAndMintTo', () => { mintKeypair = Keypair.generate(); mint = mintKeypair.publicKey; - tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); - stateTreeInfo = selectStateTreeInfo( - await rpc.getCachedActiveStateTreeInfos(), - ); - /// Create external SPL mint await createTestSplMint(rpc, payer, mintKeypair, mintAuthority); /// Register mint await createTokenPool(rpc, payer, mint); + tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); + stateTreeInfo = selectStateTreeInfo( + await rpc.getCachedActiveStateTreeInfos(), + ); }); it('should mintTo compressed account with external spl mint', async () => { @@ -133,6 +132,10 @@ describe('approveAndMintTo', () => { await createTokenPool(rpc, payer, token22Mint); assert(token22Mint.equals(token22MintKeypair.publicKey)); + const tokenPoolInfoT22 = selectTokenPoolInfo( + await getTokenPoolInfos(rpc, token22Mint), + ); + await approveAndMintTo( rpc, payer, @@ -141,7 +144,7 @@ describe('approveAndMintTo', () => { token22MintAuthority, 1000000000, stateTreeInfo, - tokenPoolInfo, + tokenPoolInfoT22, ); await assertApproveAndMintTo(rpc, token22Mint, bn(1000000000), bob); diff --git a/js/compressed-token/tests/e2e/compress-spl-token-account.test.ts b/js/compressed-token/tests/e2e/compress-spl-token-account.test.ts index 3b25098e35..d40daff4e7 100644 --- a/js/compressed-token/tests/e2e/compress-spl-token-account.test.ts +++ b/js/compressed-token/tests/e2e/compress-spl-token-account.test.ts @@ -334,6 +334,10 @@ describe('compressSplTokenAccount', () => { true, ) ).mint; + + const tokenPoolInfoT22 = selectTokenPoolInfo( + await getTokenPoolInfos(rpc, mint), + ); const mintAccountInfo = await rpc.getAccountInfo(mint); assert.equal( mintAccountInfo!.owner.toBase58(), @@ -379,7 +383,7 @@ describe('compressSplTokenAccount', () => { aliceAta, undefined, stateTreeInfo, - tokenPoolInfo, + tokenPoolInfoT22, ); // Get final balances diff --git a/js/compressed-token/tests/e2e/compress.test.ts b/js/compressed-token/tests/e2e/compress.test.ts index 60e91365fa..30099db1a0 100644 --- a/js/compressed-token/tests/e2e/compress.test.ts +++ b/js/compressed-token/tests/e2e/compress.test.ts @@ -335,6 +335,23 @@ describe('compress', () => { TOKEN_2022_PROGRAM_ID, ); + const tokenPoolInfoT22 = selectTokenPoolInfo( + await getTokenPoolInfos(rpc, token22Mint), + ); + + await expect( + mintTo( + rpc, + payer, + token22Mint, + bob.publicKey, + mintAuthority, + bn(10000), + stateTreeInfo, + tokenPoolInfo, + ), + ).rejects.toThrow(); + await mintTo( rpc, payer, @@ -343,9 +360,8 @@ describe('compress', () => { mintAuthority, bn(10000), stateTreeInfo, - tokenPoolInfo, + tokenPoolInfoT22, ); - await decompress( rpc, payer, @@ -370,7 +386,7 @@ describe('compress', () => { bobToken2022Ata, charlie.publicKey, stateTreeInfo, - tokenPoolInfo, + tokenPoolInfoT22, ); await assertCompress( rpc, diff --git a/js/compressed-token/tests/e2e/layout.test.ts b/js/compressed-token/tests/e2e/layout.test.ts index c384b2f25c..99b9b11eb1 100644 --- a/js/compressed-token/tests/e2e/layout.test.ts +++ b/js/compressed-token/tests/e2e/layout.test.ts @@ -26,6 +26,7 @@ import { Connection } from '@solana/web3.js'; import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token'; import { SystemProgram } from '@solana/web3.js'; import { + COMPRESSED_TOKEN_PROGRAM_ID, defaultStaticAccountsStruct, LightSystemProgram, } from '@lightprotocol/stateless.js'; diff --git a/js/compressed-token/tests/e2e/rpc-multi-trees.test.ts b/js/compressed-token/tests/e2e/rpc-multi-trees.test.ts index 027b373b17..b84801a444 100644 --- a/js/compressed-token/tests/e2e/rpc-multi-trees.test.ts +++ b/js/compressed-token/tests/e2e/rpc-multi-trees.test.ts @@ -31,10 +31,6 @@ describe('rpc-multi-trees', () => { beforeAll(async () => { rpc = createRpc(); - stateTreeInfo = await rpc.getCachedActiveStateTreeInfos()[0]; - - tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); - payer = await newAccountWithLamports(rpc, 1e9, 252); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); @@ -49,6 +45,9 @@ describe('rpc-multi-trees', () => { ) ).mint; + stateTreeInfo = (await rpc.getCachedActiveStateTreeInfos())[0]; + tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); + bob = await newAccountWithLamports(rpc, 1e9, 256); charlie = await newAccountWithLamports(rpc, 1e9, 256); diff --git a/js/compressed-token/tests/e2e/rpc-token-interop.test.ts b/js/compressed-token/tests/e2e/rpc-token-interop.test.ts index 0c2595d407..0491daff76 100644 --- a/js/compressed-token/tests/e2e/rpc-token-interop.test.ts +++ b/js/compressed-token/tests/e2e/rpc-token-interop.test.ts @@ -33,7 +33,7 @@ describe('rpc-interop token', () => { beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); testRpc = await getTestRpc(lightWasm); payer = await newAccountWithLamports(rpc); bob = await newAccountWithLamports(rpc); @@ -265,6 +265,10 @@ describe('rpc-interop token', () => { ) ).mint; + const tokenPoolInfo2 = selectTokenPoolInfo( + await getTokenPoolInfos(rpc, mint2), + ); + await mintTo( rpc, payer, @@ -273,7 +277,7 @@ describe('rpc-interop token', () => { mintAuthority, bn(1000), stateTreeInfo, - tokenPoolInfo, + tokenPoolInfo2, ); const senderAccounts = await rpc.getCompressedTokenAccountsByOwner( diff --git a/js/compressed-token/tests/e2e/transfer.test.ts b/js/compressed-token/tests/e2e/transfer.test.ts index f1e68de02f..925afc56cc 100644 --- a/js/compressed-token/tests/e2e/transfer.test.ts +++ b/js/compressed-token/tests/e2e/transfer.test.ts @@ -76,13 +76,6 @@ async function assertTransfer( ); } - console.log( - 'recipient compressed token accounts', - recipientCompressedTokenAccounts.map(acc => - acc.parsed.amount.toString(), - ), - ); - /// recipient should have received the amount expect( @@ -236,57 +229,8 @@ describe('transfer', () => { mint, }); - console.log( - 'charlie pre compressed token accounts 4', - charliePreCompressedTokenAccounts4.items.map(acc => - acc.parsed.amount.toString(), - ), - ); - - const bobPreCompressedTokenAccounts5 = - await rpc.getCompressedTokenAccountsByOwner(bob.publicKey, { - mint, - }); + await transfer(rpc, payer, mint, bn(700), charlie, bob.publicKey); - console.log( - 'bob pre compressed token accounts 5', - bobPreCompressedTokenAccounts5.items.map(acc => - acc.parsed.amount.toString(), - ), - ); - - const txId = await transfer( - rpc, - payer, - mint, - bn(700), - charlie, - bob.publicKey, - ); - - const bobPostCompressedTokenAccounts6 = - await rpc.getCompressedTokenAccountsByOwner(bob.publicKey, { - mint, - }); - - console.log( - 'bob post compressed token accounts 6', - bobPostCompressedTokenAccounts6.items.map(acc => - acc.parsed.amount.toString(), - ), - ); - - const charliePostCompressedTokenAccounts7 = - await rpc.getCompressedTokenAccountsByOwner(charlie.publicKey, { - mint, - }); - - console.log( - 'charlie post compressed token accounts 7', - charliePostCompressedTokenAccounts7.items.map(acc => - acc.parsed.amount.toString(), - ), - ); await assertTransfer( rpc, charliePreCompressedTokenAccounts4.items, diff --git a/js/stateless.js/tests/e2e/test-rpc.test.ts b/js/stateless.js/tests/e2e/test-rpc.test.ts index 55dce91735..925f54ecda 100644 --- a/js/stateless.js/tests/e2e/test-rpc.test.ts +++ b/js/stateless.js/tests/e2e/test-rpc.test.ts @@ -78,14 +78,9 @@ describe('test-rpc', () => { const compressedAccountProof = await rpc.getCompressedAccountProof( bn(refHash), ); - console.log( - 'compressedAccounts', - compressedAccounts.items.map(x => x), - ); - console.log('compressedAccountProof', compressedAccountProof); + const proof = compressedAccountProof.merkleProof.map(x => x.toString()); - console.log('proof', proof); expect(proof.length).toStrictEqual(26); expect(compressedAccountProof.hash).toStrictEqual(refHash); expect(compressedAccountProof.leafIndex).toStrictEqual( From cc5dbc2ad35f5af55547bb631eac093bf29f261d Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 8 Apr 2025 06:00:58 +0100 Subject: [PATCH 13/19] rm logs --- js/compressed-token/src/program.ts | 34 ------------------- .../test-rpc/get-parsed-events.ts | 4 +-- js/stateless.js/tests/e2e/rpc-interop.test.ts | 21 +----------- 3 files changed, 2 insertions(+), 57 deletions(-) diff --git a/js/compressed-token/src/program.ts b/js/compressed-token/src/program.ts index 8050d34a0b..27c64fbcbd 100644 --- a/js/compressed-token/src/program.ts +++ b/js/compressed-token/src/program.ts @@ -784,19 +784,6 @@ export class CompressedTokenProgram { toAddress, } = params; - console.log( - 'input token accounts queues', - inputCompressedTokenAccounts.map( - acc => acc.compressedAccount.nullifierQueue, - ), - ); - console.log( - 'input token accounts trees', - inputCompressedTokenAccounts.map( - acc => acc.compressedAccount.merkleTree, - ), - ); - console.log('output state tree info', outputStateTreeInfo); const tokenTransferOutputs: TokenTransferOutputData[] = createTransferOutputState( inputCompressedTokenAccounts, @@ -814,27 +801,6 @@ export class CompressedTokenProgram { tokenTransferOutputs, }); - console.log( - 'packed remaining account metas', - remainingAccountMetas.map(meta => meta.pubkey.toBase58()), - ); - console.log( - 'packed input - tree indices', - inputTokenDataWithContext.map( - data => data.merkleContext.merkleTreePubkeyIndex, - ), - ); - console.log( - 'packed input - queue indices', - inputTokenDataWithContext.map( - data => data.merkleContext.nullifierQueuePubkeyIndex, - ), - ); - console.log( - 'packed output - tree indices', - packedOutputTokenData.map(data => data.merkleTreeIndex), - ); - const { mint, currentOwner } = parseTokenData( inputCompressedTokenAccounts, ); diff --git a/js/stateless.js/src/test-helpers/test-rpc/get-parsed-events.ts b/js/stateless.js/src/test-helpers/test-rpc/get-parsed-events.ts index 72cca5c923..d3049e6f02 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/get-parsed-events.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/get-parsed-events.ts @@ -240,9 +240,7 @@ export function parseLightTransaction( const insertIntoQueuesDiscriminatorStr = bs58.encode( INSERT_INTO_QUEUES_DISCRIMINATOR, ); - if (discriminatorStr !== insertIntoQueuesDiscriminatorStr) { - // console.log('discriminator does not match'); - } else { + if (discriminatorStr === insertIntoQueuesDiscriminatorStr) { const dataSlice = data.slice(12); appendInputsData = deserializeAppendNullifyCreateAddressInputsIndexer( diff --git a/js/stateless.js/tests/e2e/rpc-interop.test.ts b/js/stateless.js/tests/e2e/rpc-interop.test.ts index 7b3bbe314e..e3c811133b 100644 --- a/js/stateless.js/tests/e2e/rpc-interop.test.ts +++ b/js/stateless.js/tests/e2e/rpc-interop.test.ts @@ -30,6 +30,7 @@ const log = async ( console.log(`${prefix} - indexed: `, accounts.items.length); }; +// debug helper. const logIndexed = async ( rpc: Rpc, testRpc: TestRpc, @@ -488,8 +489,6 @@ describe('rpc-interop', () => { }); it('getCompressedAccountsByOwner should match', async () => { - await logIndexed(rpc, testRpc, payer, 'payer'); - const senderAccounts = await rpc.getCompressedAccountsByOwner( payer.publicKey, ); @@ -513,7 +512,6 @@ describe('rpc-interop', () => { ); }); - await logIndexed(rpc, testRpc, bob, 'bob'); const receiverAccounts = await rpc.getCompressedAccountsByOwner( bob.publicKey, ); @@ -569,11 +567,9 @@ describe('rpc-interop', () => { }); it('getMultipleCompressedAccounts should match', async () => { - await logIndexed(rpc, testRpc, payer, 'before compress'); await compress(rpc, payer, 1e9, payer.publicKey, stateTreeInfo); executedTxs++; - await logIndexed(rpc, testRpc, payer, 'after compress'); const senderAccounts = await rpc.getCompressedAccountsByOwner( payer.publicKey, ); @@ -689,7 +685,6 @@ describe('rpc-interop', () => { const addressTreeInfo = getDefaultAddressTreeInfo(); const address = deriveAddress(seed, addressTreeInfo.tree); - await logIndexed(rpc, testRpc, payer, 'before create account1'); await createAccount( rpc, payer, @@ -698,9 +693,7 @@ describe('rpc-interop', () => { addressTreeInfo, stateTreeInfo, ); - await logIndexed(rpc, testRpc, payer, 'after create account1'); - await sleep(3000); const accounts = await rpc.getCompressedAccountsByOwner( payer.publicKey, ); @@ -712,14 +705,6 @@ describe('rpc-interop', () => { payer.publicKey, ); - console.log( - 'All accounts from testRpc:', - allAccountsTestRpc.items.map(i => i.hash), - ); - console.log( - 'All accounts from rpc:', - allAccountsRpc.items.map(i => i.hash), - ); const latestAccount = accounts.items[0]; // assert the address was indexed @@ -739,9 +724,7 @@ describe('rpc-interop', () => { const addressTreeInfo = getDefaultAddressTreeInfo(); const address = deriveAddress(seed, addressTreeInfo.tree); - console.log('expected address base58', address.toBase58()); - await logIndexed(rpc, testRpc, payer, 'before create account2'); await createAccount( rpc, payer, @@ -751,8 +734,6 @@ describe('rpc-interop', () => { stateTreeInfo, ); - await logIndexed(rpc, testRpc, payer, 'after create account2'); - // fetch the owners latest account const accounts = await rpc.getCompressedAccountsByOwner( payer.publicKey, From 0dd62ea60bc162ee139a859268af7cba9933b988 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 8 Apr 2025 07:01:00 +0100 Subject: [PATCH 14/19] clean --- .../e2e/compress-spl-token-account.test.ts | 4 +- js/stateless.js/CHANGELOG.md | 13 +++- .../src/test-helpers/test-rpc/test-rpc.ts | 65 ------------------- js/stateless.js/tests/e2e/test-rpc.test.ts | 11 +--- js/stateless.js/tests/e2e/testnet.test.ts | 8 +-- 5 files changed, 18 insertions(+), 83 deletions(-) diff --git a/js/compressed-token/tests/e2e/compress-spl-token-account.test.ts b/js/compressed-token/tests/e2e/compress-spl-token-account.test.ts index d40daff4e7..d7f896bf16 100644 --- a/js/compressed-token/tests/e2e/compress-spl-token-account.test.ts +++ b/js/compressed-token/tests/e2e/compress-spl-token-account.test.ts @@ -258,7 +258,9 @@ describe('compressSplTokenAccount', () => { expect(compressedAfter.items.length).toBe( compressedBefore.items.length + 1, ); - expect(compressedAfter.items[0].parsed.amount.eq(bn(0))).toBe(true); + expect( + compressedAfter.items.some(item => item.parsed.amount.eq(bn(0))), + ).toBe(true); }); it('should fail when non-owner tries to compress', async () => { diff --git a/js/stateless.js/CHANGELOG.md b/js/stateless.js/CHANGELOG.md index f5986184a8..b714cbdc81 100644 --- a/js/stateless.js/CHANGELOG.md +++ b/js/stateless.js/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [0.21.0] - 2025-04-08 + +This release has several breaking changes which are necessary for protocol +scalability. Please reach out to the [team](https://t.me/swen_light) if you need help migrating. + +### Breaking changes + +TBD. + ## [0.20.5-0.20.9] - 2025-02-24 ### Bumped to latest compressed-token sdk @@ -21,7 +30,9 @@ Fixed a bug where we lose precision on token amounts if compressed token account ### Changed -- `createRpc` can now also be called with only the `rpcEndpoint` parameter. In this case, `compressionApiEndpoint` and `proverEndpoint` will default to the same value. If no parameters are provided, default localnet values are used. +- `createRpc` can now also be called with only the `rpcEndpoint` parameter. In + this case, `compressionApiEndpoint` and `proverEndpoint` will default to the + same value. If no parameters are provided, default localnet values are used. ## [0.19.0] - 2025-01-20 diff --git a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts index 3ba4a4153c..4c3e485b09 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts @@ -272,71 +272,6 @@ export class TestRpc extends Connection implements CompressionApiInterface { async confirmTransactionIndexed(_slot: number): Promise { return true; } - // /** - // * Fetch the latest merkle proofs for multiple compressed accounts specified - // * by an array account hashes - // */ - // async getMultipleCompressedAccountProofs( - // hashes: BN254[], - // ): Promise { - // /// Build tree - // const events: PublicTransactionEvent[] = await getParsedEvents( - // this, - // ).then(events => events.reverse()); - // const allLeaves: number[][] = []; - // const allLeafIndices: number[] = []; - // for (const event of events) { - // for ( - // let index = 0; - // index < event.outputCompressedAccounts.length; - // index++ - // ) { - // const hash = event.outputCompressedAccountHashes[index]; - - // allLeaves.push(hash); - // allLeafIndices.push(event.outputLeafIndices[index]); - // } - // } - // const tree = new MerkleTree( - // this.depth, - // this.lightWasm, - // allLeaves.map(leaf => bn(leaf).toString()), - // ); - - // /// create merkle proofs and assemble return type - // const merkleProofs: MerkleContextWithMerkleProof[] = []; - - // for (let i = 0; i < hashes.length; i++) { - // const leafIndex = tree.indexOf(hashes[i].toString()); - // const pathElements = tree.path(leafIndex).pathElements; - // const bnPathElements = pathElements.map(value => bn(value)); - // const root = bn(tree.root()); - // const merkleProof: MerkleContextWithMerkleProof = { - // hash: hashes[i].toArray('be', 32), - // merkleTree: this.merkleTreeAddress, - // leafIndex: leafIndex, - // merkleProof: bnPathElements, - // nullifierQueue: this.nullifierQueueAddress, - // rootIndex: allLeaves.length, - // root: root, - // }; - // merkleProofs.push(merkleProof); - // } - - // /// Validate - // merkleProofs.forEach((proof, index) => { - // const leafIndex = proof.leafIndex; - // const computedHash = tree.elements()[leafIndex]; - // const hashArr = bn(computedHash).toArray('be', 32); - // if (!hashArr.every((val, index) => val === proof.hash[index])) { - // throw new Error( - // `Mismatch at index ${index}: expected ${proof.hash.toString()}, got ${hashArr.toString()}`, - // ); - // } - // }); - - // return merkleProofs; - // } /** * Fetch the latest merkle proofs for multiple compressed accounts specified diff --git a/js/stateless.js/tests/e2e/test-rpc.test.ts b/js/stateless.js/tests/e2e/test-rpc.test.ts index 925f54ecda..3ab5235c88 100644 --- a/js/stateless.js/tests/e2e/test-rpc.test.ts +++ b/js/stateless.js/tests/e2e/test-rpc.test.ts @@ -86,7 +86,7 @@ describe('test-rpc', () => { expect(compressedAccountProof.leafIndex).toStrictEqual( compressedAccounts.items[0].leafIndex, ); - // expect(compressedAccountProof.rootIndex).toStrictEqual(2); + preCompressBalance = await rpc.getBalance(payer.publicKey); await transfer( @@ -95,7 +95,6 @@ describe('test-rpc', () => { compressLamportsAmount, payer, payer.publicKey, - // merkleTree, ); const compressedAccounts1 = await rpc.getCompressedAccountsByOwner( payer.publicKey, @@ -110,13 +109,7 @@ describe('test-rpc', () => { STATE_MERKLE_TREE_NETWORK_FEE.toNumber(), ); - await compress( - rpc, - payer, - compressLamportsAmount, - payer.publicKey, - // defaultTestStateTreeAccounts().merkleTree, - ); + await compress(rpc, payer, compressLamportsAmount, payer.publicKey); const compressedAccounts2 = await rpc.getCompressedAccountsByOwner( payer.publicKey, ); diff --git a/js/stateless.js/tests/e2e/testnet.test.ts b/js/stateless.js/tests/e2e/testnet.test.ts index b3a09e49d7..0f4b12a5c4 100644 --- a/js/stateless.js/tests/e2e/testnet.test.ts +++ b/js/stateless.js/tests/e2e/testnet.test.ts @@ -20,13 +20,7 @@ describe('testnet transfer', () => { payer = await newAccountWithLamports(rpc, 2e9, 256); bob = await newAccountWithLamports(rpc, 2e9, 256); - await compress( - rpc, - payer, - 1e9, - payer.publicKey, - // defaultTestStateTreeAccounts().merkleTree, - ); + await compress(rpc, payer, 1e9, payer.publicKey); }); const numberOfTransfers = 10; From 65f0f787d3d5c9a8a278643f1257db01dc463d16 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 9 Apr 2025 20:35:34 +0100 Subject: [PATCH 15/19] ctxs to infos, removed redundant getMintProgramId calls --- .../src/actions/approve-and-mint-to.ts | 7 ---- .../src/actions/compress-spl-token-account.ts | 8 +---- js/compressed-token/src/actions/compress.ts | 7 ---- .../src/actions/create-token-pool.ts | 2 +- js/compressed-token/src/actions/decompress.ts | 10 ++---- js/compressed-token/src/actions/mint-to.ts | 36 +++++++++---------- js/compressed-token/src/program.ts | 26 ++++---------- .../src/utils/get-token-pool-infos.ts | 8 +++++ js/stateless.js/CHANGELOG.md | 8 ++++- .../test-rpc/get-compressed-accounts.ts | 4 +-- .../test-rpc/get-compressed-token-accounts.ts | 4 +-- 11 files changed, 45 insertions(+), 75 deletions(-) diff --git a/js/compressed-token/src/actions/approve-and-mint-to.ts b/js/compressed-token/src/actions/approve-and-mint-to.ts index 24d405c522..929cfef888 100644 --- a/js/compressed-token/src/actions/approve-and-mint-to.ts +++ b/js/compressed-token/src/actions/approve-and-mint-to.ts @@ -56,7 +56,6 @@ async function getStorageOptions( * @param outputStateTreeInfo State tree info * @param tokenPoolInfo Token pool info * @param confirmOptions Options for confirming the transaction - * @param tokenProgramId Token program id * * @return Signature of the confirmed transaction */ @@ -70,11 +69,7 @@ export async function approveAndMintTo( outputStateTreeInfo?: StateTreeInfo, tokenPoolInfo?: TokenPoolInfo, confirmOptions?: ConfirmOptions, - tokenProgramId?: PublicKey, ): Promise { - tokenProgramId = - tokenProgramId ?? - (await CompressedTokenProgram.get_mint_program_id(mint, rpc)); outputStateTreeInfo = outputStateTreeInfo ?? selectStateTreeInfo(await rpc.getCachedActiveStateTreeInfos()); @@ -90,7 +85,6 @@ export async function approveAndMintTo( undefined, undefined, confirmOptions, - tokenProgramId, ); const ixs = await CompressedTokenProgram.approveAndMintTo({ @@ -102,7 +96,6 @@ export async function approveAndMintTo( toPubkey, outputStateTreeInfo, tokenPoolInfo, - tokenProgramId, }); const { blockhash } = await rpc.getLatestBlockhash(); diff --git a/js/compressed-token/src/actions/compress-spl-token-account.ts b/js/compressed-token/src/actions/compress-spl-token-account.ts index 4905f6b864..98dcc89ef3 100644 --- a/js/compressed-token/src/actions/compress-spl-token-account.ts +++ b/js/compressed-token/src/actions/compress-spl-token-account.ts @@ -37,8 +37,7 @@ import { CompressedTokenProgram } from '../program'; * account into * @param tokenPoolInfo Token pool info * @param confirmOptions Options for confirming the transaction - * @param tokenProgramId Optional: token program id. Default: SPL Token - * Program ID + * * @return Signature of the confirmed transaction */ @@ -52,11 +51,7 @@ export async function compressSplTokenAccount( outputStateTreeInfo?: StateTreeInfo, tokenPoolInfo?: TokenPoolInfo, confirmOptions?: ConfirmOptions, - tokenProgramId?: PublicKey, ): Promise { - tokenProgramId = - tokenProgramId ?? - (await CompressedTokenProgram.get_mint_program_id(mint, rpc)); outputStateTreeInfo = outputStateTreeInfo ?? selectStateTreeInfo(await rpc.getCachedActiveStateTreeInfos()); @@ -72,7 +67,6 @@ export async function compressSplTokenAccount( remainingAmount, outputStateTreeInfo, tokenPoolInfo, - tokenProgramId, }); const blockhashCtx = await rpc.getLatestBlockhash(); diff --git a/js/compressed-token/src/actions/compress.ts b/js/compressed-token/src/actions/compress.ts index c063f72625..c20a4e9ce6 100644 --- a/js/compressed-token/src/actions/compress.ts +++ b/js/compressed-token/src/actions/compress.ts @@ -39,8 +39,6 @@ import { * state tree account. * @param tokenPoolInfo Token pool info * @param confirmOptions Options for confirming the transaction - * @param tokenProgramId Optional: token program id. Default: SPL Token - * Program ID * * @return Signature of the confirmed transaction */ @@ -55,11 +53,7 @@ export async function compress( outputStateTreeInfo?: StateTreeInfo, tokenPoolInfo?: TokenPoolInfo, confirmOptions?: ConfirmOptions, - tokenProgramId?: PublicKey, ): Promise { - tokenProgramId = tokenProgramId - ? tokenProgramId - : await CompressedTokenProgram.get_mint_program_id(mint, rpc); outputStateTreeInfo = outputStateTreeInfo ?? selectStateTreeInfo(await rpc.getCachedActiveStateTreeInfos()); @@ -76,7 +70,6 @@ export async function compress( mint, outputStateTreeInfo, tokenPoolInfo, - tokenProgramId, }); const blockhashCtx = await rpc.getLatestBlockhash(); diff --git a/js/compressed-token/src/actions/create-token-pool.ts b/js/compressed-token/src/actions/create-token-pool.ts index 4793439f3c..fa712078f3 100644 --- a/js/compressed-token/src/actions/create-token-pool.ts +++ b/js/compressed-token/src/actions/create-token-pool.ts @@ -31,7 +31,7 @@ export async function createTokenPool( ): Promise { tokenProgramId = tokenProgramId ? tokenProgramId - : await CompressedTokenProgram.get_mint_program_id(mint, rpc); + : await CompressedTokenProgram.getMintProgramId(mint, rpc); const ix = await CompressedTokenProgram.createTokenPool({ feePayer: payer.publicKey, diff --git a/js/compressed-token/src/actions/decompress.ts b/js/compressed-token/src/actions/decompress.ts index a1190ce6d1..004a5ee522 100644 --- a/js/compressed-token/src/actions/decompress.ts +++ b/js/compressed-token/src/actions/decompress.ts @@ -40,8 +40,7 @@ import { getTokenPoolInfos } from '../utils/get-token-pool-infos'; * default state tree account. * @param tokenPoolInfos Token pool infos * @param confirmOptions Options for confirming the transaction - * @param tokenProgramId Optional: token program id. Default: SPL Token - * Program ID + * * @return Signature of the confirmed transaction */ @@ -55,12 +54,7 @@ export async function decompress( outputStateTreeInfo?: StateTreeInfo, tokenPoolInfos?: TokenPoolInfo[], confirmOptions?: ConfirmOptions, - tokenProgramId?: PublicKey, ): Promise { - tokenProgramId = tokenProgramId - ? tokenProgramId - : await CompressedTokenProgram.get_mint_program_id(mint, rpc); - amount = bn(amount); const compressedTokenAccounts = await rpc.getCompressedTokenAccountsByOwner( @@ -84,6 +78,7 @@ export async function decompress( selectStateTreeInfo(await rpc.getCachedActiveStateTreeInfos()); tokenPoolInfos = tokenPoolInfos ?? (await getTokenPoolInfos(rpc, mint)); + const selectedTokenPoolInfos = selectTokenPoolInfosForDecompression( tokenPoolInfos, amount, @@ -98,7 +93,6 @@ export async function decompress( tokenPoolInfos: selectedTokenPoolInfos, recentInputStateRootIndices: proof.rootIndices, recentValidityProof: proof.compressedProof, - tokenProgramId, }); const { blockhash } = await rpc.getLatestBlockhash(); diff --git a/js/compressed-token/src/actions/mint-to.ts b/js/compressed-token/src/actions/mint-to.ts index cd1524fa42..a8fa29d58c 100644 --- a/js/compressed-token/src/actions/mint-to.ts +++ b/js/compressed-token/src/actions/mint-to.ts @@ -24,17 +24,20 @@ import { /** * Mint compressed tokens to a solana address * - * @param rpc Rpc to use - * @param payer Payer of the transaction fees - * @param mint Mint for the account - * @param destination Address of the account to mint to. Can be an array of - * addresses if the amount is an array of amounts. - * @param authority Minting authority - * @param amount Amount to mint. Can be an array of amounts if the - * destination is an array of addresses. - * @param outputStateTreeInfo State tree account that the compressed tokens should be - * part of. Defaults to the default state tree account. - * @param confirmOptions Options for confirming the transaction + * @param rpc Rpc to use + * @param payer Payer of the transaction fees + * @param mint Mint for the account + * @param toPubkey Address of the account to mint to. Can be an + * array of addresses if the amount is an array of + * amounts. + * @param authority Minting authority + * @param amount Amount to mint. Can be an array of amounts if + * the toPubkey is an array of addresses. + * @param outputStateTreeInfo State tree account that the compressed tokens + * should be part of. Defaults to the default state + * tree account. + * @param tokenPoolInfo Token pool information + * @param confirmOptions Options for confirming the transaction * * @return Signature of the confirmed transaction */ @@ -42,22 +45,16 @@ export async function mintTo( rpc: Rpc, payer: Signer, mint: PublicKey, - destination: PublicKey | PublicKey[], + toPubkey: PublicKey | PublicKey[], authority: Signer, amount: number | BN | number[] | BN[], outputStateTreeInfo?: StateTreeInfo, tokenPoolInfo?: TokenPoolInfo, confirmOptions?: ConfirmOptions, - tokenProgramId?: PublicKey, ): Promise { - tokenProgramId = tokenProgramId - ? tokenProgramId - : await CompressedTokenProgram.get_mint_program_id(mint, rpc); - outputStateTreeInfo = outputStateTreeInfo ?? selectStateTreeInfo(await rpc.getCachedActiveStateTreeInfos()); - tokenPoolInfo = tokenPoolInfo ?? selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); @@ -67,10 +64,9 @@ export async function mintTo( mint, authority: authority.publicKey, amount, - toPubkey: destination, + toPubkey, outputStateTreeInfo, tokenPoolInfo, - tokenProgramId, }); const { blockhash } = await rpc.getLatestBlockhash(); diff --git a/js/compressed-token/src/program.ts b/js/compressed-token/src/program.ts index 27c64fbcbd..754405a01e 100644 --- a/js/compressed-token/src/program.ts +++ b/js/compressed-token/src/program.ts @@ -162,10 +162,6 @@ export type DecompressParams = { * Tokenpool addresses. One or more token pools can be provided. */ tokenPoolInfos: TokenPoolInfo | TokenPoolInfo[]; - /** - * Optional: The token program ID. Default: SPL Token Program ID - */ - tokenProgramId?: PublicKey; }; export type TransferParams = { @@ -308,10 +304,6 @@ export type MintToParams = { * Tokenpool addresses. One or more token pools can be provided. */ tokenPoolInfo: TokenPoolInfo; - /** - * Optional: The token program ID. Default: SPL Token Program ID - */ - tokenProgramId?: PublicKey; }; /** @@ -366,10 +358,6 @@ export type ApproveAndMintToParams = { * Tokenpool addresses. One or more token pools can be provided. */ tokenPoolInfo: TokenPoolInfo; - /** - * Optional: The token program ID. Default: SPL Token Program ID - */ - tokenProgramId?: PublicKey; }; export type CreateTokenProgramLookupTableParams = { @@ -677,11 +665,10 @@ export class CompressedTokenProgram { outputStateTreeInfo, toPubkey, amount, - tokenProgramId, tokenPoolInfo, } = params; - const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; + const tokenProgram = tokenPoolInfo.tokenProgram; checkTokenPoolInfo(tokenPoolInfo, mint); const amounts = toArray(amount).map(amount => bn(amount)); @@ -736,7 +723,7 @@ export class CompressedTokenProgram { authority, outputStateTreeInfo, toPubkey, - tokenProgramId, + tokenPoolInfo, } = params; @@ -749,7 +736,7 @@ export class CompressedTokenProgram { authority, amount, [], - tokenProgramId, + tokenPoolInfo.tokenProgram, ); /// 2. Compress from mint authority ATA to recipient compressed account @@ -762,7 +749,6 @@ export class CompressedTokenProgram { amount: params.amount, outputStateTreeInfo, tokenPoolInfo, - tokenProgramId, }); return [splMintToInstruction, compressInstruction]; @@ -1022,7 +1008,6 @@ export class CompressedTokenProgram { outputStateTreeInfo, recentValidityProof, recentInputStateRootIndices, - tokenProgramId, } = params; const amount = bn(params.amount); const tokenPoolInfos = toArray(params.tokenPoolInfos); @@ -1060,7 +1045,8 @@ export class CompressedTokenProgram { lamportsChangeAccountMerkleTreeIndex: null, }; const data = encodeTransferInstructionData(rawData); - const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; + const tokenProgram = tokenPoolInfos[0].tokenProgram; + const { accountCompressionAuthority, noopProgram, @@ -1192,7 +1178,7 @@ export class CompressedTokenProgram { }); } - static async get_mint_program_id( + static async getMintProgramId( mint: PublicKey, connection: Connection, ): Promise { diff --git a/js/compressed-token/src/utils/get-token-pool-infos.ts b/js/compressed-token/src/utils/get-token-pool-infos.ts index 775700c259..da6215bdb3 100644 --- a/js/compressed-token/src/utils/get-token-pool-infos.ts +++ b/js/compressed-token/src/utils/get-token-pool-infos.ts @@ -26,6 +26,14 @@ export function checkTokenPoolInfo( return true; } +/** + * Get the token pool infos for a given mint. + * @param rpc The RPC client + * @param mint The mint of the token pool + * @param commitment The commitment to use + * + * @returns The token pool infos + */ export async function getTokenPoolInfos( rpc: Rpc, mint: PublicKey, diff --git a/js/stateless.js/CHANGELOG.md b/js/stateless.js/CHANGELOG.md index b714cbdc81..92226dac1c 100644 --- a/js/stateless.js/CHANGELOG.md +++ b/js/stateless.js/CHANGELOG.md @@ -7,7 +7,13 @@ scalability. Please reach out to the [team](https://t.me/swen_light) if you need ### Breaking changes -TBD. +- ActiveTreeBundle renamed to StateTreeInfo +- outputStateTree () + + + + + ## [0.20.5-0.20.9] - 2025-02-24 diff --git a/js/stateless.js/src/test-helpers/test-rpc/get-compressed-accounts.ts b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-accounts.ts index 3a586e89af..01339e1e2f 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/get-compressed-accounts.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-accounts.ts @@ -95,7 +95,7 @@ async function getCompressedAccountsForTest(rpc: Rpc) { const events = (await getParsedEvents(rpc)).reverse(); const allOutputAccounts: CompressedAccountWithMerkleContext[] = []; const allInputAccountHashes: BN[] = []; - const ctxs = await rpc.getCachedActiveStateTreeInfos(); + const infos = await rpc.getCachedActiveStateTreeInfos(); for (const event of events) { for ( @@ -110,7 +110,7 @@ async function getCompressedAccountsForTest(rpc: Rpc) { // In test-rpc we can do this with a static set of trees because it's local-only. const { queue, treeType, tree } = getQueueForTree( - ctxs, + infos, new PublicKey(smt), ); diff --git a/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts index 06fd5cf613..ee014439de 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts @@ -76,7 +76,7 @@ async function parseEventWithTokenTlvData( rpc: Rpc, ): Promise { const pubkeyArray = event.pubkeyArray; - const ctxs = await rpc.getCachedActiveStateTreeInfos(); + const infos = await rpc.getCachedActiveStateTreeInfos(); const outputHashes = event.outputCompressedAccountHashes; const outputCompressedAccountsWithParsedTokenData: ParsedTokenAccount[] = @@ -84,7 +84,7 @@ async function parseEventWithTokenTlvData( const maybeTree = pubkeyArray[event.outputCompressedAccounts[i].merkleTreeIndex]; - const { queue, treeType, tree } = getQueueForTree(ctxs, maybeTree); + const { queue, treeType, tree } = getQueueForTree(infos, maybeTree); if ( !tree.equals( From 1130c3bda0dddf02d644719a870106a7e2d1318e Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 9 Apr 2025 20:37:50 +0100 Subject: [PATCH 16/19] rm deadcode, storageoptions --- .../src/actions/approve-and-mint-to.ts | 22 +------------------ js/compressed-token/src/types.ts | 5 ----- js/stateless.js/src/programs/system.ts | 7 ------ .../src/test-helpers/test-rpc/test-rpc.ts | 6 +---- 4 files changed, 2 insertions(+), 38 deletions(-) diff --git a/js/compressed-token/src/actions/approve-and-mint-to.ts b/js/compressed-token/src/actions/approve-and-mint-to.ts index 929cfef888..6a1e3abc9a 100644 --- a/js/compressed-token/src/actions/approve-and-mint-to.ts +++ b/js/compressed-token/src/actions/approve-and-mint-to.ts @@ -11,14 +11,12 @@ import { buildAndSignTx, Rpc, dedupeSigner, - pickRandomTreeAndQueue, StateTreeInfo, selectStateTreeInfo, - toArray, } from '@lightprotocol/stateless.js'; import { CompressedTokenProgram } from '../program'; import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token'; -import { isSingleTokenPoolInfo, StorageOptions } from '../types'; + import { getTokenPoolInfos, selectTokenPoolInfo, @@ -26,24 +24,6 @@ import { TokenPoolInfo, } from '../utils/get-token-pool-infos'; -async function getStorageOptions( - rpc: Rpc, - mint: PublicKey, - decompressAmount?: number | BN, -): Promise { - const res = await Promise.all([ - rpc.getCachedActiveStateTreeInfos(), - getTokenPoolInfos(rpc, mint), - ]); - - return { - stateTreeInfo: selectStateTreeInfo(res[0]), - tokenPoolInfos: decompressAmount - ? selectTokenPoolInfosForDecompression(res[1], decompressAmount) - : selectTokenPoolInfo(res[1]), - }; -} - /** * Mint compressed tokens to a solana address from an external mint authority * diff --git a/js/compressed-token/src/types.ts b/js/compressed-token/src/types.ts index ca9e3e9c65..5659942475 100644 --- a/js/compressed-token/src/types.ts +++ b/js/compressed-token/src/types.ts @@ -80,11 +80,6 @@ export type CompressSplTokenAccountInstructionData = { cpiContext: CompressedCpiContext | null; }; -export type StorageOptions = { - stateTreeInfo: StateTreeInfo; - tokenPoolInfos: TokenPoolInfo | TokenPoolInfo[]; -}; - export function isSingleTokenPoolInfo( tokenPoolInfos: TokenPoolInfo | TokenPoolInfo[], ): tokenPoolInfos is TokenPoolInfo { diff --git a/js/stateless.js/src/programs/system.ts b/js/stateless.js/src/programs/system.ts index 67ad7f36a5..dd9982b41d 100644 --- a/js/stateless.js/src/programs/system.ts +++ b/js/stateless.js/src/programs/system.ts @@ -32,13 +32,6 @@ export const sumUpLamports = ( ); }; -type StorageOptions = { - /** - * The state tree info that the tx output should be inserted into. Defaults to a - * public state tree if unspecified. - */ - stateTreeInfo: StateTreeInfo; -}; /** * Create compressed account system transaction params */ diff --git a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts index 4c3e485b09..071251f5d6 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts @@ -355,7 +355,6 @@ export class TestRpc extends Connection implements CompressionApiInterface { } for (let i = 0; i < hashes.length; i++) { - // const hashStr = hashes[i].toString(); const leafIndex = leaves.findIndex(leaf => bn(leaf).eq(hashes[i]), ); @@ -395,10 +394,7 @@ export class TestRpc extends Connection implements CompressionApiInterface { const computedHash = leavesByTree.get(proof.merkleTree.toBase58())! .leaves[leafIndex]; const hashArr = bn(computedHash).toArray('be', 32); - if ( - !hashArr.every((val, index) => val === proof.hash[index]) - // !proof.nullifierQueue.equals(PublicKey.default) - ) { + if (!hashArr.every((val, index) => val === proof.hash[index])) { throw new Error( `Mismatch at index ${index}: expected ${proof.hash.toString()}, got ${hashArr.toString()}`, ); From c2cf028fff284ff19e05832183bd5d1b1ad13a48 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 9 Apr 2025 20:52:56 +0100 Subject: [PATCH 17/19] fix cli getMindProgramId use --- cli/src/commands/compress-spl/index.ts | 3 +-- cli/src/commands/decompress-spl/index.ts | 3 +-- js/stateless.js/CHANGELOG.md | 9 ++------- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/cli/src/commands/compress-spl/index.ts b/cli/src/commands/compress-spl/index.ts index ee1920ad91..c5c8c09236 100644 --- a/cli/src/commands/compress-spl/index.ts +++ b/cli/src/commands/compress-spl/index.ts @@ -52,7 +52,7 @@ class CompressSplCommand extends Command { const toPublicKey = new PublicKey(to); const mintPublicKey = new PublicKey(mint); const payer = defaultSolanaWalletKeypair(); - const tokenProgramId = await CompressedTokenProgram.get_mint_program_id( + const tokenProgramId = await CompressedTokenProgram.getMintProgramId( mintPublicKey, rpc(), ); @@ -76,7 +76,6 @@ class CompressSplCommand extends Command { undefined, undefined, undefined, - tokenProgramId, ); loader.stop(false); diff --git a/cli/src/commands/decompress-spl/index.ts b/cli/src/commands/decompress-spl/index.ts index 36d532962e..c98dc64a81 100644 --- a/cli/src/commands/decompress-spl/index.ts +++ b/cli/src/commands/decompress-spl/index.ts @@ -52,7 +52,7 @@ class DecompressSplCommand extends Command { const toPublicKey = new PublicKey(to); const mintPublicKey = new PublicKey(mint); const payer = defaultSolanaWalletKeypair(); - const tokenProgramId = await CompressedTokenProgram.get_mint_program_id( + const tokenProgramId = await CompressedTokenProgram.getMintProgramId( mintPublicKey, rpc(), ); @@ -78,7 +78,6 @@ class DecompressSplCommand extends Command { undefined, undefined, undefined, - tokenProgramId, ); loader.stop(false); diff --git a/js/stateless.js/CHANGELOG.md b/js/stateless.js/CHANGELOG.md index 92226dac1c..04a68a0e70 100644 --- a/js/stateless.js/CHANGELOG.md +++ b/js/stateless.js/CHANGELOG.md @@ -7,13 +7,8 @@ scalability. Please reach out to the [team](https://t.me/swen_light) if you need ### Breaking changes -- ActiveTreeBundle renamed to StateTreeInfo -- outputStateTree () - - - - - +- ActiveTreeBundle renamed to StateTreeInfo +- outputStateTree () ## [0.20.5-0.20.9] - 2025-02-24 From dede9c80fda8bb2f9c209353fc0d04f6f4ae8170 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 9 Apr 2025 22:22:43 +0100 Subject: [PATCH 18/19] fix tokenpool --- js/compressed-token/src/utils/get-token-pool-infos.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/compressed-token/src/utils/get-token-pool-infos.ts b/js/compressed-token/src/utils/get-token-pool-infos.ts index da6215bdb3..e8e0ff8194 100644 --- a/js/compressed-token/src/utils/get-token-pool-infos.ts +++ b/js/compressed-token/src/utils/get-token-pool-infos.ts @@ -1,5 +1,5 @@ import { Commitment, PublicKey } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID, unpackAccount } from '@solana/spl-token'; +import { unpackAccount } from '@solana/spl-token'; import { CompressedTokenProgram } from '../program'; import { Rpc } from '@lightprotocol/stateless.js'; import BN from 'bn.js'; @@ -42,6 +42,7 @@ export async function getTokenPoolInfos( const addresses = Array.from({ length: 5 }, (_, i) => deriveTokenPoolPdaWithBump(mint, i), ); + const accountInfos = await rpc.getMultipleAccountsInfo( addresses, commitment, @@ -59,8 +60,7 @@ export async function getTokenPoolInfos( : null, ); - const tokenProgram = parsedInfos[0]!.owner; - + const tokenProgram = accountInfos[0]!.owner; return parsedInfos.map((parsedInfo, i) => { if (!parsedInfo) { return { From d147d4f35808cb272cec3493300ff36a6e87c92b Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 9 Apr 2025 23:55:03 +0100 Subject: [PATCH 19/19] fix test --- js/compressed-token/package.json | 2 +- .../src/actions/approve-and-mint-to.ts | 1 + js/compressed-token/src/program.ts | 18 +----- .../tests/e2e/compress.test.ts | 1 + .../tests/e2e/rpc-multi-trees.test.ts | 64 +++++++++++-------- 5 files changed, 44 insertions(+), 42 deletions(-) diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index a874942602..89b66beb08 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -87,7 +87,7 @@ "test:e2e:select-accounts": "vitest run tests/e2e/select-accounts.test.ts --reporter=verbose", "test:e2e:create-token-pool": "pnpm test-validator && vitest run tests/e2e/create-token-pool.test.ts", "test:e2e:mint-to": "pnpm test-validator && vitest run tests/e2e/mint-to.test.ts --reporter=verbose", - "test:e2e:approve-and-mint-to": "pnpm test-validator && vitest run tests/e2e/approve-and-mint-to.test.ts --reporter=verbose", + "test:e2e:approve-and-mint-to": "pnpm test-validator && vitest run tests/e2e/approve-and-mint-to.test.ts --reporter=verbose --bail=1", "test:e2e:merge-token-accounts": "pnpm test-validator && vitest run tests/e2e/merge-token-accounts.test.ts --reporter=verbose", "test:e2e:transfer": "pnpm test-validator && vitest run tests/e2e/transfer.test.ts --reporter=verbose --bail=1", "test:e2e:compress": "pnpm test-validator && vitest run tests/e2e/compress.test.ts --reporter=verbose", diff --git a/js/compressed-token/src/actions/approve-and-mint-to.ts b/js/compressed-token/src/actions/approve-and-mint-to.ts index 6a1e3abc9a..3c0ea1f913 100644 --- a/js/compressed-token/src/actions/approve-and-mint-to.ts +++ b/js/compressed-token/src/actions/approve-and-mint-to.ts @@ -65,6 +65,7 @@ export async function approveAndMintTo( undefined, undefined, confirmOptions, + tokenPoolInfo.tokenProgram, ); const ixs = await CompressedTokenProgram.approveAndMintTo({ diff --git a/js/compressed-token/src/program.ts b/js/compressed-token/src/program.ts index 754405a01e..4a044c8336 100644 --- a/js/compressed-token/src/program.ts +++ b/js/compressed-token/src/program.ts @@ -85,10 +85,6 @@ export type CompressParams = { * Tokenpool. */ tokenPoolInfo: TokenPoolInfo; - /** - * Optional: The token program ID. Default: SPL Token Program ID - */ - tokenProgramId?: PublicKey; }; export type CompressSplTokenAccountParams = { @@ -120,10 +116,6 @@ export type CompressSplTokenAccountParams = { * The token pool info. */ tokenPoolInfo: TokenPoolInfo; - /** - * Optional: The token program ID. Default: SPL Token Program ID - */ - tokenProgramId?: PublicKey; }; export type DecompressParams = { @@ -723,7 +715,6 @@ export class CompressedTokenProgram { authority, outputStateTreeInfo, toPubkey, - tokenPoolInfo, } = params; @@ -903,7 +894,6 @@ export class CompressedTokenProgram { toAddress, mint, outputStateTreeInfo, - tokenProgramId, tokenPoolInfo, } = params; @@ -969,8 +959,6 @@ export class CompressedTokenProgram { }; const data = encodeTransferInstructionData(rawData); - const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; - checkTokenPoolInfo(tokenPoolInfo, mint); const keys = transferAccountsLayout({ @@ -983,7 +971,7 @@ export class CompressedTokenProgram { systemProgram: SystemProgram.programId, tokenPoolPda: tokenPoolInfo.tokenPoolPda, compressOrDecompressTokenAccount: source, - tokenProgram, + tokenProgram: tokenPoolInfo.tokenProgram, }); keys.push(...remainingAccountMetas); @@ -1129,9 +1117,7 @@ export class CompressedTokenProgram { remainingAmount, outputStateTreeInfo, tokenPoolInfo, - tokenProgramId, } = params; - const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; checkTokenPoolInfo(tokenPoolInfo, mint); const remainingAccountMetas: AccountMeta[] = [ @@ -1165,7 +1151,7 @@ export class CompressedTokenProgram { selfProgram: this.programId, tokenPoolPda: tokenPoolInfo.tokenPoolPda, compressOrDecompressTokenAccount: tokenAccount, - tokenProgram, + tokenProgram: tokenPoolInfo.tokenProgram, systemProgram: SystemProgram.programId, }); diff --git a/js/compressed-token/tests/e2e/compress.test.ts b/js/compressed-token/tests/e2e/compress.test.ts index 30099db1a0..2f5cf58928 100644 --- a/js/compressed-token/tests/e2e/compress.test.ts +++ b/js/compressed-token/tests/e2e/compress.test.ts @@ -338,6 +338,7 @@ describe('compress', () => { const tokenPoolInfoT22 = selectTokenPoolInfo( await getTokenPoolInfos(rpc, token22Mint), ); + console.log('tokenPoolInfoT22', tokenPoolInfoT22); await expect( mintTo( diff --git a/js/compressed-token/tests/e2e/rpc-multi-trees.test.ts b/js/compressed-token/tests/e2e/rpc-multi-trees.test.ts index b84801a444..6c4ea37754 100644 --- a/js/compressed-token/tests/e2e/rpc-multi-trees.test.ts +++ b/js/compressed-token/tests/e2e/rpc-multi-trees.test.ts @@ -26,6 +26,7 @@ describe('rpc-multi-trees', () => { let mintAuthority: Keypair; let stateTreeInfo: StateTreeInfo; + let stateTreeInfo2: StateTreeInfo; let tokenPoolInfo: TokenPoolInfo; beforeAll(async () => { @@ -46,6 +47,7 @@ describe('rpc-multi-trees', () => { ).mint; stateTreeInfo = (await rpc.getCachedActiveStateTreeInfos())[0]; + stateTreeInfo2 = (await rpc.getCachedActiveStateTreeInfos())[1]; tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); bob = await newAccountWithLamports(rpc, 1e9, 256); @@ -63,7 +65,15 @@ describe('rpc-multi-trees', () => { ); // should auto land in same tree - await transfer(rpc, payer, mint, bn(700), bob, charlie.publicKey); + await transfer( + rpc, + payer, + mint, + bn(700), + bob, + charlie.publicKey, + stateTreeInfo2, + ); }); it('getCompressedTokenAccountsByOwner work with random state tree', async () => { @@ -79,37 +89,41 @@ describe('rpc-multi-trees', () => { expect(senderAccounts.length).toBe(1); expect(receiverAccounts.length).toBe(1); - expect(senderAccounts[0].compressedAccount.merkleTree.toBase58()).toBe( - stateTreeInfo.tree.toBase58(), - ); expect( - receiverAccounts[0].compressedAccount.merkleTree.toBase58(), - ).toBe(stateTreeInfo.tree.toBase58()); + senderAccounts[0].compressedAccount.merkleTree.toBase58() === + stateTreeInfo2.tree.toBase58(), + ).toBe(true); + expect( + receiverAccounts[0].compressedAccount.merkleTree.toBase58() === + stateTreeInfo2.tree.toBase58(), + ).toBe(true); }); - it('getCompressedTokenAccountBalance should return consistent tree and queue ', async () => { + it('getCompressedTokenAccountBalance should return consistent tree and queue', async () => { const senderAccounts = await rpc.getCompressedTokenAccountsByOwner( bob.publicKey, { mint }, ); - expect( - senderAccounts.items[0].compressedAccount.merkleTree.toBase58(), - ).toBe(stateTreeInfo.tree.toBase58()); - expect( - senderAccounts.items[0].compressedAccount.nullifierQueue.toBase58(), - ).toBe(stateTreeInfo.queue.toBase58()); + const senderAccount = senderAccounts.items[0].compressedAccount; + + expect(senderAccount.merkleTree.toBase58()).toBe( + stateTreeInfo2.tree.toBase58(), + ); + expect(senderAccount.nullifierQueue.toBase58()).toBe( + stateTreeInfo2.queue.toBase58(), + ); }); it('should return both compressed token accounts in different trees', async () => { - const info2 = await rpc.getCachedActiveStateTreeInfos()[1]; - const previousTree = stateTreeInfo.tree; - - let otherInfo = info2; - if (previousTree.toBase58() === stateTreeInfo.tree.toBase58()) { - otherInfo = stateTreeInfo; - } - - await mintTo(rpc, payer, mint, bob.publicKey, mintAuthority, bn(1042)); + await mintTo( + rpc, + payer, + mint, + bob.publicKey, + mintAuthority, + bn(1042), + stateTreeInfo, + ); const senderAccounts = await rpc.getCompressedTokenAccountsByOwner( bob.publicKey, @@ -118,17 +132,17 @@ describe('rpc-multi-trees', () => { const previousAccount = senderAccounts.items.find( account => account.compressedAccount.merkleTree.toBase58() === - previousTree.toBase58(), + stateTreeInfo2.tree.toBase58(), ); const newlyMintedAccount = senderAccounts.items.find( account => account.compressedAccount.merkleTree.toBase58() === - otherInfo.tree.toBase58(), + stateTreeInfo.tree.toBase58() && + account.parsed.amount.toNumber() === 1042, ); expect(previousAccount).toBeDefined(); expect(newlyMintedAccount).toBeDefined(); - expect(newlyMintedAccount!.parsed.amount.toNumber()).toBe(1042); }); });