diff --git a/cli/src/commands/compress-spl/index.ts b/cli/src/commands/compress-spl/index.ts index f1c4c6d6d6..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(), ); @@ -75,7 +75,7 @@ class CompressSplCommand extends Command { toPublicKey, undefined, undefined, - tokenProgramId, + undefined, ); loader.stop(false); diff --git a/cli/src/commands/decompress-spl/index.ts b/cli/src/commands/decompress-spl/index.ts index 0b61c53f88..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(), ); @@ -77,7 +77,7 @@ class DecompressSplCommand extends Command { recipientAta.address, undefined, undefined, - tokenProgramId, + undefined, ); loader.stop(false); 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/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/package.json b/js/compressed-token/package.json index df3cfa42bb..89b66beb08 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -87,9 +87,9 @@ "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", + "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/actions/approve-and-mint-to.ts b/js/compressed-token/src/actions/approve-and-mint-to.ts index ff12b120d4..3c0ea1f913 100644 --- a/js/compressed-token/src/actions/approve-and-mint-to.ts +++ b/js/compressed-token/src/actions/approve-and-mint-to.ts @@ -11,23 +11,31 @@ import { buildAndSignTx, Rpc, dedupeSigner, - pickRandomTreeAndQueue, + StateTreeInfo, + selectStateTreeInfo, } from '@lightprotocol/stateless.js'; import { CompressedTokenProgram } from '../program'; import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + selectTokenPoolInfosForDecompression, + TokenPoolInfo, +} from '../utils/get-token-pool-infos'; + /** * 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 merkleTree State tree account that the compressed tokens should be - * part of. Defaults to random public 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 + * @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 * * @return Signature of the confirmed transaction */ @@ -35,16 +43,19 @@ export async function approveAndMintTo( rpc: Rpc, payer: Signer, mint: PublicKey, - destination: PublicKey, + toPubkey: PublicKey, authority: Signer, amount: number | BN, - merkleTree?: PublicKey, + 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)); const authorityTokenAccount = await getOrCreateAssociatedTokenAccount( rpc, @@ -54,40 +65,29 @@ export async function approveAndMintTo( undefined, undefined, confirmOptions, - tokenProgramId, + tokenPoolInfo.tokenProgram, ); - if (!merkleTree) { - const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); - const { tree } = pickRandomTreeAndQueue(stateTreeInfo); - merkleTree = tree; - } - const ixs = await CompressedTokenProgram.approveAndMintTo({ feePayer: payer.publicKey, mint, authority: authority.publicKey, authorityTokenAccount: authorityTokenAccount.address, amount, - toPubkey: destination, - merkleTree, - tokenProgramId, + toPubkey, + outputStateTreeInfo, + tokenPoolInfo, }); const { blockhash } = await rpc.getLatestBlockhash(); 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..98dcc89ef3 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,14 @@ 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 + * * @return Signature of the confirmed transaction */ @@ -36,14 +47,17 @@ 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); + outputStateTreeInfo = + outputStateTreeInfo ?? + selectStateTreeInfo(await rpc.getCachedActiveStateTreeInfos()); + tokenPoolInfo = + tokenPoolInfo ?? + selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); const compressIx = await CompressedTokenProgram.compressSplTokenAccount({ feePayer: payer.publicKey, @@ -51,12 +65,13 @@ export async function compressSplTokenAccount( tokenAccount, mint, remainingAmount, - outputStateTree, - tokenProgramId, + outputStateTreeInfo, + tokenPoolInfo, }); const blockhashCtx = await rpc.getLatestBlockhash(); const additionalSigners = dedupeSigner(payer, [owner]); + const signedTx = buildAndSignTx( [ ComputeBudgetProgram.setComputeUnitLimit({ @@ -68,11 +83,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 8d58972be0..c20a4e9ce6 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,12 +34,12 @@ 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 * - * * @return Signature of the confirmed transaction */ export async function compress( @@ -43,19 +50,16 @@ 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.getCachedActiveStateTreeInfo(); - 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,8 +68,8 @@ export async function compress( toAddress, amount, mint, - outputStateTree: merkleTree, - tokenProgramId, + outputStateTreeInfo, + tokenPoolInfo, }); const blockhashCtx = await rpc.getLatestBlockhash(); @@ -73,7 +77,7 @@ export async function compress( const signedTx = buildAndSignTx( [ ComputeBudgetProgram.setComputeUnitLimit({ - units: 1_000_000, + units: 600_000, }), compressIx, ], @@ -81,11 +85,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/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 d0ebf3aed5..004a5ee522 100644 --- a/js/compressed-token/src/actions/decompress.ts +++ b/js/compressed-token/src/actions/decompress.ts @@ -11,28 +11,36 @@ import { buildAndSignTx, Rpc, dedupeSigner, + selectStateTreeInfo, + StateTreeInfo, } from '@lightprotocol/stateless.js'; import BN from 'bn.js'; import { CompressedTokenProgram } from '../program'; import { selectMinCompressedTokenAccountsForTransfer } from '../utils'; +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 + * * @return Signature of the confirmed transaction */ @@ -43,14 +51,10 @@ export async function decompress( amount: number | BN, owner: Signer, toAddress: PublicKey, - merkleTree?: PublicKey, + 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( @@ -60,7 +64,6 @@ export async function decompress( }, ); - /// TODO: consider using a different selection algorithm const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer( compressedTokenAccounts.items, amount, @@ -70,15 +73,26 @@ export async function decompress( inputAccounts.map(account => bn(account.compressedAccount.hash)), ); + outputStateTreeInfo = + outputStateTreeInfo ?? + selectStateTreeInfo(await rpc.getCachedActiveStateTreeInfos()); + + tokenPoolInfos = 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, + tokenPoolInfos: selectedTokenPoolInfos, recentInputStateRootIndices: proof.rootIndices, recentValidityProof: proof.compressedProof, - tokenProgramId, }); const { blockhash } = await rpc.getLatestBlockhash(); @@ -89,6 +103,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/src/actions/merge-token-accounts.ts b/js/compressed-token/src/actions/merge-token-accounts.ts index 84212a4c27..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, - outputStateTree: 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 2f304613c4..a8fa29d58c 100644 --- a/js/compressed-token/src/actions/mint-to.ts +++ b/js/compressed-token/src/actions/mint-to.ts @@ -11,24 +11,33 @@ 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 * - * @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 merkleTree 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 */ @@ -36,36 +45,32 @@ export async function mintTo( rpc: Rpc, payer: Signer, mint: PublicKey, - destination: PublicKey | PublicKey[], + toPubkey: PublicKey | PublicKey[], authority: Signer, amount: number | BN | number[] | BN[], - merkleTree?: PublicKey, + outputStateTreeInfo?: StateTreeInfo, + tokenPoolInfo?: TokenPoolInfo, confirmOptions?: ConfirmOptions, - tokenProgramId?: PublicKey, ): Promise { - tokenProgramId = tokenProgramId - ? tokenProgramId - : await CompressedTokenProgram.get_mint_program_id(mint, rpc); - - const additionalSigners = dedupeSigner(payer, [authority]); - - if (!merkleTree) { - const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); - const { tree } = pickRandomTreeAndQueue(stateTreeInfo); - merkleTree = tree; - } + outputStateTreeInfo = + outputStateTreeInfo ?? + selectStateTreeInfo(await rpc.getCachedActiveStateTreeInfos()); + tokenPoolInfo = + tokenPoolInfo ?? + selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); const ix = await CompressedTokenProgram.mintTo({ feePayer: payer.publicKey, mint, authority: authority.publicKey, - amount: amount, - toPubkey: destination, - merkleTree, - tokenProgramId, + amount, + toPubkey, + outputStateTreeInfo, + tokenPoolInfo, }); const { blockhash } = await rpc.getLatestBlockhash(); + const additionalSigners = dedupeSigner(payer, [authority]); const tx = buildAndSignTx( [ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), ix], @@ -74,7 +79,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/compressed-token/src/instructions/pack-compressed-token-accounts.ts b/js/compressed-token/src/instructions/pack-compressed-token-accounts.ts index a29d6e512e..3615836ddf 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, @@ -60,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( @@ -96,7 +96,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..4a044c8336 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, @@ -44,6 +45,10 @@ import { CompressedTokenInstructionDataTransfer, TokenTransferOutputData, } from './types'; +import { + checkTokenPoolInfo, + TokenPoolInfo, +} from './utils/get-token-pool-infos'; export type CompressParams = { /** @@ -75,11 +80,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; /** - * Optional: The token program ID. Default: SPL Token Program ID + * Tokenpool. */ - tokenProgramId?: PublicKey; + tokenPoolInfo: TokenPoolInfo; }; export type CompressSplTokenAccountParams = { @@ -106,11 +111,11 @@ 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 + * The token pool info. */ - tokenProgramId?: PublicKey; + tokenPoolInfo: TokenPoolInfo; }; export type DecompressParams = { @@ -144,11 +149,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; /** - * Optional: The token program ID. Default: SPL Token Program ID + * Tokenpool addresses. One or more token pools can be provided. */ - tokenProgramId?: PublicKey; + tokenPoolInfos: TokenPoolInfo | TokenPoolInfo[]; }; export type TransferParams = { @@ -184,7 +189,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 +253,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,14 +289,13 @@ 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. */ - merkleTree?: PublicKey; + outputStateTreeInfo: StateTreeInfo; /** - * Optional: The token program ID. Default: SPL Token Program ID + * Tokenpool addresses. One or more token pools can be provided. */ - tokenProgramId?: PublicKey; + tokenPoolInfo: TokenPoolInfo; }; /** @@ -338,14 +342,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; /** - * Optional: The token program ID. Default: SPL Token Program ID + * Tokenpool addresses. One or more token pools can be provided. */ - tokenProgramId?: PublicKey; + tokenPoolInfo: TokenPoolInfo; }; export type CreateTokenProgramLookupTableParams = { @@ -539,6 +543,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 { @@ -638,14 +654,14 @@ export class CompressedTokenProgram { mint, feePayer, authority, - merkleTree, + outputStateTreeInfo, toPubkey, amount, - tokenProgramId, + tokenPoolInfo, } = params; - const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; - const tokenPoolPda = this.deriveTokenPoolPda(mint); + const tokenProgram = tokenPoolInfo.tokenProgram; + checkTokenPoolInfo(tokenPoolInfo, mint); const amounts = toArray(amount).map(amount => bn(amount)); @@ -663,13 +679,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: merkleTree ?? defaultTestStateTreeAccounts().merkleTree, + merkleTree: outputStateTreeInfo.tree, selfProgram: this.programId, systemProgram: SystemProgram.programId, solPoolPda: null, // TODO: add lamports support @@ -697,9 +713,9 @@ export class CompressedTokenProgram { feePayer, authorityTokenAccount, authority, - merkleTree, + outputStateTreeInfo, toPubkey, - tokenProgramId, + tokenPoolInfo, } = params; const amount: bigint = BigInt(params.amount.toString()); @@ -711,7 +727,7 @@ export class CompressedTokenProgram { authority, amount, [], - tokenProgramId, + tokenPoolInfo.tokenProgram, ); /// 2. Compress from mint authority ATA to recipient compressed account @@ -722,12 +738,13 @@ export class CompressedTokenProgram { toAddress: toPubkey, mint, amount: params.amount, - outputStateTree: merkleTree, - tokenProgramId, + outputStateTreeInfo, + tokenPoolInfo, }); return [splMintToInstruction, compressInstruction]; } + /** * Construct transfer instruction for compressed tokens */ @@ -740,7 +757,7 @@ export class CompressedTokenProgram { recentInputStateRootIndices, recentValidityProof, amount, - outputStateTrees, + outputStateTreeInfo, toAddress, } = params; @@ -756,7 +773,7 @@ export class CompressedTokenProgram { remainingAccountMetas, } = packCompressedTokenAccounts({ inputCompressedTokenAccounts, - outputStateTrees, + outputStateTreeInfo, rootIndices: recentInputStateRootIndices, tokenTransferOutputs, }); @@ -876,8 +893,8 @@ export class CompressedTokenProgram { source, toAddress, mint, - outputStateTree, - tokenProgramId, + outputStateTreeInfo, + tokenPoolInfo, } = params; if (Array.isArray(params.amount) !== Array.isArray(params.toAddress)) { @@ -920,7 +937,7 @@ export class CompressedTokenProgram { remainingAccountMetas, } = packCompressedTokenAccounts({ inputCompressedTokenAccounts: [], - outputStateTrees: outputStateTree, + outputStateTreeInfo, rootIndices: [], tokenTransferOutputs, }); @@ -942,7 +959,7 @@ export class CompressedTokenProgram { }; const data = encodeTransferInstructionData(rawData); - const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; + checkTokenPoolInfo(tokenPoolInfo, mint); const keys = transferAccountsLayout({ ...defaultStaticAccountsStruct(), @@ -952,9 +969,9 @@ export class CompressedTokenProgram { lightSystemProgram: LightSystemProgram.programId, selfProgram: this.programId, systemProgram: SystemProgram.programId, - tokenPoolPda: this.deriveTokenPoolPda(mint), + tokenPoolPda: tokenPoolInfo.tokenPoolPda, compressOrDecompressTokenAccount: source, - tokenProgram, + tokenProgram: tokenPoolInfo.tokenProgram, }); keys.push(...remainingAccountMetas); @@ -976,12 +993,12 @@ export class CompressedTokenProgram { payer, inputCompressedTokenAccounts, toAddress, - outputStateTree, + outputStateTreeInfo, recentValidityProof, recentInputStateRootIndices, - tokenProgramId, } = params; const amount = bn(params.amount); + const tokenPoolInfos = toArray(params.tokenPoolInfos); const tokenTransferOutputs = createDecompressOutputState( inputCompressedTokenAccounts, @@ -995,7 +1012,7 @@ export class CompressedTokenProgram { remainingAccountMetas, } = packCompressedTokenAccounts({ inputCompressedTokenAccounts, - outputStateTrees: outputStateTree, + outputStateTreeInfo, rootIndices: recentInputStateRootIndices, tokenTransferOutputs: tokenTransferOutputs, }); @@ -1016,7 +1033,8 @@ export class CompressedTokenProgram { lamportsChangeAccountMerkleTreeIndex: null, }; const data = encodeTransferInstructionData(rawData); - const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; + const tokenProgram = tokenPoolInfos[0].tokenProgram; + const { accountCompressionAuthority, noopProgram, @@ -1034,13 +1052,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, @@ -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, - tokenProgramId, + outputStateTreeInfo, + tokenPoolInfo, } = params; - const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; + checkTokenPoolInfo(tokenPoolInfo, mint); const remainingAccountMetas: AccountMeta[] = [ { - pubkey: outputStateTree, + pubkey: outputStateTreeInfo.tree, isSigner: false, isWritable: true, }, @@ -1124,9 +1149,9 @@ export class CompressedTokenProgram { accountCompressionAuthority: accountCompressionAuthority, accountCompressionProgram: accountCompressionProgram, selfProgram: this.programId, - tokenPoolPda: this.deriveTokenPoolPda(mint), + tokenPoolPda: tokenPoolInfo.tokenPoolPda, compressOrDecompressTokenAccount: tokenAccount, - tokenProgram, + tokenProgram: tokenPoolInfo.tokenProgram, systemProgram: SystemProgram.programId, }); @@ -1139,7 +1164,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/types.ts b/js/compressed-token/src/types.ts index 208ea5b849..5659942475 100644 --- a/js/compressed-token/src/types.ts +++ b/js/compressed-token/src/types.ts @@ -3,7 +3,9 @@ import BN from 'bn.js'; import { CompressedProof, PackedMerkleContext, + StateTreeInfo, } from '@lightprotocol/stateless.js'; +import { TokenPoolInfo } from './utils/get-token-pool-infos'; export type CompressedCpiContext = { setContext: boolean; @@ -78,6 +80,12 @@ export type CompressSplTokenAccountInstructionData = { cpiContext: CompressedCpiContext | null; }; +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 new file mode 100644 index 0000000000..e8e0ff8194 --- /dev/null +++ b/js/compressed-token/src/utils/get-token-pool-infos.ts @@ -0,0 +1,193 @@ +import { Commitment, PublicKey } from '@solana/web3.js'; +import { 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; +} + +/** + * 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, + 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 = accountInfos[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, +} + +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; +}; + +/** + * 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); + + // filter only infos that are initialized + infos = infos.filter(info => info.isInitialized); + + // 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 One or multiple token pool infos + */ +export function selectTokenPoolInfosForDecompression( + infos: TokenPoolInfo[], + decompressAmount: number | BN, +): TokenPoolInfo | TokenPoolInfo[] { + infos = shuffleArray(infos); + // Find the first info where balance is 10x the requested amount + 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 2b952cdd9f..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 @@ -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(); @@ -79,6 +88,10 @@ describe('approveAndMintTo', () => { /// 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 () => { @@ -91,7 +104,8 @@ describe('approveAndMintTo', () => { bob, mintAuthority, 1000000000, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); await assertApproveAndMintTo(rpc, mint, bn(1000000000), bob); @@ -118,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, @@ -125,7 +143,8 @@ describe('approveAndMintTo', () => { bob, token22MintAuthority, 1000000000, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + 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 a11d4dd29f..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 @@ -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); @@ -238,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 () => { @@ -262,13 +284,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 +315,9 @@ describe('compressSplTokenAccount', () => { mint, alice, aliceAta, - invalidTree, + undefined, + invalidTreeInfo, + tokenPoolInfo, ), ).rejects.toThrow(); }); @@ -307,6 +336,10 @@ describe('compressSplTokenAccount', () => { true, ) ).mint; + + const tokenPoolInfoT22 = selectTokenPoolInfo( + await getTokenPoolInfos(rpc, mint), + ); const mintAccountInfo = await rpc.getAccountInfo(mint); assert.equal( mintAccountInfo!.owner.toBase58(), @@ -331,7 +364,7 @@ describe('compressSplTokenAccount', () => { alice.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await decompress(rpc, payer, mint, bn(1000), alice, aliceAta); @@ -350,7 +383,9 @@ describe('compressSplTokenAccount', () => { mint, alice, aliceAta, - defaultTestStateTreeAccounts().merkleTree, + undefined, + stateTreeInfo, + 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 414e236b18..2f5cf58928 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(); @@ -317,6 +335,24 @@ describe('compress', () => { TOKEN_2022_PROGRAM_ID, ); + const tokenPoolInfoT22 = selectTokenPoolInfo( + await getTokenPoolInfos(rpc, token22Mint), + ); + console.log('tokenPoolInfoT22', tokenPoolInfoT22); + + await expect( + mintTo( + rpc, + payer, + token22Mint, + bob.publicKey, + mintAuthority, + bn(10000), + stateTreeInfo, + tokenPoolInfo, + ), + ).rejects.toThrow(); + await mintTo( rpc, payer, @@ -324,9 +360,9 @@ describe('compress', () => { bob.publicKey, mintAuthority, bn(10000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfoT22, ); - await decompress( rpc, payer, @@ -350,7 +386,8 @@ describe('compress', () => { bob, bobToken2022Ata, charlie.publicKey, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfoT22, ); await assertCompress( rpc, diff --git a/js/compressed-token/tests/e2e/decompress.test.ts b/js/compressed-token/tests/e2e/decompress.test.ts index aaa6b72ef9..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, @@ -8,10 +8,18 @@ import { defaultTestStateTreeAccounts, newAccountWithLamports, getTestRpc, + getActiveStateTreeInfos, + selectStateTreeInfo, + 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 @@ -61,17 +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; - const { merkleTree } = defaultTestStateTreeAccounts(); + let stateTreeInfo: StateTreeInfo; + let tokenPoolInfos: TokenPoolInfo[]; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); rpc = await getTestRpc(lightWasm); payer = await newAccountWithLamports(rpc, 1e9); + bob = await newAccountWithLamports(rpc, 1e9); + charlie = await newAccountWithLamports(rpc, 1e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); @@ -85,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, @@ -102,7 +114,8 @@ describe('decompress', () => { bob.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + selectTokenPoolInfo(tokenPoolInfos), ); }); @@ -125,7 +138,8 @@ describe('decompress', () => { bn(5), bob, charlieAta, - merkleTree, + stateTreeInfo, + tokenPoolInfos, ); await assertDecompress( diff --git a/js/compressed-token/tests/e2e/layout.test.ts b/js/compressed-token/tests/e2e/layout.test.ts index f198509db2..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'; @@ -41,11 +42,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/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 c97a5db3b1..6c4ea37754 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,15 +24,14 @@ describe('rpc-multi-trees', () => { let charlie: Signer; let mint: PublicKey; let mintAuthority: Keypair; - let treeAndQueue: { tree: PublicKey; queue: PublicKey }; + + let stateTreeInfo: StateTreeInfo; + let stateTreeInfo2: StateTreeInfo; + let tokenPoolInfo: TokenPoolInfo; beforeAll(async () => { rpc = createRpc(); - treeAndQueue = pickRandomTreeAndQueue( - await rpc.getCachedActiveStateTreeInfo(), - ); - payer = await newAccountWithLamports(rpc, 1e9, 252); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); @@ -46,6 +46,10 @@ 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); charlie = await newAccountWithLamports(rpc, 1e9, 256); @@ -56,11 +60,20 @@ describe('rpc-multi-trees', () => { bob.publicKey, mintAuthority, bn(1000), - treeAndQueue.tree, + stateTreeInfo, + tokenPoolInfo, ); // 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 () => { @@ -76,45 +89,32 @@ describe('rpc-multi-trees', () => { expect(senderAccounts.length).toBe(1); expect(receiverAccounts.length).toBe(1); - expect(senderAccounts[0].compressedAccount.merkleTree.toBase58()).toBe( - treeAndQueue.tree.toBase58(), - ); expect( - receiverAccounts[0].compressedAccount.merkleTree.toBase58(), - ).toBe(treeAndQueue.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(treeAndQueue.tree.toBase58()); - expect( - senderAccounts.items[0].compressedAccount.nullifierQueue.toBase58(), - ).toBe(treeAndQueue.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 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; - } - await mintTo( rpc, payer, @@ -122,7 +122,7 @@ describe('rpc-multi-trees', () => { bob.publicKey, mintAuthority, bn(1042), - otherTree, + stateTreeInfo, ); const senderAccounts = await rpc.getCompressedTokenAccountsByOwner( @@ -132,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() === - otherTree.toBase58(), + stateTreeInfo.tree.toBase58() && + account.parsed.amount.toNumber() === 1042, ); expect(previousAccount).toBeDefined(); expect(newlyMintedAccount).toBeDefined(); - expect(newlyMintedAccount!.parsed.amount.toNumber()).toBe(1042); }); }); 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..0491daff76 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 = createRpc(); + 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); @@ -252,6 +265,10 @@ describe('rpc-interop token', () => { ) ).mint; + const tokenPoolInfo2 = selectTokenPoolInfo( + await getTokenPoolInfos(rpc, mint2), + ); + await mintTo( rpc, payer, @@ -259,7 +276,8 @@ describe('rpc-interop token', () => { bob.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + 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 94ece860d4..925afc56cc 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'; @@ -82,11 +77,15 @@ async function assertTransfer( } /// recipient should have received the amount - const recipientCompressedTokenAccount = recipientCompressedTokenAccounts[0]; - expect(recipientCompressedTokenAccount.parsed.amount.eq(refAmount)).toBe( - true, - ); - expect(recipientCompressedTokenAccount.parsed.delegate).toBe(null); + + 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 +97,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 +107,10 @@ describe('transfer', () => { mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); + stateTreeInfo = selectStateTreeInfo( + await rpc.getCachedActiveStateTreeInfos(), + ); + mint = ( await createMint( rpc, @@ -129,7 +133,7 @@ describe('transfer', () => { bob.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); }); @@ -149,7 +153,7 @@ describe('transfer', () => { bn(700), bob, charlie.publicKey, - merkleTree, + stateTreeInfo, ); await assertTransfer( @@ -176,7 +180,7 @@ describe('transfer', () => { bn(200), bob, charlie.publicKey, - merkleTree, + stateTreeInfo, ); await assertTransfer( @@ -204,7 +208,7 @@ describe('transfer', () => { bn(5), charlie, bob.publicKey, - merkleTree, + stateTreeInfo, ); await assertTransfer( @@ -224,6 +228,7 @@ describe('transfer', () => { await rpc.getCompressedTokenAccountsByOwner(charlie.publicKey, { mint, }); + await transfer(rpc, payer, mint, bn(700), charlie, bob.publicKey); await assertTransfer( @@ -245,7 +250,7 @@ describe('transfer', () => { 10000, bob, charlie.publicKey, - merkleTree, + stateTreeInfo, ), ).rejects.toThrow('Insufficient balance for transfer'); }); @@ -276,7 +281,7 @@ describe('transfer', () => { bob.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); /// send 700 from bob -> charlie @@ -295,7 +300,7 @@ describe('transfer', () => { bn(700), bob, charlie.publicKey, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await assertTransfer( @@ -319,12 +324,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 +360,7 @@ describe('e2e transfer with multiple accounts', () => { sender.publicKey, mintAuthority, new BN(25), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await mintTo( rpc, @@ -359,7 +369,7 @@ describe('e2e transfer with multiple accounts', () => { sender.publicKey, mintAuthority, new BN(25), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await mintTo( rpc, @@ -368,7 +378,7 @@ describe('e2e transfer with multiple accounts', () => { sender.publicKey, mintAuthority, new BN(25), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await mintTo( rpc, @@ -377,7 +387,7 @@ describe('e2e transfer with multiple accounts', () => { sender.publicKey, mintAuthority, new BN(25), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); const senderAccounts = await rpc.getCompressedTokenAccountsByOwner( @@ -400,7 +410,7 @@ describe('e2e transfer with multiple accounts', () => { sender, transferAmount, recipient, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); assertTransfer( @@ -421,7 +431,7 @@ async function transferHelper( owner: Signer, amount: BN, toAddress: PublicKey, - merkleTree: PublicKey, + stateTreeInfo: StateTreeInfo, ) { const compressedTokenAccounts = await rpc.getCompressedTokenAccountsByOwner( owner.publicKey, @@ -444,7 +454,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/CHANGELOG.md b/js/stateless.js/CHANGELOG.md index f5986184a8..04a68a0e70 100644 --- a/js/stateless.js/CHANGELOG.md +++ b/js/stateless.js/CHANGELOG.md @@ -1,5 +1,15 @@ # 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 + +- ActiveTreeBundle renamed to StateTreeInfo +- outputStateTree () + ## [0.20.5-0.20.9] - 2025-02-24 ### Bumped to latest compressed-token sdk @@ -21,7 +31,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/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 3a3c13edcb..6072ea1a15 100644 --- a/js/stateless.js/src/actions/compress.ts +++ b/js/stateless.js/src/actions/compress.ts @@ -7,45 +7,50 @@ 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, + selectStateTreeInfo, + 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) { - const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); - const { tree } = pickRandomTreeAndQueue(stateTreeInfo); - outputStateTree = tree; + if (!outputStateTreeInfo) { + const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); + outputStateTreeInfo = selectStateTreeInfo(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..a082a73e13 100644 --- a/js/stateless.js/src/actions/create-account.ts +++ b/js/stateless.js/src/actions/create-account.ts @@ -9,64 +9,64 @@ import { LightSystemProgram, selectMinCompressedSolAccountsForTransfer, } from '../programs'; -import { pickRandomTreeAndQueue, Rpc } from '../rpc'; +import { Rpc } from '../rpc'; import { NewAddressParams, buildAndSignTx, deriveAddress, deriveAddressSeed, + selectStateTreeInfo, sendAndConfirmTx, } from '../utils'; -import { defaultTestStateTreeAccounts } from '../constants'; -import { bn } from '../state'; +import { + addressQueue, + addressTree, + defaultTestStateTreeAccounts, + getDefaultAddressTreeInfo, +} from '../constants'; +import { AddressTreeInfo, 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 addressTreeInfo Optional address tree info. Defaults to a current + * shared address tree. + * @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, payer: Signer, seeds: Uint8Array[], programId: PublicKey, - addressTree?: PublicKey, - addressQueue?: PublicKey, - outputStateTree?: 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 (!outputStateTree) { - const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); - const { tree } = pickRandomTreeAndQueue(stateTreeInfo); - outputStateTree = tree; + if (!outputStateTreeInfo) { + const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); + outputStateTreeInfo = selectStateTreeInfo(stateTreeInfo); } const proof = await rpc.getValidityProofV0(undefined, [ { address: bn(address.toBytes()), - tree: addressTree, - queue: addressQueue, + tree, + queue, }, ]); @@ -83,7 +83,7 @@ export async function createAccount( newAddress: Array.from(address.toBytes()), recentValidityProof: proof.compressedProof, programId, - outputStateTree, + outputStateTreeInfo, }); const tx = buildAndSignTx( @@ -101,32 +101,28 @@ 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 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 * - * @return Transaction signature + * @return Transaction signature */ -// TODO: add support for payer != user owner export async function createAccountWithLamports( rpc: Rpc, payer: Signer, seeds: Uint8Array[], lamports: number | BN, programId: PublicKey, - addressTree?: PublicKey, - addressQueue?: PublicKey, - outputStateTree?: PublicKey, + addressTreeInfo?: AddressTreeInfo, + outputStateTreeInfo?: StateTreeInfo, confirmOptions?: ConfirmOptions, ): Promise { lamports = bn(lamports); @@ -140,28 +136,23 @@ export async function createAccountWithLamports( lamports, ); - if (!outputStateTree) { - const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); - const { tree } = pickRandomTreeAndQueue(stateTreeInfo); - outputStateTree = tree; + if (!outputStateTreeInfo) { + const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); + outputStateTreeInfo = selectStateTreeInfo(stateTreeInfo); } 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: @@ -179,8 +170,7 @@ export async function createAccountWithLamports( recentValidityProof: proof.compressedProof, 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..3e5ffb2ab4 100644 --- a/js/stateless.js/src/actions/decompress.ts +++ b/js/stateless.js/src/actions/decompress.ts @@ -7,34 +7,43 @@ import { } from '@solana/web3.js'; import { LightSystemProgram, sumUpLamports } from '../programs'; import { Rpc } from '../rpc'; -import { buildAndSignTx, sendAndConfirmTx } from '../utils'; +import { + buildAndSignTx, + selectStateTreeInfo, + 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.getCachedActiveStateTreeInfos(); + outputStateTreeInfo = selectStateTreeInfo(stateTreeInfo); + } const userCompressedAccountsWithMerkleContext: CompressedAccountWithMerkleContext[] = (await rpc.getCompressedAccountsByOwner(payer.publicKey)).items; @@ -58,7 +67,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/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/actions/transfer.ts b/js/stateless.js/src/actions/transfer.ts index 7df9d106dc..1a2f14ca7a 100644 --- a/js/stateless.js/src/actions/transfer.ts +++ b/js/stateless.js/src/actions/transfer.ts @@ -13,23 +13,30 @@ 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, + selectStateTreeInfo, + 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 +46,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 +55,11 @@ export async function transfer( const batchSize = 1000; // Maximum allowed by the API lamports = bn(lamports); + if (!outputStateTreeInfo) { + const stateTreeInfo = await rpc.getCachedActiveStateTreeInfos(); + outputStateTreeInfo = selectStateTreeInfo(stateTreeInfo); + } + while (accumulatedLamports.lt(lamports)) { const batchConfig: GetCompressedAccountsByOwnerConfig = { filters: undefined, @@ -95,7 +107,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..e4e423de83 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,36 @@ 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, }, ]; }; +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 getLightStateTreeInfo}. + * For public networks, fetch via {@link defaultStateTreeLookupTables} and {@link getActiveStateTreeInfos}. */ export const defaultTestStateTreeAccounts = () => { return { @@ -145,6 +155,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/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..dd9982b41d 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'; @@ -52,7 +53,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 +111,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 +137,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 +177,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 +306,7 @@ export class LightSystemProgram { newAddressParams, newAddress, recentValidityProof, - outputStateTree, + outputStateTreeInfo, inputCompressedAccounts, inputStateRootIndices, lamports, @@ -326,7 +327,7 @@ export class LightSystemProgram { inputCompressedAccounts ?? [], inputStateRootIndices ?? [], outputCompressedAccounts, - outputStateTree, + outputStateTreeInfo, ); const { newAddressParamsPacked, remainingAccounts } = @@ -372,7 +373,7 @@ export class LightSystemProgram { lamports, recentInputStateRootIndices, recentValidityProof, - outputStateTrees, + outputStateTreeInfo, }: TransferParams): Promise { /// Create output state const outputCompressedAccounts = this.createTransferOutputState( @@ -390,7 +391,7 @@ export class LightSystemProgram { inputCompressedAccounts, recentInputStateRootIndices, outputCompressedAccounts, - outputStateTrees, + outputStateTreeInfo, ); /// Encode instruction data @@ -434,7 +435,7 @@ export class LightSystemProgram { payer, toAddress, lamports, - outputStateTree, + outputStateTreeInfo, }: CompressParams): Promise { /// Create output state lamports = bn(lamports); @@ -453,7 +454,7 @@ export class LightSystemProgram { [], [], [outputCompressedAccount], - outputStateTree, + outputStateTreeInfo, ); /// Encode instruction data @@ -499,7 +500,7 @@ export class LightSystemProgram { lamports, recentInputStateRootIndices, recentValidityProof, - outputStateTree, + outputStateTreeInfo, }: DecompressParams): Promise { /// Create output state lamports = bn(lamports); @@ -518,7 +519,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..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 { @@ -71,9 +72,10 @@ 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-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({ @@ -126,13 +128,13 @@ 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; 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, ), @@ -188,7 +190,7 @@ async function getCompressedTokenAccountsByOwnerOrDelegate( /** @internal */ function buildCompressedAccountWithMaybeTokenData( accountStructWithOptionalTokenData: any, - activeStateTreeInfo: ActiveTreeBundle[], + activeStateTreeInfo: StateTreeInfo[], ): { account: CompressedAccountWithMerkleContext; maybeTokenData: TokenData | null; @@ -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: ActiveTreeBundle[], - 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 * @@ -582,7 +561,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 +576,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,24 +598,25 @@ export class Rpc extends Connection implements CompressionApiInterface { /** * Manually set state tree addresses */ - setStateTreeInfo(info: ActiveTreeBundle[]): void { + setStateTreeInfo(info: StateTreeInfo[]): void { this.activeStateTreeInfo = info; } /** + * * 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(); } - 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 +624,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,9 +646,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(); } /** @@ -730,8 +686,8 @@ export class Rpc extends Connection implements CompressionApiInterface { return null; } - const activeStateTreeInfo = await this.getCachedActiveStateTreeInfo(); - const associatedQueue = getQueueForTree( + const activeStateTreeInfo = await this.getCachedActiveStateTreeInfos(); + const { queue, treeType, tree } = getQueueForTree( activeStateTreeInfo, res.result.value.tree!, ); @@ -739,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, ), @@ -839,8 +795,8 @@ export class Rpc extends Connection implements CompressionApiInterface { `failed to get proof for compressed account ${hash.toString()}`, ); } - const activeStateTreeInfo = await this.getCachedActiveStateTreeInfo(); - const associatedQueue = getQueueForTree( + const activeStateTreeInfo = await this.getCachedActiveStateTreeInfos(); + const { queue, treeType, tree } = getQueueForTree( activeStateTreeInfo, res.result.value.merkleTree, ); @@ -850,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, }; @@ -884,17 +840,17 @@ 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( + 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, ), @@ -940,9 +896,9 @@ 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( + const { queue, treeType, tree } = getQueueForTree( activeStateTreeInfo, proof.merkleTree, ); @@ -951,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, }; @@ -998,17 +954,17 @@ 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( + 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, ), @@ -1257,7 +1213,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/state/types.ts b/js/stateless.js/src/state/types.ts index 1717951314..e326b84461 100644 --- a/js/stateless.js/src/state/types.ts +++ b/js/stateless.js/src/state/types.ts @@ -7,27 +7,73 @@ 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 selectStateTreeInfo} to + * select a random tree from active Trees. + * + * Example: + * ```typescript + * const infos = await getCachedActiveStateTreeInfos(); + * const info = selectStateTreeInfo(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; - queue: PublicKey | null; + /** + * The state nullfier queue belonging to merkleTree. + */ + queue: PublicKey; + /** + * The compressed cpi context account. + */ cpiContext: PublicKey | null; + /** + * The type of tree. One of {@link TreeType}. + */ 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-accounts.ts b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-accounts.ts index d87be0d809..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 @@ -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 infos = 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( + infos, + 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 c010d08491..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 @@ -1,8 +1,9 @@ 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 } from '../../constants'; import { Rpc } from '../../rpc'; +import { getQueueForTree } from './get-compressed-accounts'; import { ParsedTokenAccount, WithCursor } from '../../rpc-interface'; import { CompressedAccount, @@ -21,10 +22,6 @@ import { Layout, } from '@coral-xyz/borsh'; -const tokenProgramId: PublicKey = new PublicKey( - 'cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m', -); - type TokenData = { mint: PublicKey; owner: PublicKey; @@ -55,7 +52,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; @@ -73,24 +70,34 @@ export function parseTokenLayoutWithIdl( /** * parse compressed accounts of an event with token layout * @internal - * TODO: refactor */ async function parseEventWithTokenTlvData( event: PublicTransactionEvent, + rpc: Rpc, ): Promise { const pubkeyArray = event.pubkeyArray; + const infos = await rpc.getCachedActiveStateTreeInfos(); const outputHashes = event.outputCompressedAccountHashes; const outputCompressedAccountsWithParsedTokenData: ParsedTokenAccount[] = event.outputCompressedAccounts.map((compressedAccount, i) => { - const merkleContext: MerkleContext = { - merkleTree: + const maybeTree = + pubkeyArray[event.outputCompressedAccounts[i].merkleTreeIndex]; + + const { queue, treeType, tree } = getQueueForTree(infos, maybeTree); + + 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], }; @@ -134,10 +141,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 @@ -170,14 +178,17 @@ 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), ); return { items: accounts.sort( (a, b) => - b.compressedAccount.leafIndex - a.compressedAccount.leafIndex, + a.compressedAccount.leafIndex - b.compressedAccount.leafIndex, ), cursor: null, }; @@ -190,7 +201,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 => @@ -207,7 +221,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..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/src/test-helpers/test-rpc/test-rpc.ts b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts index aaef9cb7e1..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 @@ -4,6 +4,7 @@ import { getCompressedAccountByHashTest, getCompressedAccountsByOwnerTest, getMultipleCompressedAccountsByHashTest, + getQueueForTree, } from './get-compressed-accounts'; import { getCompressedTokenAccountByHashTest, @@ -14,6 +15,7 @@ import { import { MerkleTree } from '../merkle-tree/merkle-tree'; import { getParsedEvents } from './get-parsed-events'; import { + COMPRESSED_TOKEN_PROGRAM_ID, defaultTestStateTreeAccounts, localTestActiveStateTreeInfo, } from '../../constants'; @@ -42,6 +44,7 @@ import { CompressedAccountWithMerkleContext, MerkleContextWithMerkleProof, PublicTransactionEvent, + TreeType, bn, } from '../../state'; import { IndexedArray } from '../merkle-tree'; @@ -51,7 +54,7 @@ import { convertNonInclusionMerkleProofInputsToHex, proverRequest, } from '../../rpc'; -import { ActiveTreeBundle } from '../../state/types'; +import { StateTreeInfo } from '../../state/types'; export interface TestRpcConfig { /** @@ -110,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, @@ -124,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, }, ); @@ -144,14 +140,10 @@ 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; - activeStateTreeInfo: ActiveTreeBundle[] | null = null; + activeStateTreeInfo: StateTreeInfo[] | null = null; /** * Establish a Compression-compatible JSON RPC mock-connection @@ -178,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; } @@ -207,21 +181,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 getCachedActiveStateTreeInfos(): Promise { return localTestActiveStateTreeInfo(); } /** * Returns local test state trees. */ - async getLatestActiveStateTreeInfo(): Promise { + async getActiveStateTreeInfos(): Promise { return localTestActiveStateTreeInfo(); } @@ -298,6 +272,7 @@ 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 @@ -305,12 +280,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; @@ -318,41 +304,95 @@ 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()); + if (!treeData) { + throw new Error(`Tree not found: ${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 { + throw new Error( + `Unsupported 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 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, + }; + + 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])) { throw new Error( @@ -361,9 +401,15 @@ export class TestRpc extends Connection implements CompressionApiInterface { } }); - return merkleProofs; + // Return proofs in the order of requested hashes + 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. * Owner can be a program or user account @@ -624,8 +670,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 new file mode 100644 index 0000000000..cef4efc4ca --- /dev/null +++ b/js/stateless.js/src/utils/get-state-tree-infos.ts @@ -0,0 +1,108 @@ +import { Connection, PublicKey } from '@solana/web3.js'; +import { AddressTreeInfo, StateTreeInfo, TreeType } from '../state/types'; +import { + defaultTestStateTreeAccounts, + getDefaultAddressTreeInfo, +} from '../constants'; + +/** + * @deprecated use {@link selectStateTreeInfo} 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 selectStateTreeInfo( + 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]; +} + +/** + * Get most recent active state tree data we store in lookup table for each + * public state tree + */ +export async function getActiveStateTreeInfos({ + connection, + stateTreeLookupTableAddress, + nullifyTableAddress, +}: { + connection: Connection; + stateTreeLookupTableAddress: PublicKey; + nullifyTableAddress: PublicKey; +}): Promise { + const stateTreeLookupTable = await connection.getAddressLookupTable( + stateTreeLookupTableAddress, + ); + + if (!stateTreeLookupTable.value) { + throw new Error('State tree lookup table not found'); + } + + if (stateTreeLookupTable.value.state.addresses.length % 3 !== 0) { + throw new Error( + 'State tree lookup table must have a multiple of 3 addresses', + ); + } + + const nullifyTable = + await connection.getAddressLookupTable(nullifyTableAddress); + if (!nullifyTable.value) { + throw new Error('Nullify table not found'); + } + const stateTreePubkeys = stateTreeLookupTable.value.state.addresses; + const nullifyTablePubkeys = nullifyTable.value.state.addresses; + + 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)) { + contexts.push({ + tree, + queue: stateTreePubkeys[i + 1], + cpiContext: stateTreePubkeys[i + 2], + treeType: TreeType.StateV1, + }); + } + } + + return contexts; +} diff --git a/js/stateless.js/src/utils/index.ts b/js/stateless.js/src/utils/index.ts index 6a5101897c..9e9489a959 100644 --- a/js/stateless.js/src/utils/index.ts +++ b/js/stateless.js/src/utils/index.ts @@ -7,4 +7,6 @@ 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'; +export * from './state-tree-lookup-table'; +export * from './dedupe-signer'; diff --git a/js/stateless.js/src/utils/get-light-state-tree-info.ts b/js/stateless.js/src/utils/state-tree-lookup-table.ts similarity index 62% rename from js/stateless.js/src/utils/get-light-state-tree-info.ts rename to js/stateless.js/src/utils/state-tree-lookup-table.ts index e129111b2d..d06d18bc2d 100644 --- a/js/stateless.js/src/utils/get-light-state-tree-info.ts +++ b/js/stateless.js/src/utils/state-tree-lookup-table.ts @@ -1,23 +1,23 @@ import { - AddressLookupTableProgram, - Connection, - Keypair, PublicKey, + Keypair, + Connection, + AddressLookupTableProgram, Signer, } from '@solana/web3.js'; import { buildAndSignTx, sendAndConfirmTx } from './send-and-confirm'; -import { dedupeSigner } from '../actions'; -import { ActiveTreeBundle, TreeType } from '../state/types'; +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 + * @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 +45,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 +56,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 +118,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 +130,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, @@ -202,56 +203,3 @@ export async function nullifyLookupTable({ txId, }; } - -/** - * Get most recent , active state tree data - * we store in lookup table for each public state tree - */ -export async function getLightStateTreeInfo({ - connection, - stateTreeLookupTableAddress, - nullifyTableAddress, -}: { - connection: Connection; - stateTreeLookupTableAddress: PublicKey; - nullifyTableAddress: PublicKey; -}): Promise { - const stateTreeLookupTable = await connection.getAddressLookupTable( - stateTreeLookupTableAddress, - ); - - if (!stateTreeLookupTable.value) { - throw new Error('State tree lookup table not found'); - } - - if (stateTreeLookupTable.value.state.addresses.length % 3 !== 0) { - throw new Error( - 'State tree lookup table must have a multiple of 3 addresses', - ); - } - - const nullifyTable = - await connection.getAddressLookupTable(nullifyTableAddress); - if (!nullifyTable.value) { - throw new Error('Nullify table not found'); - } - const stateTreePubkeys = stateTreeLookupTable.value.state.addresses; - const nullifyTablePubkeys = nullifyTable.value.state.addresses; - - const bundles: ActiveTreeBundle[] = []; - - 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({ - tree, - queue: stateTreePubkeys[i + 1], - cpiContext: stateTreePubkeys[i + 2], - treeType: TreeType.State, - }); - } - } - - return bundles; -} diff --git a/js/stateless.js/tests/e2e/compress.test.ts b/js/stateless.js/tests/e2e/compress.test.ts index 7fe342f496..537bb3824b 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 () => { @@ -86,8 +92,7 @@ describe('compress', () => { ], LightSystemProgram.programId, undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await createAccountWithLamports( @@ -101,9 +106,9 @@ describe('compress', () => { ], 0, LightSystemProgram.programId, + undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await createAccount( @@ -117,8 +122,7 @@ describe('compress', () => { ], LightSystemProgram.programId, undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await createAccount( @@ -132,8 +136,7 @@ describe('compress', () => { ], LightSystemProgram.programId, undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await expect( createAccount( @@ -148,8 +151,7 @@ describe('compress', () => { ], LightSystemProgram.programId, undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ), ).rejects.toThrow(); const postCreateAccountsBalance = await rpc.getBalance(payer.publicKey); @@ -177,7 +179,7 @@ describe('compress', () => { payer, compressLamportsAmount, payer.publicKey, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); const compressedAccounts = await rpc.getCompressedAccountsByOwner( @@ -210,8 +212,7 @@ describe('compress', () => { 100, LightSystemProgram.programId, undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); const postCreateAccountBalance = await rpc.getBalance(payer.publicKey); @@ -228,13 +229,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 +268,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/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) { diff --git a/js/stateless.js/tests/e2e/rpc-interop.test.ts b/js/stateless.js/tests/e2e/rpc-interop.test.ts index ab9a096def..e3c811133b 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,8 @@ import { defaultTestStateTreeAccounts, deriveAddress, deriveAddressSeed, + getDefaultAddressTreeInfo, + selectStateTreeInfo, sleep, } from '../../src'; import { getTestRpc, TestRpc } from '../../src/test-helpers/test-rpc'; @@ -18,12 +21,33 @@ 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); +}; + +// debug helper. +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; let rpc: Rpc; let testRpc: TestRpc; let executedTxs = 0; + let stateTreeInfo: StateTreeInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); rpc = createRpc(); @@ -34,13 +58,10 @@ describe('rpc-interop', () => { payer = await newAccountWithLamports(rpc, 10e9, 256); bob = await newAccountWithLamports(rpc, 10e9, 256); - await compress( - rpc, - payer, - 1e9, - payer.publicKey, - defaultTestStateTreeAccounts().merkleTree, - ); + const stateTreeInfos = await rpc.getActiveStateTreeInfos(); + stateTreeInfo = selectStateTreeInfo(stateTreeInfos); + + await compress(rpc, payer, 1e9, payer.publicKey, stateTreeInfo); executedTxs++; }); @@ -106,11 +127,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++; }); @@ -171,8 +199,7 @@ describe('rpc-interop', () => { newAddressSeedsTest, LightSystemProgram.programId, undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); executedTxs++; @@ -184,8 +211,7 @@ describe('rpc-interop', () => { newAddressSeeds, LightSystemProgram.programId, undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); executedTxs++; }); @@ -316,7 +342,6 @@ describe('rpc-interop', () => { LightSystemProgram.programId, undefined, undefined, - defaultTestStateTreeAccounts().merkleTree, ); executedTxs++; }); @@ -425,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, @@ -535,13 +567,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, stateTreeInfo); executedTxs++; const senderAccounts = await rpc.getCompressedAccountsByOwner( @@ -587,7 +613,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( @@ -656,23 +682,29 @@ 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 address = deriveAddress(seed, addressTree); + const addressTreeInfo = getDefaultAddressTreeInfo(); + const address = deriveAddress(seed, addressTreeInfo.tree); await createAccount( rpc, payer, seeds, LightSystemProgram.programId, - undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + addressTreeInfo, + stateTreeInfo, ); - // fetch the owners latest account const accounts = await rpc.getCompressedAccountsByOwner( payer.publicKey, ); + + const allAccountsTestRpc = await testRpc.getCompressedAccountsByOwner( + payer.publicKey, + ); + const allAccountsRpc = await rpc.getCompressedAccountsByOwner( + payer.publicKey, + ); + const latestAccount = accounts.items[0]; // assert the address was indexed @@ -686,27 +718,27 @@ 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); + + const addressTreeInfo = getDefaultAddressTreeInfo(); + const address = deriveAddress(seed, addressTreeInfo.tree); await createAccount( rpc, payer, seeds, LightSystemProgram.programId, - addressTree, - addressQueue, - defaultTestStateTreeAccounts().merkleTree, + addressTreeInfo, + stateTreeInfo, ); // 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/rpc-multi-trees.test.ts b/js/stateless.js/tests/e2e/rpc-multi-trees.test.ts index 0af156b695..cd745f74d4 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,8 +77,8 @@ describe('rpc-multi-trees', () => { let address: PublicKey; it('must create account with random output tree (pickRandomTreeAndQueue)', async () => { - const tree = pickRandomTreeAndQueue( - await rpc.getCachedActiveStateTreeInfo(), + const tree = selectStateTreeInfo( + await rpc.getCachedActiveStateTreeInfos(), ); const seed = randomBytes(32); @@ -94,8 +94,7 @@ describe('rpc-multi-trees', () => { [seed], LightSystemProgram.programId, undefined, - undefined, - tree.tree, // output state tree + tree, // output state tree ); randTrees.push(tree.tree); @@ -120,18 +119,18 @@ describe('rpc-multi-trees', () => { expect(validityProof.nullifierQueues[0]).toEqual(randQueues[pos]); /// Executes transfers using random output trees - const tree1 = pickRandomTreeAndQueue( - await rpc.getCachedActiveStateTreeInfo(), + 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( - await rpc.getCachedActiveStateTreeInfo(), + 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,8 +194,8 @@ describe('rpc-multi-trees', () => { /// Creates a compressed account with address and lamports using a /// (combined) 'validityProof' from Photon - const tree = pickRandomTreeAndQueue( - await rpc.getCachedActiveStateTreeInfo(), + const tree = selectStateTreeInfo( + await rpc.getCachedActiveStateTreeInfos(), ); await createAccountWithLamports( rpc, @@ -205,8 +204,7 @@ describe('rpc-multi-trees', () => { 0, LightSystemProgram.programId, undefined, - undefined, - tree.tree, + tree, ); executedTxs++; randTrees.push(tree.tree); @@ -238,8 +236,8 @@ describe('rpc-multi-trees', () => { ); }); - const tree = pickRandomTreeAndQueue( - await rpc.getCachedActiveStateTreeInfo(), + const tree = selectStateTreeInfo( + await rpc.getCachedActiveStateTreeInfos(), ); await transfer( rpc, @@ -247,20 +245,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..3ab5235c88 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 () => { @@ -91,6 +78,7 @@ describe('test-rpc', () => { const compressedAccountProof = await rpc.getCompressedAccountProof( bn(refHash), ); + const proof = compressedAccountProof.merkleProof.map(x => x.toString()); expect(proof.length).toStrictEqual(26); @@ -98,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( @@ -107,7 +95,6 @@ describe('test-rpc', () => { compressLamportsAmount, payer, payer.publicKey, - merkleTree, ); const compressedAccounts1 = await rpc.getCompressedAccountsByOwner( payer.publicKey, @@ -122,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 0b42522016..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; diff --git a/js/stateless.js/tests/e2e/transfer.test.ts b/js/stateless.js/tests/e2e/transfer.test.ts index 75a58d62be..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; 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'], }, });