From 0fb2696cf583a20b5ecf3782bf76601b0964e6c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Swen=20Sch=C3=A4ferjohann?= <42959314+SwenSchaeferjohann@users.noreply.github.com> Date: Thu, 10 Apr 2025 02:24:43 +0100 Subject: [PATCH] chore: backport breaking api changes to v1 js SDKs (#1661) * stateless.js add treeinfos * wip * add getTokenPoolInfos * wip * wip * wip - storageoptions * wip * wip * wip * wip - known bug in rpc-interop.test.ts if using random trees * debugged test-rpc in ctoken * all ctoken tests working * rm logs * clean * ctxs to infos, removed redundant getMintProgramId calls * rm deadcode, storageoptions * fix cli getMindProgramId use * fix tokenpool * fix test stateless.js add treeinfos wip add getTokenPoolInfos wip wip wip - storageoptions wip wip wip wip - known bug in rpc-interop.test.ts if using random trees debugged test-rpc in ctoken all ctoken tests working rm logs clean ctxs to infos, removed redundant getMintProgramId calls rm deadcode, storageoptions fix cli getMindProgramId use fix tokenpool fix test update CHANGELOG.md files update changelog update CHANGELOG.md export get-token-pool-infos.ts wip clean wip refactor merklecontext refactor StateTreeInfo wip wip debug test-rpc getCompressedTokenAccountsByOwner fix unit test wip wip wip debug .readUIntLE wip wip wip fix buffer conversion at test-rpc decode wip tests working compressedProof -> validityProof update changelog wip refactor: do not allow output trees for decompress, transfer fmt wip rename getCachedStateTreeInfos upd changelog use with getCachedStateTreeInfos getStateTreeInfos wip wip delegate test wip added transfer-delegated.test.ts added transfer-delegated test cases. all green add tests for decompress-delegated fix rm logs from program update changelog update err msg fix state-tree-luts wip getActiveStateTreeInfos -> getAllStateTreeInfos wip fix sigs for nullifyStateTree wip wip cleanup add getCachedActiveStateTreeInfos mock revert to compressedProof add tokenpools tests update comment update CHANGELOG.md update changelog 0.21.0 update changelog fix fix event parsing post rebase stateless js refactor dedupe types link to computebudgetprogram wip fix DX for instructions: add docstrings to call signatures wip dont break mergeTokenAccounts wip wip add v2 trees to test-rpc v1 mergeable test-rpc tests working rpc-interop tests working with v2 all stateless.js tests working with v2 compressed-token tests working rebase to main fixup cargo lock wip chore: backport breaking api changes to v1 js SDKs (#1661) * stateless.js add treeinfos * wip * add getTokenPoolInfos * wip * wip * wip - storageoptions * wip * wip * wip * wip - known bug in rpc-interop.test.ts if using random trees * debugged test-rpc in ctoken * all ctoken tests working * rm logs * clean * ctxs to infos, removed redundant getMintProgramId calls * rm deadcode, storageoptions * fix cli getMindProgramId use * fix tokenpool * fix test stateless.js add treeinfos wip add getTokenPoolInfos wip wip wip - storageoptions wip wip wip wip - known bug in rpc-interop.test.ts if using random trees debugged test-rpc in ctoken all ctoken tests working rm logs clean ctxs to infos, removed redundant getMintProgramId calls rm deadcode, storageoptions fix cli getMindProgramId use fix tokenpool fix test update CHANGELOG.md files update changelog update CHANGELOG.md export get-token-pool-infos.ts wip clean wip refactor merklecontext refactor StateTreeInfo wip wip debug test-rpc getCompressedTokenAccountsByOwner fix unit test wip wip wip debug .readUIntLE wip wip wip fix buffer conversion at test-rpc decode wip tests working compressedProof -> validityProof update changelog wip refactor: do not allow output trees for decompress, transfer fmt wip rename getCachedStateTreeInfos upd changelog use with getCachedStateTreeInfos getStateTreeInfos wip wip delegate test wip added transfer-delegated.test.ts added transfer-delegated test cases. all green add tests for decompress-delegated fix rm logs from program update changelog update err msg fix state-tree-luts wip getActiveStateTreeInfos -> getAllStateTreeInfos wip fix sigs for nullifyStateTree wip wip cleanup add getCachedActiveStateTreeInfos mock revert to compressedProof add tokenpools tests update comment update CHANGELOG.md update changelog 0.21.0 update changelog fix fix event parsing post rebase stateless js refactor dedupe types link to computebudgetprogram wip fix DX for instructions: add docstrings to call signatures wip dont break mergeTokenAccounts wip wip add v2 trees to test-rpc v1 mergeable test-rpc tests working rpc-interop tests working with v2 all stateless.js tests working with v2 compressed-token tests working rebase to main fixup cargo lock wip wip rename statetreeinfo -> treeinfo wip wip wip cli test works wip --- Cargo.lock | 898 ++++++------- Cargo.toml | 4 + cli/src/commands/compress-spl/index.ts | 5 +- cli/src/commands/decompress-spl/index.ts | 5 +- cli/src/commands/init/index.ts | 2 +- cli/src/commands/start-prover/index.ts | 2 +- examples/browser/nextjs/src/app/page.tsx | 10 +- js/compressed-token/CHANGELOG.md | 96 +- js/compressed-token/package.json | 17 +- js/compressed-token/rollup.config.js | 3 +- .../src/actions/approve-and-mint-to.ts | 67 +- js/compressed-token/src/actions/approve.ts | 84 ++ .../src/actions/compress-spl-token-account.ts | 54 +- js/compressed-token/src/actions/compress.ts | 65 +- .../src/actions/create-mint.ts | 52 +- .../src/actions/create-token-pool.ts | 74 +- .../create-token-program-lookup-table.ts | 52 +- .../src/actions/decompress-delegated.ts | 99 ++ js/compressed-token/src/actions/decompress.ts | 63 +- js/compressed-token/src/actions/index.ts | 14 +- .../src/actions/merge-token-accounts.ts | 31 +- js/compressed-token/src/actions/mint-to.ts | 69 +- js/compressed-token/src/actions/revoke.ts | 75 ++ .../src/actions/transfer-delegated.ts | 79 ++ js/compressed-token/src/actions/transfer.ts | 37 +- js/compressed-token/src/constants.ts | 14 + js/compressed-token/src/idl.ts | 216 ++- js/compressed-token/src/index.ts | 9 +- js/compressed-token/src/instructions/index.ts | 1 - js/compressed-token/src/layout.ts | 394 ++++-- js/compressed-token/src/program.ts | 1168 ++++++++++++----- js/compressed-token/src/types.ts | 55 +- .../src/utils/get-token-pool-infos.ts | 229 ++++ js/compressed-token/src/utils/index.ts | 3 + .../pack-compressed-token-accounts.ts | 53 +- .../src/utils/select-input-accounts.ts | 97 +- js/compressed-token/src/utils/validation.ts | 24 + .../tests/e2e/approve-and-mint-to.test.ts | 21 +- .../e2e/compress-spl-token-account.test.ts | 56 +- .../tests/e2e/compress.test.ts | 89 +- .../tests/e2e/create-token-pool.test.ts | 223 +++- .../tests/e2e/decompress-delegated.test.ts | 208 +++ .../tests/e2e/decompress.test.ts | 31 +- .../tests/e2e/delegate.test.ts | 455 +++++++ js/compressed-token/tests/e2e/layout.test.ts | 218 ++- .../tests/e2e/merge-token-accounts.test.ts | 26 +- js/compressed-token/tests/e2e/mint-to.test.ts | 47 +- .../tests/e2e/multi-pool.test.ts | 194 +++ .../tests/e2e/rpc-multi-trees.test.ts | 93 +- .../tests/e2e/rpc-token-interop.test.ts | 32 +- .../tests/e2e/select-accounts.test.ts | 535 ++++---- .../tests/e2e/transfer-delegated.test.ts | 411 ++++++ .../tests/e2e/transfer.test.ts | 107 +- js/compressed-token/tsconfig.json | 2 +- js/compressed-token/vitest.config.ts | 1 + js/stateless.js/CHANGELOG.md | 129 +- js/stateless.js/package.json | 10 +- js/stateless.js/src/actions/compress.ts | 36 +- js/stateless.js/src/actions/create-account.ts | 113 +- js/stateless.js/src/actions/decompress.ts | 17 +- js/stateless.js/src/actions/index.ts | 2 +- js/stateless.js/src/actions/transfer.ts | 28 +- js/stateless.js/src/constants.ts | 90 +- js/stateless.js/src/index.ts | 4 +- js/stateless.js/src/instruction/index.ts | 1 - js/stateless.js/src/programs/index.ts | 1 - .../src/{ => programs/system}/idl.ts | 56 +- js/stateless.js/src/programs/system/index.ts | 5 + .../src/programs/{ => system}/layout.ts | 78 +- .../system/pack.ts} | 109 +- .../programs/{system.ts => system/program.ts} | 117 +- .../system/select-compressed-accounts.ts | 38 + js/stateless.js/src/rpc-interface.ts | 240 +++- js/stateless.js/src/rpc.ts | 897 +++++++------ js/stateless.js/src/state/BN254.ts | 13 +- js/stateless.js/src/state/bn.ts | 12 + .../src/state/compressed-account.ts | 119 +- js/stateless.js/src/state/index.ts | 1 + js/stateless.js/src/state/types.ts | 461 ++++++- .../test-rpc/get-compressed-accounts.ts | 19 +- .../test-rpc/get-compressed-token-accounts.ts | 86 +- .../test-rpc/get-parsed-events.ts | 9 +- .../src/test-helpers/test-rpc/test-rpc.ts | 515 +++++--- js/stateless.js/src/utils/address.ts | 4 +- .../src/utils/calculate-compute-unit-price.ts | 2 +- js/stateless.js/src/utils/conversion.ts | 9 +- .../common.ts => utils/dedupe-signer.ts} | 0 .../src/utils/get-state-tree-infos.ts | 213 +++ js/stateless.js/src/utils/index.ts | 7 +- .../src/utils/parse-validity-proof.ts | 22 +- ...ree-info.ts => state-tree-lookup-table.ts} | 157 +-- js/stateless.js/tests/e2e/compress.test.ts | 59 +- js/stateless.js/tests/e2e/layout.test.ts | 13 +- js/stateless.js/tests/e2e/rpc-interop.test.ts | 179 +-- .../tests/e2e/rpc-multi-trees.test.ts | 142 +- js/stateless.js/tests/e2e/test-rpc.test.ts | 36 +- js/stateless.js/tests/e2e/testnet.test.ts | 12 +- js/stateless.js/tests/e2e/transfer.test.ts | 10 +- .../pack-compressed-accounts.test.ts | 22 +- .../tests/unit/state/bn254.test.ts | 2 +- .../unit/state/compressed-account.test.ts | 46 +- .../tests/unit/utils/conversion.test.ts | 18 +- .../tests/unit/utils/tree-info.test.ts | 129 ++ js/stateless.js/vitest.config.ts | 5 +- pnpm-lock.yaml | 77 ++ programs/system/src/context.rs | 11 +- rust-toolchain.toml | 1 + 107 files changed, 8007 insertions(+), 3344 deletions(-) create mode 100644 js/compressed-token/src/actions/approve.ts create mode 100644 js/compressed-token/src/actions/decompress-delegated.ts create mode 100644 js/compressed-token/src/actions/revoke.ts create mode 100644 js/compressed-token/src/actions/transfer-delegated.ts delete mode 100644 js/compressed-token/src/instructions/index.ts create mode 100644 js/compressed-token/src/utils/get-token-pool-infos.ts rename js/compressed-token/src/{instructions => utils}/pack-compressed-token-accounts.ts (72%) create mode 100644 js/compressed-token/src/utils/validation.ts create mode 100644 js/compressed-token/tests/e2e/decompress-delegated.test.ts create mode 100644 js/compressed-token/tests/e2e/delegate.test.ts create mode 100644 js/compressed-token/tests/e2e/multi-pool.test.ts create mode 100644 js/compressed-token/tests/e2e/transfer-delegated.test.ts delete mode 100644 js/stateless.js/src/instruction/index.ts rename js/stateless.js/src/{ => programs/system}/idl.ts (97%) create mode 100644 js/stateless.js/src/programs/system/index.ts rename js/stateless.js/src/programs/{ => system}/layout.ts (91%) rename js/stateless.js/src/{instruction/pack-compressed-accounts.ts => programs/system/pack.ts} (57%) rename js/stateless.js/src/programs/{system.ts => system/program.ts} (82%) create mode 100644 js/stateless.js/src/programs/system/select-compressed-accounts.ts create mode 100644 js/stateless.js/src/state/bn.ts rename js/stateless.js/src/{actions/common.ts => utils/dedupe-signer.ts} (100%) create mode 100644 js/stateless.js/src/utils/get-state-tree-infos.ts rename js/stateless.js/src/utils/{get-light-state-tree-info.ts => state-tree-lookup-table.ts} (53%) create mode 100644 js/stateless.js/tests/unit/utils/tree-info.test.ts diff --git a/Cargo.lock b/Cargo.lock index 4414ad6cb5..e232d96bbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "Inflector" @@ -35,7 +35,7 @@ dependencies = [ "rand 0.8.5", "solana-sdk", "solana-security-txt", - "zerocopy 0.8.25", + "zerocopy", ] [[package]] @@ -124,15 +124,15 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.2.15", + "getrandom 0.3.3", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -150,7 +150,7 @@ version = "1.1.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -321,7 +321,7 @@ dependencies = [ "regex", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -363,7 +363,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "syn 1.0.109", "thiserror 1.0.69", ] @@ -394,9 +394,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -409,36 +409,36 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] @@ -500,7 +500,7 @@ dependencies = [ "ark-std 0.5.0", "educe 0.6.0", "fnv", - "hashbrown 0.15.2", + "hashbrown 0.15.3", "itertools 0.13.0", "num-bigint 0.4.6", "num-integer", @@ -565,7 +565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -591,7 +591,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -619,7 +619,7 @@ dependencies = [ "ark-std 0.5.0", "educe 0.6.0", "fnv", - "hashbrown 0.15.2", + "hashbrown 0.15.3", ] [[package]] @@ -666,7 +666,7 @@ checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -760,9 +760,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64" +checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07" dependencies = [ "brotli", "flate2", @@ -791,7 +791,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -819,9 +819,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -885,9 +885,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" dependencies = [ "serde", ] @@ -906,9 +906,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389a099b34312839e16420d499a9cad9650541715937ffbdd40d36f49e77eeb3" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" dependencies = [ "arrayref", "arrayvec", @@ -979,7 +979,7 @@ dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1006,9 +1006,9 @@ dependencies = [ [[package]] name = "brotli" -version = "7.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -1017,9 +1017,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.3" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -1036,9 +1036,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" [[package]] name = "bv" @@ -1052,15 +1052,15 @@ dependencies = [ [[package]] name = "bytecount" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "bytemuck" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" dependencies = [ "bytemuck_derive", ] @@ -1073,7 +1073,7 @@ checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1105,14 +1105,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a98356df42a2eb1bd8f1793ae4ee4de48e384dd974ce5eac8eee802edb7492be" dependencies = [ "serde", - "toml 0.8.20", + "toml 0.8.23", ] [[package]] name = "cc" -version = "1.2.19" +version = "1.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" +checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" dependencies = [ "jobserver", "libc", @@ -1145,14 +1145,14 @@ checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1190,9 +1190,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.37" +version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" +checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" dependencies = [ "clap_builder", "clap_derive", @@ -1200,9 +1200,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.37" +version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" +checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" dependencies = [ "anstream", "anstyle", @@ -1219,7 +1219,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1267,9 +1267,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "combine" @@ -1380,9 +1380,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -1547,7 +1547,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1571,7 +1571,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1582,7 +1582,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1741,7 +1741,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1764,7 +1764,7 @@ checksum = "a6cbae11b3de8fce2a456e8ea3dada226b35fe791f0dc1d360c0941f0bb681f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1826,7 +1826,7 @@ dependencies = [ "derivation-path", "ed25519-dalek", "hmac 0.12.1", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -1850,7 +1850,7 @@ dependencies = [ "enum-ordinalize 4.3.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1891,7 +1891,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1904,7 +1904,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1924,7 +1924,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1971,9 +1971,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", "windows-sys 0.59.0", @@ -2006,6 +2006,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fastbloom" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27cea6e7f512d43b098939ff4d5a5d6fe3db07971e1d05176fe26c642d33f5b8" +dependencies = [ + "getrandom 0.3.3", + "rand 0.9.1", + "siphasher 1.0.1", + "wide", +] + [[package]] name = "fastmurmur3" version = "0.2.0" @@ -2087,7 +2099,7 @@ dependencies = [ "bb8", "borsh 0.10.4", "bs58", - "clap 4.5.37", + "clap 4.5.39", "create-address-test-program", "dashmap 6.1.0", "dotenvy", @@ -2239,7 +2251,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2313,9 +2325,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", @@ -2326,9 +2338,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "js-sys", @@ -2375,7 +2387,7 @@ dependencies = [ "futures-sink", "futures-timer", "futures-util", - "getrandom 0.3.2", + "getrandom 0.3.3", "no-std-compat", "nonzero_ext", "parking_lot", @@ -2416,7 +2428,7 @@ dependencies = [ "indexmap 2.9.0", "slab", "tokio", - "tokio-util 0.7.14", + "tokio-util 0.7.15", "tracing", ] @@ -2435,7 +2447,7 @@ dependencies = [ "indexmap 2.9.0", "slab", "tokio", - "tokio-util 0.7.14", + "tokio-util 0.7.15", "tracing", ] @@ -2471,9 +2483,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" dependencies = [ "allocator-api2", ] @@ -2528,9 +2540,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" [[package]] name = "hex" @@ -2721,14 +2733,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.6" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.3.1", "hyper 1.6.0", "hyper-util", - "rustls 0.23.26", + "rustls 0.23.27", "rustls-pki-types", "tokio", "tokio-rustls 0.26.2", @@ -2767,9 +2779,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ "base64 0.22.1", "bytes", @@ -2817,21 +2829,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -2840,31 +2853,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -2872,67 +2865,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -2952,9 +2932,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -2978,7 +2958,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.3", "serde", ] @@ -3070,9 +3050,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.8" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ad87c89110f55e4cd4dc2893a9790820206729eaf221555f742d540b0724a0" +checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" dependencies = [ "jiff-static", "log", @@ -3083,13 +3063,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.8" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d076d5b64a7e2fe6f0743f02c43ca4a6725c0f904203bfe276a5b3e793103605" +checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3120,7 +3100,7 @@ version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", "libc", ] @@ -3176,7 +3156,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "libc", ] @@ -3269,7 +3249,7 @@ dependencies = [ "solana-sysvar", "thiserror 2.0.12", "tokio", - "zerocopy 0.8.25", + "zerocopy", ] [[package]] @@ -3362,7 +3342,7 @@ dependencies = [ "solana-program-error", "solana-pubkey", "thiserror 2.0.12", - "zerocopy 0.8.25", + "zerocopy", ] [[package]] @@ -3383,7 +3363,7 @@ dependencies = [ "solana-security-txt", "spl-token", "spl-token-2022 7.0.0", - "zerocopy 0.8.25", + "zerocopy", ] [[package]] @@ -3433,7 +3413,7 @@ dependencies = [ "num-bigint 0.4.6", "pinocchio", "rand 0.8.5", - "sha2 0.10.8", + "sha2 0.10.9", "sha3", "solana-program-error", "solana-pubkey", @@ -3482,7 +3462,7 @@ dependencies = [ "bs58", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3498,7 +3478,7 @@ dependencies = [ "solana-program-error", "solana-sysvar", "thiserror 2.0.12", - "zerocopy 0.8.25", + "zerocopy", ] [[package]] @@ -3653,7 +3633,7 @@ dependencies = [ "proc-macro2", "quote", "solana-pubkey", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3678,7 +3658,7 @@ dependencies = [ "anchor-lang", "light-compressed-account", "light-zero-copy", - "zerocopy 0.8.25", + "zerocopy", ] [[package]] @@ -3704,7 +3684,7 @@ dependencies = [ "solana-pubkey", "solana-security-txt", "thiserror 2.0.12", - "zerocopy 0.8.25", + "zerocopy", ] [[package]] @@ -3765,7 +3745,7 @@ dependencies = [ "rand 0.8.5", "solana-program-error", "thiserror 2.0.12", - "zerocopy 0.8.25", + "zerocopy", ] [[package]] @@ -3776,9 +3756,9 @@ checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "litesvm" @@ -3845,9 +3825,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -3859,6 +3839,12 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.1.0" @@ -3937,13 +3923,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3987,7 +3973,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg-if", "cfg_aliases", "libc", @@ -4087,7 +4073,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4133,11 +4119,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi 0.5.1", "libc", ] @@ -4159,7 +4145,7 @@ dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4192,6 +4178,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -4200,11 +4192,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.72" +version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg-if", "foreign-types", "libc", @@ -4221,7 +4213,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4232,9 +4224,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.107" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", @@ -4292,9 +4284,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -4302,9 +4294,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -4391,7 +4383,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4452,9 +4444,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" @@ -4465,6 +4457,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -4477,7 +4478,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.25", + "zerocopy", ] [[package]] @@ -4488,12 +4489,12 @@ checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" [[package]] name = "prettyplease" -version = "0.2.32" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d" dependencies = [ "proc-macro2", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4533,7 +4534,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4583,7 +4584,7 @@ checksum = "9e2e25ee72f5b24d773cae88422baddefff7714f97aab68d96fe2b6fc4a28fb2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4603,9 +4604,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.7" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", "cfg_aliases", @@ -4613,7 +4614,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.26", + "rustls 0.23.27", "socket2", "thiserror 2.0.12", "tokio", @@ -4623,16 +4624,18 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.10" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" dependencies = [ "bytes", - "getrandom 0.3.2", + "fastbloom", + "getrandom 0.3.3", + "lru-slab", "rand 0.9.1", "ring", "rustc-hash 2.1.1", - "rustls 0.23.26", + "rustls 0.23.27", "rustls-pki-types", "rustls-platform-verifier", "slab", @@ -4644,9 +4647,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" +checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" dependencies = [ "cfg_aliases", "libc", @@ -4756,7 +4759,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -4765,7 +4768,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", ] [[package]] @@ -4783,7 +4786,7 @@ version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -4808,11 +4811,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -4821,7 +4824,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "libredox", "thiserror 1.0.69", ] @@ -4832,7 +4835,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "libredox", "thiserror 2.0.12", ] @@ -4939,7 +4942,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls 0.24.1", - "tokio-util 0.7.14", + "tokio-util 0.7.15", "tower-service", "url", "wasm-bindgen", @@ -4966,7 +4969,7 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "hyper 1.6.0", - "hyper-rustls 0.27.6", + "hyper-rustls 0.27.7", "hyper-tls 0.6.0", "hyper-util", "ipnet", @@ -4979,7 +4982,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.26", + "rustls 0.23.27", "rustls-pki-types", "serde", "serde_json", @@ -5021,7 +5024,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -5029,23 +5032,23 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.3.1" +version = "7.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" dependencies = [ "libc", "rtoolbox", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] name = "rtoolbox" -version = "0.0.2" +version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5086,11 +5089,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.5" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys", @@ -5111,14 +5114,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.26" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.1", + "rustls-webpki 0.103.3", "subtle", "zeroize", ] @@ -5146,31 +5149,32 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", + "zeroize", ] [[package]] name = "rustls-platform-verifier" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5467026f437b4cb2a533865eaa73eb840019a0916f4b9ec563c6e617e086c9" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" dependencies = [ - "core-foundation 0.10.0", + "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", "once_cell", - "rustls 0.23.26", + "rustls 0.23.27", "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.103.1", + "rustls-webpki 0.103.3", "security-framework 3.2.0", "security-framework-sys", - "webpki-root-certs", + "webpki-root-certs 0.26.11", "windows-sys 0.59.0", ] @@ -5192,9 +5196,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.1" +version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "ring", "rustls-pki-types", @@ -5203,9 +5207,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" @@ -5213,6 +5217,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + [[package]] name = "same-file" version = "1.0.6" @@ -5224,9 +5237,9 @@ dependencies = [ [[package]] name = "scc" -version = "2.3.3" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea091f6cac2595aa38993f04f4ee692ed43757035c36e67c180b6828356385b1" +checksum = "22b2d775fb28f245817589471dd49c5edf64237f4a19d10ce9a92ff4651a27f4" dependencies = [ "sdd", ] @@ -5305,7 +5318,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -5318,8 +5331,8 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 2.9.0", - "core-foundation 0.10.0", + "bitflags 2.9.1", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -5376,7 +5389,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -5393,9 +5406,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -5439,7 +5452,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -5477,7 +5490,7 @@ checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -5506,9 +5519,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -5548,9 +5561,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -5558,9 +5571,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -5577,6 +5590,12 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -5588,15 +5607,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -6242,9 +6261,9 @@ dependencies = [ [[package]] name = "solana-decode-error" -version = "2.2.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a6a6383af236708048f8bd8d03db8ca4ff7baf4a48e5d580f4cce545925470" +checksum = "8c781686a18db2f942e70913f7ca15dc120ec38dcab42ff7557db2c70c625a35" dependencies = [ "num-traits", ] @@ -6311,7 +6330,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c5fd2662ae7574810904585fd443545ed2b568dbd304b25a31e79ccc76e81b" dependencies = [ - "siphasher", + "siphasher 0.3.11", "solana-hash", "solana-pubkey", ] @@ -6504,7 +6523,7 @@ checksum = "9ce496a475e5062ba5de97215ab39d9c358f9c9df4bb7f3a45a1f1a8bd9065ed" dependencies = [ "bincode", "borsh 1.5.7", - "getrandom 0.2.15", + "getrandom 0.2.16", "js-sys", "num-traits", "serde", @@ -6516,11 +6535,11 @@ dependencies = [ [[package]] name = "solana-instructions-sysvar" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427f2d0d6dc0bb49f16cef5e7f975180d2e80aab9bdd3b2af68e2d029ec63f43" +checksum = "e0e85a6fad5c2d0c4f5b91d34b8ca47118fc593af706e523cdbedf846a954f57" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "solana-account-info", "solana-instruction", "solana-program-error", @@ -6800,7 +6819,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "004f2d2daf407b3ec1a1ca5ec34b3ccdfd6866dd2d3c7d0715004a96e4b6d127" dependencies = [ "bincode", - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg_eval", "serde", "serde_derive", @@ -6863,9 +6882,9 @@ dependencies = [ [[package]] name = "solana-precompile-error" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ff64daa2933c22982b323d88d0cdf693201ef56ac381ae16737fd5f579e07d6" +checksum = "4d87b2c1f5de77dfe2b175ee8dd318d196aaca4d0f66f02842f80c852811f9f8" dependencies = [ "num-traits", "solana-decode-error", @@ -6913,7 +6932,7 @@ dependencies = [ "bytemuck", "console_error_panic_hook", "console_log", - "getrandom 0.2.15", + "getrandom 0.2.16", "lazy_static", "log", "memoffset", @@ -6993,9 +7012,9 @@ dependencies = [ [[package]] name = "solana-program-error" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8ae2c1a8d0d4ae865882d5770a7ebca92bab9c685e43f0461682c6c05a35bfa" +checksum = "9ee2e0217d642e2ea4bee237f37bd61bb02aec60da3647c48ff88f6556ade775" dependencies = [ "borsh 1.5.7", "num-traits", @@ -7086,7 +7105,7 @@ dependencies = [ "bytemuck_derive", "curve25519-dalek 4.1.3", "five8_const", - "getrandom 0.2.15", + "getrandom 0.2.16", "js-sys", "num-traits", "rand 0.8.5", @@ -7141,7 +7160,7 @@ dependencies = [ "log", "quinn", "quinn-proto", - "rustls 0.23.26", + "rustls 0.23.27", "solana-connection-cache", "solana-keypair", "solana-measure", @@ -7461,7 +7480,7 @@ dependencies = [ "bs58", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -7531,7 +7550,7 @@ checksum = "36187af2324f079f65a675ec22b31c24919cb4ac22c79472e85d819db9bbbc15" dependencies = [ "hmac 0.12.1", "pbkdf2 0.11.0", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -7569,7 +7588,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0037386961c0d633421f53560ad7c80675c0447cba4d1bb66d60974dd486c7ea" dependencies = [ - "sha2 0.10.8", + "sha2 0.10.9", "solana-define-syscall", "solana-hash", ] @@ -7730,7 +7749,7 @@ dependencies = [ "quinn", "quinn-proto", "rand 0.8.5", - "rustls 0.23.26", + "rustls 0.23.27", "smallvec", "socket2", "solana-keypair", @@ -7749,7 +7768,7 @@ dependencies = [ "solana-transaction-metrics-tracker", "thiserror 2.0.12", "tokio", - "tokio-util 0.7.14", + "tokio-util 0.7.15", "x509-parser", ] @@ -7923,7 +7942,7 @@ version = "2.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec21c6c242ee93642aa50b829f5727470cdbdf6b461fb7323fe4bc31d1b54c08" dependencies = [ - "rustls 0.23.26", + "rustls 0.23.27", "solana-keypair", "solana-pubkey", "solana-signer", @@ -8372,7 +8391,7 @@ checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750" dependencies = [ "quote", "spl-discriminator-syn", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -8383,8 +8402,8 @@ checksum = "8c1f05593b7ca9eac7caca309720f2eafb96355e037e6d373b909a80fe7b69b9" dependencies = [ "proc-macro2", "quote", - "sha2 0.10.8", - "syn 2.0.100", + "sha2 0.10.9", + "syn 2.0.101", "thiserror 1.0.69", ] @@ -8456,8 +8475,8 @@ checksum = "e6d375dd76c517836353e093c2dbb490938ff72821ab568b545fd30ab3256b3e" dependencies = [ "proc-macro2", "quote", - "sha2 0.10.8", - "syn 2.0.100", + "sha2 0.10.9", + "syn 2.0.101", ] [[package]] @@ -8727,9 +8746,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -8765,13 +8784,13 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -8791,7 +8810,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -8920,7 +8939,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -8975,12 +8994,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.19.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", @@ -9039,7 +9058,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -9050,7 +9069,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -9115,9 +9134,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -9185,7 +9204,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -9214,7 +9233,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.26", + "rustls 0.23.27", "tokio", ] @@ -9289,9 +9308,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -9311,9 +9330,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -9323,26 +9342,33 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.9.0", "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.2" @@ -9360,11 +9386,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc2d9e086a412a451384326f521c8123a99a466b329941a9403696bff9b0da2" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "bytes", "futures-util", "http 1.3.1", @@ -9414,20 +9440,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -9643,12 +9669,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -9663,12 +9683,14 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", + "js-sys", "serde", + "wasm-bindgen", ] [[package]] @@ -9744,7 +9766,7 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-tungstenite 0.21.0", - "tokio-util 0.7.14", + "tokio-util 0.7.15", "tower-service", "tracing", ] @@ -9792,7 +9814,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "wasm-bindgen-shared", ] @@ -9827,7 +9849,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9863,9 +9885,18 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "0.26.8" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09aed61f5e8d2c18344b3faa33a4c837855fe56642757754775548fee21386c4" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.0", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a83f7e1a9f8712695c03eabe9ed3fbca0feff0152f33f12593e5a6303cb1a4" dependencies = [ "rustls-pki-types", ] @@ -9894,6 +9925,16 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "wide" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b5576b9a81633f3e8df296ce0063042a73507636cbe956c61133dd7034ab22" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "winapi" version = "0.3.9" @@ -9927,36 +9968,37 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.57.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-result 0.1.2", - "windows-targets 0.52.6", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] name = "windows-implement" -version = "0.57.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] name = "windows-interface" -version = "0.57.0" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -9967,22 +10009,13 @@ checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-registry" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" dependencies = [ - "windows-result 0.3.4", + "windows-link", + "windows-result", "windows-strings", - "windows-targets 0.53.0", -] - -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", ] [[package]] @@ -9996,9 +10029,9 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] @@ -10078,29 +10111,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -10119,12 +10136,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -10143,12 +10154,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -10167,24 +10172,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -10203,12 +10196,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -10227,12 +10214,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -10251,12 +10232,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -10275,17 +10250,11 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - [[package]] name = "winnow" -version = "0.7.6" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] @@ -10306,20 +10275,14 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "wyz" @@ -10356,7 +10319,7 @@ dependencies = [ "anyhow", "ark-bn254 0.5.0", "ark-ff 0.5.0", - "clap 4.5.37", + "clap 4.5.39", "dirs", "groth16-solana", "light-batched-merkle-tree", @@ -10374,7 +10337,7 @@ dependencies = [ "quote", "rand 0.8.5", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "solana-client", "solana-program", "solana-sdk", @@ -10385,9 +10348,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -10397,23 +10360,14 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", - "synstructure 0.13.1", -] - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "zerocopy-derive 0.7.35", + "syn 2.0.101", + "synstructure 0.13.2", ] [[package]] @@ -10422,18 +10376,7 @@ version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ - "zerocopy-derive 0.8.25", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", + "zerocopy-derive", ] [[package]] @@ -10444,7 +10387,7 @@ checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -10464,8 +10407,8 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", - "synstructure 0.13.1", + "syn 2.0.101", + "synstructure 0.13.2", ] [[package]] @@ -10485,14 +10428,25 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", ] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -10501,13 +10455,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index cd00d86c91..815598d32c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,6 +112,10 @@ anchor-spl = "=0.31.1" # Anchor compatibility borsh = "0.10.0" +# Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + # Macro helpers proc-macro2 = "1.0" quote = "1.0" diff --git a/cli/src/commands/compress-spl/index.ts b/cli/src/commands/compress-spl/index.ts index f1c4c6d6d6..63582cfe11 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(), ); @@ -73,9 +73,6 @@ class CompressSplCommand extends Command { payer, sourceAta, toPublicKey, - undefined, - undefined, - tokenProgramId, ); loader.stop(false); diff --git a/cli/src/commands/decompress-spl/index.ts b/cli/src/commands/decompress-spl/index.ts index 0b61c53f88..de64e9f006 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(), ); @@ -75,9 +75,6 @@ class DecompressSplCommand extends Command { amount, payer, recipientAta.address, - undefined, - undefined, - tokenProgramId, ); loader.stop(false); diff --git a/cli/src/commands/init/index.ts b/cli/src/commands/init/index.ts index 5db1875482..3fe2bc61ed 100644 --- a/cli/src/commands/init/index.ts +++ b/cli/src/commands/init/index.ts @@ -28,7 +28,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/cli/src/commands/start-prover/index.ts b/cli/src/commands/start-prover/index.ts index d47d34e929..792e75f935 100644 --- a/cli/src/commands/start-prover/index.ts +++ b/cli/src/commands/start-prover/index.ts @@ -62,7 +62,7 @@ class StartProver extends Command { const proverPort = flags["prover-port"] || 3001; const force = flags["force"] || false; - const redisUrl = flags["redisUrl"] || process.env.REDIS_URL; + const redisUrl = flags["redisUrl"] || process.env.REDIS_URL || undefined; await startProver( proverPort, flags["run-mode"], diff --git a/examples/browser/nextjs/src/app/page.tsx b/examples/browser/nextjs/src/app/page.tsx index 570afe8168..e37162f92a 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.getStateTreeInfos(), + ); 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,6 @@ const SendButton: FC = () => { toAddress: recipient, lamports: 1e7, inputCompressedAccounts: selectedAccounts, - outputStateTrees: [defaultTestStateTreeAccounts().merkleTree], recentValidityProof: compressedProof, recentInputStateRootIndices: rootIndices, }); diff --git a/js/compressed-token/CHANGELOG.md b/js/compressed-token/CHANGELOG.md index ee03edd307..5908f2f8fc 100644 --- a/js/compressed-token/CHANGELOG.md +++ b/js/compressed-token/CHANGELOG.md @@ -1,3 +1,97 @@ +## [0.22.0] + +- `CreateMint` action now allows passing a non-payer mint and freeze authority. +- More efficient computebudgets for actions. +- Better DX: Parameter lookup in call signatures of CompressedTokenProgram instructions +- QoL: improved typedocs. + +## [0.21.0] + +#### Breaking Changes + +This release has several breaking changes which improve protocol +scalability. Please reach out to the [team](https://t.me/swen_light) if you need help migrating. + +### Migration guide: Compress + +**Old Code** (remove this) + +```typescript +const activeStateTrees = await connection.getCachedActiveStateTreeInfo(); +const { tree } = pickRandomTreeAndQueue(activeStateTrees); +// ... +``` + +**New Code** + +```typescript +// ... +const treeInfos = await rpc.getStateTreeInfos(); +const treeInfo = selectStateTreeInfo(treeInfos); + +const infos = await getTokenPoolInfos(rpc, mint); +const info = selectTokenPoolInfo(infos); + +const compressIx = await CompressedTokenProgram.compress({ + // ... + outputStateTreeInfo, + tokenPoolInfo, +}); +``` + +### Migration guide: Decompress + +```typescript +// ... +// new code: +const infos = await getTokenPoolInfos(rpc, mint); +const selectedInfos = selectTokenPoolInfosForDecompression( + tokenPoolInfos, + amount, +); + +const ix = await CompressedTokenProgram.decompress({ + // ... + tokenPoolInfos: selectedTokenPoolInfos, +}); +``` + +### Overview + +- new type: TokenPoolInfo +- Instruction Changes: + + - `compress`, `mintTo`, `approveAndMintTo`, `compressSplTokenAccount` now require valid TokenPoolInfo + - `decompress` now requires an array of one or more TokenPoolInfos. + - `decompress`, `transfer` now do not allow state tree overrides. + +- Action Changes: + + - Removed optional tokenProgramId: PublicKey + - removed optional merkleTree: PublicKey + - removed optional outputStateTree: PublicKey + - added optional stateTreeInfo: StateTreeInfo + - added optional tokenPoolInfo: TokenPoolInfo + +- new instructions: + - `approve`, `revoke`: delegated transfer support. + - `addTokenPools`: you can now register additional token pool pdas. Use + this if you need very high concurrency. + +### Why the Changes are helpful + +`getStateTreeInfos()` retrieves relevant info about all active state trees. + +When building a transaction you can now pick a random treeInfo via `selectStateTreeInfo(infos)`. + +This lets you and other apps execute more transactions within Solana's write lock +limits. + +The same applies to `getTokenPoolInfos`. When you compress or decompress SPL +tokens, a tokenpool gets write-locked. If you need additional per-block write +lock capacity, you can register and sample additional (up to 4) tokenpool +accounts. + ## [0.20.5-0.20.9] - 2025-02-24 ### Changed @@ -17,7 +111,7 @@ ### Added - `selectSmartCompressedTokenAccountsForTransfer` and - `selectSmartCompressedTokenAccountsForTransferorPartial` + `selectSmartCompressedTokenAccountsForTransferOrPartial` ### Changed diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index df3cfa42bb..9b0fab25b5 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/compressed-token", - "version": "0.20.9", + "version": "0.21.0", "description": "JS client to interact with the compressed-token program", "sideEffects": false, "main": "dist/cjs/node/index.cjs", @@ -82,20 +82,25 @@ "test:unit:all": "EXCLUDE_E2E=true vitest run", "test-all:verbose": "vitest run --reporter=verbose", "test-validator": "./../../cli/test_bin/run test-validator --prover-run-mode rpc", + "test-validator-skip-prover": "./../../cli/test_bin/run test-validator --skip-prover", "test:e2e:create-mint": "pnpm test-validator && NODE_OPTIONS='--trace-deprecation' vitest run tests/e2e/create-mint.test.ts --reporter=verbose", - "test:e2e:layout": "vitest run tests/e2e/layout.test.ts --reporter=verbose", + "test:e2e:layout": "vitest run tests/e2e/layout.test.ts --reporter=verbose --bail=1", "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:mint-to": "pnpm test-validator && vitest run tests/e2e/mint-to.test.ts --reporter=verbose --bail=1", + "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:delegate": "pnpm test-validator && vitest run tests/e2e/delegate.test.ts --reporter=verbose --bail=1", + "test:e2e:transfer-delegated": "pnpm test-validator && vitest run tests/e2e/transfer-delegated.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", + "test:e2e:decompress-delegated": "pnpm test-validator && vitest run tests/e2e/decompress-delegated.test.ts --reporter=verbose", "test:e2e:rpc-token-interop": "pnpm test-validator && vitest run tests/e2e/rpc-token-interop.test.ts --reporter=verbose", "test:e2e:rpc-multi-trees": "pnpm test-validator && vitest run tests/e2e/rpc-multi-trees.test.ts --reporter=verbose", - "test:e2e:all": "pnpm test-validator && vitest run tests/e2e/create-mint.test.ts && vitest run tests/e2e/mint-to.test.ts && vitest run tests/e2e/transfer.test.ts && vitest run tests/e2e/compress.test.ts && vitest run tests/e2e/compress-spl-token-account.test.ts && vitest run tests/e2e/decompress.test.ts && vitest run tests/e2e/create-token-pool.test.ts && vitest run tests/e2e/approve-and-mint-to.test.ts && vitest run tests/e2e/rpc-token-interop.test.ts && vitest run tests/e2e/rpc-multi-trees.test.ts && vitest run tests/e2e/layout.test.ts && vitest run tests/e2e/select-accounts.test.ts", + "test:e2e:multi-pool": "pnpm test-validator && vitest run tests/e2e/multi-pool.test.ts --reporter=verbose", + "test:e2e:all": "pnpm test-validator && vitest run tests/e2e/create-mint.test.ts && vitest run tests/e2e/mint-to.test.ts && vitest run tests/e2e/transfer.test.ts && vitest run tests/e2e/delegate.test.ts && vitest run tests/e2e/transfer-delegated.test.ts && vitest run tests/e2e/multi-pool.test.ts && vitest run tests/e2e/decompress-delegated.test.ts && pnpm test-validator-skip-prover && vitest run tests/e2e/compress.test.ts && vitest run tests/e2e/compress-spl-token-account.test.ts && vitest run tests/e2e/decompress.test.ts && vitest run tests/e2e/create-token-pool.test.ts && vitest run tests/e2e/approve-and-mint-to.test.ts && vitest run tests/e2e/rpc-token-interop.test.ts && vitest run tests/e2e/rpc-multi-trees.test.ts && vitest run tests/e2e/layout.test.ts && vitest run tests/e2e/select-accounts.test.ts", "pull-idl": "../../scripts/push-compressed-token-idl.sh", "build": "rimraf dist && pnpm build:bundle", "build:bundle": "rollup -c", diff --git a/js/compressed-token/rollup.config.js b/js/compressed-token/rollup.config.js index 5e1d6f5312..4fac8ae5ef 100644 --- a/js/compressed-token/rollup.config.js +++ b/js/compressed-token/rollup.config.js @@ -47,10 +47,9 @@ const rolls = (fmt, env) => ({ env === 'browser' ? nodePolyfills() : undefined, terser({ compress: { - drop_console: true, + drop_console: false, drop_debugger: true, passes: 3, - pure_funcs: ['console.log', 'console.error', 'console.warn'], booleans_as_integers: true, keep_fargs: false, keep_fnames: false, 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..e4dc13ccc9 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,33 @@ import { buildAndSignTx, Rpc, dedupeSigner, - pickRandomTreeAndQueue, + selectStateTreeInfo, + toArray, + TreeInfo, } from '@lightprotocol/stateless.js'; import { CompressedTokenProgram } from '../program'; import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + 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 Fee payer + * @param mint SPL Mint address + * @param toPubkey Address of the account to mint to + * @param authority Minting authority + * @param amount Amount to mint + * @param outputStateTreeInfo Optional: State tree account that the compressed + * tokens should be inserted into. Defaults to a + * shared state tree account. + * @param tokenPoolInfo Optional: Token pool info. + * @param confirmOptions Options for confirming the transaction * * @return Signature of the confirmed transaction */ @@ -35,16 +45,19 @@ export async function approveAndMintTo( rpc: Rpc, payer: Signer, mint: PublicKey, - destination: PublicKey, + toPubkey: PublicKey, authority: Signer, amount: number | BN, - merkleTree?: PublicKey, + outputStateTreeInfo?: TreeInfo, + tokenPoolInfo?: TokenPoolInfo, confirmOptions?: ConfirmOptions, - tokenProgramId?: PublicKey, ): Promise { - tokenProgramId = tokenProgramId - ? tokenProgramId - : await CompressedTokenProgram.get_mint_program_id(mint, rpc); + outputStateTreeInfo = + outputStateTreeInfo ?? + selectStateTreeInfo(await rpc.getStateTreeInfos()); + tokenPoolInfo = + tokenPoolInfo ?? + selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); const authorityTokenAccount = await getOrCreateAssociatedTokenAccount( rpc, @@ -54,24 +67,18 @@ 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(); @@ -79,7 +86,9 @@ export async function approveAndMintTo( const tx = buildAndSignTx( [ - ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), + ComputeBudgetProgram.setComputeUnitLimit({ + units: 150_000 + toArray(amount).length * 20_000, + }), ...ixs, ], payer, @@ -87,7 +96,5 @@ export async function approveAndMintTo( additionalSigners, ); - const txId = await sendAndConfirmTx(rpc, tx, confirmOptions); - - return txId; + return await sendAndConfirmTx(rpc, tx, confirmOptions); } diff --git a/js/compressed-token/src/actions/approve.ts b/js/compressed-token/src/actions/approve.ts new file mode 100644 index 0000000000..e11776f483 --- /dev/null +++ b/js/compressed-token/src/actions/approve.ts @@ -0,0 +1,84 @@ +import { + ComputeBudgetProgram, + ConfirmOptions, + PublicKey, + Signer, + TransactionSignature, +} from '@solana/web3.js'; +import { + bn, + sendAndConfirmTx, + buildAndSignTx, + Rpc, + dedupeSigner, +} from '@lightprotocol/stateless.js'; +import BN from 'bn.js'; +import { CompressedTokenProgram } from '../program'; +import { + selectMinCompressedTokenAccountsForTransfer, + selectTokenAccountsForApprove, +} from '../utils'; + +/** + * Approve a delegate to spend tokens + * + * @param rpc Rpc to use + * @param payer Fee payer + * @param mint SPL Mint address + * @param amount Number of tokens to delegate + * @param owner Owner of the SPL token account. + * @param delegate Address of the delegate + * @param confirmOptions Options for confirming the transaction + * + * @return Signature of the confirmed transaction + */ +export async function approve( + rpc: Rpc, + payer: Signer, + mint: PublicKey, + amount: number | BN, + owner: Signer, + delegate: PublicKey, + confirmOptions?: ConfirmOptions, +): Promise { + amount = bn(amount); + const compressedTokenAccounts = await rpc.getCompressedTokenAccountsByOwner( + owner.publicKey, + { + mint, + }, + ); + + const [inputAccounts] = selectTokenAccountsForApprove( + compressedTokenAccounts.items, + amount, + ); + + const proof = await rpc.getValidityProofV0( + inputAccounts.map(account => ({ + hash: account.compressedAccount.hash, + tree: account.compressedAccount.treeInfo.tree, + queue: account.compressedAccount.treeInfo.queue, + })), + ); + + const ix = await CompressedTokenProgram.approve({ + payer: payer.publicKey, + inputCompressedTokenAccounts: inputAccounts, + toAddress: delegate, + amount, + recentInputStateRootIndices: proof.rootIndices, + recentValidityProof: proof.compressedProof, + }); + + const { blockhash } = await rpc.getLatestBlockhash(); + const additionalSigners = dedupeSigner(payer, [owner]); + const signedTx = buildAndSignTx( + [ComputeBudgetProgram.setComputeUnitLimit({ units: 350_000 }), ix], + payer, + blockhash, + additionalSigners, + ); + + return sendAndConfirmTx(rpc, signedTx, 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..40bdc1e526 100644 --- a/js/compressed-token/src/actions/compress-spl-token-account.ts +++ b/js/compressed-token/src/actions/compress-spl-token-account.ts @@ -10,23 +10,32 @@ import { buildAndSignTx, Rpc, dedupeSigner, + selectStateTreeInfo, + TreeInfo, } from '@lightprotocol/stateless.js'; - import BN from 'bn.js'; - +import { + getTokenPoolInfos, + selectTokenPoolInfo, + TokenPoolInfo, +} from '../utils/get-token-pool-infos'; import { CompressedTokenProgram } from '../program'; /** * Compress SPL tokens into compressed token format * * @param rpc Rpc connection to use - * @param payer Payer of the transaction fees - * @param mint Mint of the token to compress + * @param payer Fee payer + * @param mint SPL Mint address * @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 Optional: State tree account that the compressed + * account into + * @param tokenPoolInfo Optional: Token pool info. + * @param confirmOptions Options for confirming the transaction + * * @return Signature of the confirmed transaction */ @@ -36,14 +45,17 @@ export async function compressSplTokenAccount( mint: PublicKey, owner: Signer, tokenAccount: PublicKey, - outputStateTree: PublicKey, remainingAmount?: BN, + outputStateTreeInfo?: TreeInfo, + tokenPoolInfo?: TokenPoolInfo, confirmOptions?: ConfirmOptions, - tokenProgramId?: PublicKey, ): Promise { - tokenProgramId = tokenProgramId - ? tokenProgramId - : await CompressedTokenProgram.get_mint_program_id(mint, rpc); + outputStateTreeInfo = + outputStateTreeInfo ?? + selectStateTreeInfo(await rpc.getStateTreeInfos()); + tokenPoolInfo = + tokenPoolInfo ?? + selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); const compressIx = await CompressedTokenProgram.compressSplTokenAccount({ feePayer: payer.publicKey, @@ -51,16 +63,17 @@ 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({ - units: 1_000_000, + units: 150_000, }), compressIx, ], @@ -68,11 +81,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..464a5a2985 100644 --- a/js/compressed-token/src/actions/compress.ts +++ b/js/compressed-token/src/actions/compress.ts @@ -10,29 +10,34 @@ import { buildAndSignTx, Rpc, dedupeSigner, - pickRandomTreeAndQueue, + selectStateTreeInfo, + toArray, + TreeInfo, } 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 * * @param rpc Rpc connection 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 sourceTokenAccount Source (associated) token account - * @param toAddress Destination address of the recipient - * @param merkleTree State tree account that the compressed tokens - * should be inserted into. Defaults to a default - * state tree account. + * @param payer Fee payer + * @param mint SPL Mint address + * @param amount Number of tokens to compress. + * @param owner Owner of the SPL token account. + * @param sourceTokenAccount Source SPL token account. (ATA) + * @param toAddress Recipient owner address. + * @param outputStateTreeInfo Optional: State tree account that the compressed + * tokens should be inserted into. Defaults to a + * shared state tree account. + * @param tokenPoolInfo Optional: Token pool info. * @param confirmOptions Options for confirming the transaction * - * * @return Signature of the confirmed transaction */ export async function compress( @@ -43,19 +48,16 @@ export async function compress( owner: Signer, sourceTokenAccount: PublicKey, toAddress: PublicKey | Array, - merkleTree?: PublicKey, + outputStateTreeInfo?: TreeInfo, + 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.getStateTreeInfos()); + tokenPoolInfo = + tokenPoolInfo ?? + selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); const compressIx = await CompressedTokenProgram.compress({ payer: payer.publicKey, @@ -64,8 +66,8 @@ export async function compress( toAddress, amount, mint, - outputStateTree: merkleTree, - tokenProgramId, + outputStateTreeInfo, + tokenPoolInfo, }); const blockhashCtx = await rpc.getLatestBlockhash(); @@ -73,7 +75,7 @@ export async function compress( const signedTx = buildAndSignTx( [ ComputeBudgetProgram.setComputeUnitLimit({ - units: 1_000_000, + units: 130_000 + toArray(amount).length * 20_000, }), compressIx, ], @@ -81,11 +83,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..2f5ad1b3f3 100644 --- a/js/compressed-token/src/actions/create-mint.ts +++ b/js/compressed-token/src/actions/create-mint.ts @@ -15,35 +15,34 @@ import { Rpc, buildAndSignTx, sendAndConfirmTx, - dedupeSigner, } from '@lightprotocol/stateless.js'; /** * 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 connection to use + * @param payer Fee payer + * @param mintAuthority Account that will control minting + * @param decimals Location of the decimal place + * @param keypair Optional: Mint keypair. Defaults to a random + * keypair. + * @param confirmOptions Options for confirming the transaction + * @param tokenProgramId Optional: Program ID for the token. Defaults to + * TOKEN_PROGRAM_ID. + * @param freezeAuthority Optional: Account that will control freeze and thaw. + * Defaults to none. * - * @return Address of the new mint and the transaction signature + * @return Object with mint address and transaction signature */ export async function createMint( rpc: Rpc, payer: Signer, - mintAuthority: PublicKey, + mintAuthority: PublicKey | Signer, decimals: number, keypair = Keypair.generate(), confirmOptions?: ConfirmOptions, tokenProgramId?: PublicKey | boolean, - freezeAuthority?: PublicKey, + freezeAuthority?: PublicKey | Signer, ): Promise<{ mint: PublicKey; transactionSignature: TransactionSignature }> { const rentExemptBalance = await rpc.getMinimumBalanceForRentExemption(MINT_SIZE); @@ -60,19 +59,32 @@ export async function createMint( feePayer: payer.publicKey, mint: keypair.publicKey, decimals, - authority: mintAuthority, - freezeAuthority: freezeAuthority || null, + authority: getPublicKey(mintAuthority)!, + freezeAuthority: getPublicKey(freezeAuthority), rentExemptBalance, tokenProgramId: resolvedTokenProgramId, }); const { blockhash } = await rpc.getLatestBlockhash(); - const additionalSigners = dedupeSigner(payer, [keypair]); - - const tx = buildAndSignTx(ixs, payer, blockhash, additionalSigners); + // Get required additional signers that are Keypairs, not the payer. + const additionalSigners = [mintAuthority, freezeAuthority].filter( + (signer): signer is Signer => + signer instanceof Keypair && + !signer.publicKey.equals(payer.publicKey) && + !additionalSigners?.some(s => s.publicKey.equals(signer.publicKey)), + ); + const tx = buildAndSignTx(ixs, payer, blockhash, [ + ...additionalSigners, + keypair, + ]); const txId = await sendAndConfirmTx(rpc, tx, confirmOptions); return { mint: keypair.publicKey, transactionSignature: txId }; } + +const getPublicKey = ( + signer: PublicKey | Signer | undefined, +): PublicKey | null => + signer instanceof PublicKey ? signer : signer?.publicKey || null; diff --git a/js/compressed-token/src/actions/create-token-pool.ts b/js/compressed-token/src/actions/create-token-pool.ts index 4793439f3c..fd7e074bb8 100644 --- a/js/compressed-token/src/actions/create-token-pool.ts +++ b/js/compressed-token/src/actions/create-token-pool.ts @@ -2,6 +2,7 @@ import { ConfirmOptions, PublicKey, Signer, + TransactionInstruction, TransactionSignature, } from '@solana/web3.js'; import { CompressedTokenProgram } from '../program'; @@ -10,15 +11,17 @@ import { buildAndSignTx, sendAndConfirmTx, } from '@lightprotocol/stateless.js'; +import { getTokenPoolInfos } from '../utils/get-token-pool-infos'; /** * Register an existing mint with the CompressedToken program * - * @param rpc RPC to use - * @param payer Payer of the transaction and initialization fees - * @param mintAuthority Account or multisig that will control minting. Is signer. - * @param mintAddress Address of the existing mint + * @param rpc RPC connection to use + * @param payer Fee payer + * @param mint SPL Mint address * @param confirmOptions Options for confirming the transaction + * @param tokenProgramId Optional: Address of the token program. Default: + * TOKEN_PROGRAM_ID * * @return transaction signature */ @@ -31,7 +34,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, @@ -47,3 +50,64 @@ export async function createTokenPool( return txId; } + +/** + * Create additional token pools for an existing mint + * + * @param rpc RPC connection to use + * @param payer Fee payer + * @param mint SPL Mint address + * @param numMaxAdditionalPools Number of additional token pools to create. Max + * 3. + * @param confirmOptions Optional: Options for confirming the transaction + * @param tokenProgramId Optional: Address of the token program. Default: + * TOKEN_PROGRAM_ID + * + * @return transaction signature + */ +export async function addTokenPools( + rpc: Rpc, + payer: Signer, + mint: PublicKey, + numMaxAdditionalPools: number, + confirmOptions?: ConfirmOptions, + tokenProgramId?: PublicKey, +) { + tokenProgramId = tokenProgramId + ? tokenProgramId + : await CompressedTokenProgram.getMintProgramId(mint, rpc); + const instructions: TransactionInstruction[] = []; + + const infos = (await getTokenPoolInfos(rpc, mint)).slice(0, 4); + + // Get indices of uninitialized pools + const uninitializedIndices = []; + for (let i = 0; i < infos.length; i++) { + if (!infos[i].isInitialized) { + uninitializedIndices.push(i); + } + } + + // Create instructions for requested number of pools + for (let i = 0; i < numMaxAdditionalPools; i++) { + if (i >= uninitializedIndices.length) { + break; + } + + instructions.push( + await CompressedTokenProgram.addTokenPool({ + mint, + feePayer: payer.publicKey, + tokenProgramId, + poolIndex: uninitializedIndices[i], + }), + ); + } + const { blockhash } = await rpc.getLatestBlockhash(); + + const tx = buildAndSignTx(instructions, payer, blockhash); + + const txId = await sendAndConfirmTx(rpc, tx, confirmOptions); + + return txId; +} diff --git a/js/compressed-token/src/actions/create-token-program-lookup-table.ts b/js/compressed-token/src/actions/create-token-program-lookup-table.ts index 2c8df9b82d..4d4d8d198c 100644 --- a/js/compressed-token/src/actions/create-token-program-lookup-table.ts +++ b/js/compressed-token/src/actions/create-token-program-lookup-table.ts @@ -12,14 +12,15 @@ import { CompressedTokenProgram } from '../program'; * Create a lookup table for the token program's default accounts * * @param rpc Rpc connection to use - * @param payer Payer of the transaction fees + * @param payer Fee payer * @param authority Authority of the lookup table * @param mints Optional array of mint public keys to include in * the lookup table * @param additionalAccounts Optional array of additional account public keys * to include in the lookup table * - * @return Transaction signatures and the address of the created lookup table + * @return Object with transaction signatures and the address of the created + * lookup table */ export async function createTokenProgramLookupTable( rpc: Rpc, @@ -39,35 +40,24 @@ export async function createTokenProgramLookupTable( }); const additionalSigners = dedupeSigner(payer, [authority]); - const blockhashCtx = await rpc.getLatestBlockhash(); - const signedTx = buildAndSignTx( - [instructions[0]], - payer, - blockhashCtx.blockhash, - additionalSigners, - ); + const txIds = []; - /// Must wait for the first instruction to be finalized. - const txId = await sendAndConfirmTx( - rpc, - signedTx, - { commitment: 'finalized' }, - blockhashCtx, - ); + for (const instruction of instructions) { + const blockhashCtx = await rpc.getLatestBlockhash(); + const signedTx = buildAndSignTx( + [instruction], + payer, + blockhashCtx.blockhash, + additionalSigners, + ); + const txId = await sendAndConfirmTx( + rpc, + signedTx, + { commitment: 'finalized' }, + blockhashCtx, + ); + txIds.push(txId); + } - const blockhashCtx2 = await rpc.getLatestBlockhash(); - const signedTx2 = buildAndSignTx( - [instructions[1]], - payer, - blockhashCtx2.blockhash, - additionalSigners, - ); - const txId2 = await sendAndConfirmTx( - rpc, - signedTx2, - { commitment: 'finalized' }, - blockhashCtx2, - ); - - return { txIds: [txId, txId2], address }; + return { txIds, address }; } diff --git a/js/compressed-token/src/actions/decompress-delegated.ts b/js/compressed-token/src/actions/decompress-delegated.ts new file mode 100644 index 0000000000..00dd1b31a6 --- /dev/null +++ b/js/compressed-token/src/actions/decompress-delegated.ts @@ -0,0 +1,99 @@ +import { + ComputeBudgetProgram, + ConfirmOptions, + PublicKey, + Signer, + TransactionSignature, +} from '@solana/web3.js'; +import { + bn, + sendAndConfirmTx, + buildAndSignTx, + Rpc, + dedupeSigner, +} 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 delegated compressed tokens. Remaining compressed tokens are + * returned to the owner without delegation. + * + * @param rpc Rpc connection to use + * @param payer Fee payer + * @param mint SPL Mint address + * @param amount Number of tokens to decompress + * @param owner Owner of the compressed tokens + * @param toAddress Destination **uncompressed** token account + * address. (ATA) + * @param tokenPoolInfos Optional: Token pool infos. + * @param confirmOptions Options for confirming the transaction + * + * @return Signature of the confirmed transaction + */ +export async function decompressDelegated( + rpc: Rpc, + payer: Signer, + mint: PublicKey, + amount: number | BN, + owner: Signer, + toAddress: PublicKey, + tokenPoolInfos?: TokenPoolInfo[], + confirmOptions?: ConfirmOptions, +): Promise { + amount = bn(amount); + + const compressedTokenAccounts = + await rpc.getCompressedTokenAccountsByDelegate(owner.publicKey, { + mint, + }); + + const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer( + compressedTokenAccounts.items, + amount, + ); + + const proof = await rpc.getValidityProofV0( + inputAccounts.map(account => ({ + hash: account.compressedAccount.hash, + tree: account.compressedAccount.treeInfo.tree, + queue: account.compressedAccount.treeInfo.queue, + })), + ); + + const tokenPoolInfosToUse = + tokenPoolInfos ?? + selectTokenPoolInfosForDecompression( + await getTokenPoolInfos(rpc, mint), + amount, + ); + + const ix = await CompressedTokenProgram.decompress({ + payer: payer.publicKey, + inputCompressedTokenAccounts: inputAccounts, + toAddress, + amount, + recentInputStateRootIndices: proof.rootIndices, + recentValidityProof: proof.compressedProof, + tokenPoolInfos: tokenPoolInfosToUse, + }); + + const { blockhash } = await rpc.getLatestBlockhash(); + const additionalSigners = dedupeSigner(payer, [owner]); + const signedTx = buildAndSignTx( + [ComputeBudgetProgram.setComputeUnitLimit({ units: 350_000 }), ix], + payer, + blockhash, + additionalSigners, + ); + + return sendAndConfirmTx(rpc, signedTx, confirmOptions); +} diff --git a/js/compressed-token/src/actions/decompress.ts b/js/compressed-token/src/actions/decompress.ts index d0ebf3aed5..2bfbbe509a 100644 --- a/js/compressed-token/src/actions/decompress.ts +++ b/js/compressed-token/src/actions/decompress.ts @@ -12,29 +12,29 @@ import { Rpc, dedupeSigner, } 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 connection to use + * @param payer Fee payer + * @param mint SPL Mint address + * @param amount Number of tokens to transfer + * @param owner Owner of the compressed tokens + * @param toAddress Destination **uncompressed** token account + * address. (ATA) + * @param tokenPoolInfos Optional: Token pool infos. + * @param confirmOptions Options for confirming the transaction * - * - * @return Signature of the confirmed transaction + * @return confirmed transaction signature */ export async function decompress( rpc: Rpc, @@ -43,14 +43,9 @@ export async function decompress( amount: number | BN, owner: Signer, toAddress: PublicKey, - merkleTree?: PublicKey, + 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,35 +55,43 @@ export async function decompress( }, ); - /// TODO: consider using a different selection algorithm const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer( compressedTokenAccounts.items, amount, ); - const proof = await rpc.getValidityProof( - inputAccounts.map(account => bn(account.compressedAccount.hash)), + const proof = await rpc.getValidityProofV0( + inputAccounts.map(account => ({ + hash: account.compressedAccount.hash, + tree: account.compressedAccount.treeInfo.tree, + queue: account.compressedAccount.treeInfo.queue, + })), + ); + + 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, + tokenPoolInfos: selectedTokenPoolInfos, recentInputStateRootIndices: proof.rootIndices, recentValidityProof: proof.compressedProof, - tokenProgramId, }); const { blockhash } = await rpc.getLatestBlockhash(); const additionalSigners = dedupeSigner(payer, [owner]); const signedTx = buildAndSignTx( - [ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), ix], + [ComputeBudgetProgram.setComputeUnitLimit({ units: 350_000 }), ix], payer, 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/index.ts b/js/compressed-token/src/actions/index.ts index 502e90f089..2ba2800a97 100644 --- a/js/compressed-token/src/actions/index.ts +++ b/js/compressed-token/src/actions/index.ts @@ -1,10 +1,14 @@ +export * from './approve'; export * from './approve-and-mint-to'; export * from './compress'; -export * from './decompress'; +export * from './compress-spl-token-account'; export * from './create-mint'; -export * from './mint-to'; -export * from './merge-token-accounts'; export * from './create-token-pool'; -export * from './transfer'; export * from './create-token-program-lookup-table'; -export * from './compress-spl-token-account'; +export * from './decompress'; +export * from './merge-token-accounts'; +export * from './mint-to'; +export * from './revoke'; +export * from './transfer'; +export * from './transfer-delegated'; +export * from './decompress-delegated'; diff --git a/js/compressed-token/src/actions/merge-token-accounts.ts b/js/compressed-token/src/actions/merge-token-accounts.ts index 84212a4c27..8ca4844c82 100644 --- a/js/compressed-token/src/actions/merge-token-accounts.ts +++ b/js/compressed-token/src/actions/merge-token-accounts.ts @@ -18,21 +18,19 @@ 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 connection to use + * @param payer Fee payer + * @param mint SPL Mint address + * @param owner Owner of the token accounts to be merged + * @param confirmOptions Options for confirming the transaction * - * @return Array of transaction signatures + * @return confirmed transaction signature */ export async function mergeTokenAccounts( rpc: Rpc, payer: Signer, mint: PublicKey, owner: Signer, - merkleTree?: PublicKey, confirmOptions?: ConfirmOptions, ): Promise { const compressedTokenAccounts = await rpc.getCompressedTokenAccountsByOwner( @@ -45,11 +43,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 +50,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)), @@ -70,9 +63,8 @@ export async function mergeTokenAccounts( await CompressedTokenProgram.mergeTokenAccounts({ payer: payer.publicKey, owner: owner.publicKey, - mint, inputCompressedTokenAccounts: batch, - outputStateTree: merkleTree!, + mint, recentValidityProof: proof.compressedProof, recentInputStateRootIndices: proof.rootIndices, }); @@ -89,7 +81,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..e50f20b57c 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, + selectStateTreeInfo, + TreeInfo, } 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 connection to use + * @param payer Fee payer + * @param mint SPL Mint address + * @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 Mint authority + * @param amount Amount to mint. Pass an array of amounts if the + * toPubkey is an array of addresses. + * @param outputStateTreeInfo Optional: State tree account that the compressed + * tokens should be part of. Defaults to the + * default state tree account. + * @param tokenPoolInfo Optional: 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?: TreeInfo, + 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.getStateTreeInfos()); + 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/revoke.ts b/js/compressed-token/src/actions/revoke.ts new file mode 100644 index 0000000000..47ac83ef15 --- /dev/null +++ b/js/compressed-token/src/actions/revoke.ts @@ -0,0 +1,75 @@ +import { + ComputeBudgetProgram, + ConfirmOptions, + Signer, + TransactionSignature, +} from '@solana/web3.js'; +import { + sendAndConfirmTx, + buildAndSignTx, + Rpc, + dedupeSigner, + ParsedTokenAccount, +} from '@lightprotocol/stateless.js'; +import { CompressedTokenProgram } from '../program'; + +/** + * Revoke one or more delegated token accounts + * + * @param rpc Rpc connection to use + * @param payer Fee payer + * @param accounts Delegated compressed token accounts to revoke + * @param owner Owner of the compressed tokens + * @param confirmOptions Options for confirming the transaction + * + * @return Signature of the confirmed transaction + */ +export async function revoke( + rpc: Rpc, + payer: Signer, + accounts: ParsedTokenAccount[], + owner: Signer, + confirmOptions?: ConfirmOptions, +): Promise { + const proof = await rpc.getValidityProofV0( + accounts.map(account => ({ + hash: account.compressedAccount.hash, + tree: account.compressedAccount.treeInfo.tree, + queue: account.compressedAccount.treeInfo.queue, + })), + ); + checkOwner(owner, accounts); + checkIsDelegated(accounts); + + const ix = await CompressedTokenProgram.revoke({ + payer: payer.publicKey, + inputCompressedTokenAccounts: accounts, + recentInputStateRootIndices: proof.rootIndices, + recentValidityProof: proof.compressedProof, + }); + + const { blockhash } = await rpc.getLatestBlockhash(); + const additionalSigners = dedupeSigner(payer, [owner]); + const signedTx = buildAndSignTx( + [ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }), ix], + payer, + blockhash, + additionalSigners, + ); + + return sendAndConfirmTx(rpc, signedTx, confirmOptions); +} + +function checkOwner(owner: Signer, accounts: ParsedTokenAccount[]) { + if (!owner.publicKey.equals(accounts[0].parsed.owner)) { + throw new Error( + `Owner ${owner.publicKey.toBase58()} does not match account ${accounts[0].parsed.owner.toBase58()}`, + ); + } +} + +function checkIsDelegated(accounts: ParsedTokenAccount[]) { + if (accounts.some(account => account.parsed.delegate === null)) { + throw new Error('Account is not delegated'); + } +} diff --git a/js/compressed-token/src/actions/transfer-delegated.ts b/js/compressed-token/src/actions/transfer-delegated.ts new file mode 100644 index 0000000000..a788050163 --- /dev/null +++ b/js/compressed-token/src/actions/transfer-delegated.ts @@ -0,0 +1,79 @@ +import { + ComputeBudgetProgram, + ConfirmOptions, + PublicKey, + Signer, + TransactionSignature, +} from '@solana/web3.js'; +import { + bn, + sendAndConfirmTx, + buildAndSignTx, + Rpc, + dedupeSigner, +} from '@lightprotocol/stateless.js'; +import BN from 'bn.js'; +import { CompressedTokenProgram } from '../program'; +import { selectMinCompressedTokenAccountsForTransfer } from '../utils'; + +/** + * Transfer delegated compressed tokens to another owner + * + * @param rpc Rpc connection to use + * @param payer Fee payer + * @param mint SPL Mint address + * @param amount Number of tokens to transfer + * @param owner Owner of the compressed tokens + * @param toAddress Destination address of the recipient + * @param confirmOptions Options for confirming the transaction + * + * @return confirmed transaction signature + */ +export async function transferDelegated( + rpc: Rpc, + payer: Signer, + mint: PublicKey, + amount: number | BN, + owner: Signer, + toAddress: PublicKey, + confirmOptions?: ConfirmOptions, +): Promise { + amount = bn(amount); + const compressedTokenAccounts = + await rpc.getCompressedTokenAccountsByDelegate(owner.publicKey, { + mint, + }); + + const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer( + compressedTokenAccounts.items, + amount, + ); + + const proof = await rpc.getValidityProofV0( + inputAccounts.map(account => ({ + hash: account.compressedAccount.hash, + tree: account.compressedAccount.treeInfo.tree, + queue: account.compressedAccount.treeInfo.queue, + })), + ); + + const ix = await CompressedTokenProgram.transfer({ + payer: payer.publicKey, + inputCompressedTokenAccounts: inputAccounts, + toAddress, + amount, + recentInputStateRootIndices: proof.rootIndices, + recentValidityProof: proof.compressedProof, + }); + + const { blockhash } = await rpc.getLatestBlockhash(); + const additionalSigners = dedupeSigner(payer, [owner]); + const signedTx = buildAndSignTx( + [ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }), ix], + payer, + blockhash, + additionalSigners, + ); + + return sendAndConfirmTx(rpc, signedTx, confirmOptions); +} diff --git a/js/compressed-token/src/actions/transfer.ts b/js/compressed-token/src/actions/transfer.ts index 396b46a1ce..af475e9a3b 100644 --- a/js/compressed-token/src/actions/transfer.ts +++ b/js/compressed-token/src/actions/transfer.ts @@ -12,28 +12,22 @@ import { Rpc, dedupeSigner, } from '@lightprotocol/stateless.js'; - import BN from 'bn.js'; - import { CompressedTokenProgram } from '../program'; 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 connection to use + * @param payer Fee payer + * @param mint SPL Mint address + * @param amount Number of tokens to transfer + * @param owner Owner of the compressed tokens + * @param toAddress Destination address of the recipient + * @param confirmOptions Options for confirming the transaction * - * @return Signature of the confirmed transaction + * @return confirmed transaction signature */ export async function transfer( rpc: Rpc, @@ -42,7 +36,6 @@ export async function transfer( amount: number | BN, owner: Signer, toAddress: PublicKey, - merkleTree?: PublicKey, confirmOptions?: ConfirmOptions, ): Promise { amount = bn(amount); @@ -58,8 +51,12 @@ export async function transfer( amount, ); - const proof = await rpc.getValidityProof( - inputAccounts.map(account => bn(account.compressedAccount.hash)), + const proof = await rpc.getValidityProofV0( + inputAccounts.map(account => ({ + hash: account.compressedAccount.hash, + tree: account.compressedAccount.treeInfo.tree, + queue: account.compressedAccount.treeInfo.queue, + })), ); const ix = await CompressedTokenProgram.transfer({ @@ -69,18 +66,16 @@ export async function transfer( amount, recentInputStateRootIndices: proof.rootIndices, recentValidityProof: proof.compressedProof, - outputStateTrees: merkleTree, }); 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/constants.ts b/js/compressed-token/src/constants.ts index 74acb9e2ca..496796be14 100644 --- a/js/compressed-token/src/constants.ts +++ b/js/compressed-token/src/constants.ts @@ -1,3 +1,4 @@ +import { Buffer } from 'buffer'; export const POOL_SEED = Buffer.from('pool'); export const CPI_AUTHORITY_SEED = Buffer.from('cpi_authority'); @@ -10,9 +11,22 @@ export const CREATE_TOKEN_POOL_DISCRIMINATOR = Buffer.from([ export const MINT_TO_DISCRIMINATOR = Buffer.from([ 241, 34, 48, 186, 37, 179, 123, 192, ]); +export const BATCH_COMPRESS_DISCRIMINATOR = Buffer.from([ + 65, 206, 101, 37, 147, 42, 221, 144, +]); export const TRANSFER_DISCRIMINATOR = Buffer.from([ 163, 52, 200, 231, 140, 3, 69, 186, ]); export const COMPRESS_SPL_TOKEN_ACCOUNT_DISCRIMINATOR = Buffer.from([ 112, 230, 105, 101, 145, 202, 157, 97, ]); + +export const APPROVE_DISCRIMINATOR = Buffer.from([ + 69, 74, 217, 36, 115, 117, 97, 76, +]); +export const REVOKE_DISCRIMINATOR = Buffer.from([ + 170, 23, 31, 34, 133, 173, 93, 242, +]); +export const ADD_TOKEN_POOL_DISCRIMINATOR = Buffer.from([ + 114, 143, 210, 73, 96, 115, 1, 228, +]); diff --git a/js/compressed-token/src/idl.ts b/js/compressed-token/src/idl.ts index 5ce45b666e..8aab62d499 100644 --- a/js/compressed-token/src/idl.ts +++ b/js/compressed-token/src/idl.ts @@ -1089,6 +1089,104 @@ export type LightCompressedToken = { ]; }; }, + { + name: 'CompressedTokenInstructionDataRevoke'; + type: { + kind: 'struct'; + fields: [ + { + name: 'proof'; + type: { + option: { + defined: 'CompressedProof'; + }; + }; + }, + { + name: 'mint'; + type: 'publicKey'; + }, + { + name: 'inputTokenDataWithContext'; + type: { + vec: { + defined: 'InputTokenDataWithContext'; + }; + }; + }, + { + name: 'cpiContext'; + type: { + option: { + defined: 'CompressedCpiContext'; + }; + }; + }, + { + name: 'outputAccountMerkleTreeIndex'; + type: 'u8'; + }, + ]; + }; + }, + { + name: 'CompressedTokenInstructionDataApprove'; + type: { + kind: 'struct'; + fields: [ + { + name: 'proof'; + type: { + option: { + defined: 'CompressedProof'; + }; + }; + }, + { + name: 'mint'; + type: 'publicKey'; + }, + { + name: 'inputTokenDataWithContext'; + type: { + vec: { + defined: 'InputTokenDataWithContext'; + }; + }; + }, + { + name: 'cpiContext'; + type: { + option: { + defined: 'CompressedCpiContext'; + }; + }; + }, + { + name: 'delegate'; + type: 'publicKey'; + }, + { + name: 'delegatedAmount'; + type: 'u64'; + }, + { + name: 'delegateMerkleTreeIndex'; + type: 'u8'; + }, + { + name: 'changeAccountMerkleTreeIndex'; + type: 'u8'; + }, + { + name: 'delegateLamports'; + type: { + option: 'u64'; + }; + }, + ]; + }; + }, { name: 'DelegatedTransfer'; docs: [ @@ -1383,7 +1481,7 @@ export type LightCompressedToken = { type: 'u8'; }, { - name: 'nullifierQueuePubkeyIndex'; + name: 'queuePubkeyIndex'; type: 'u8'; }, { @@ -1391,12 +1489,8 @@ export type LightCompressedToken = { type: 'u32'; }, { - name: 'queueIndex'; - type: { - option: { - defined: 'QueueIndex'; - }; - }; + name: 'proveByIndex'; + type: 'bool'; }, ]; }; @@ -2827,6 +2921,104 @@ export const IDL: LightCompressedToken = { ], }, }, + { + name: 'CompressedTokenInstructionDataRevoke', + type: { + kind: 'struct', + fields: [ + { + name: 'proof', + type: { + option: { + defined: 'CompressedProof', + }, + }, + }, + { + name: 'mint', + type: 'publicKey', + }, + { + name: 'inputTokenDataWithContext', + type: { + vec: { + defined: 'InputTokenDataWithContext', + }, + }, + }, + { + name: 'cpiContext', + type: { + option: { + defined: 'CompressedCpiContext', + }, + }, + }, + { + name: 'outputAccountMerkleTreeIndex', + type: 'u8', + }, + ], + }, + }, + { + name: 'CompressedTokenInstructionDataApprove', + type: { + kind: 'struct', + fields: [ + { + name: 'proof', + type: { + option: { + defined: 'CompressedProof', + }, + }, + }, + { + name: 'mint', + type: 'publicKey', + }, + { + name: 'inputTokenDataWithContext', + type: { + vec: { + defined: 'InputTokenDataWithContext', + }, + }, + }, + { + name: 'cpiContext', + type: { + option: { + defined: 'CompressedCpiContext', + }, + }, + }, + { + name: 'delegate', + type: 'publicKey', + }, + { + name: 'delegatedAmount', + type: 'u64', + }, + { + name: 'delegateMerkleTreeIndex', + type: 'u8', + }, + { + name: 'changeAccountMerkleTreeIndex', + type: 'u8', + }, + { + name: 'delegateLamports', + type: { + option: 'u64', + }, + }, + ], + }, + }, { name: 'DelegatedTransfer', docs: [ @@ -3125,7 +3317,7 @@ export const IDL: LightCompressedToken = { type: 'u8', }, { - name: 'nullifierQueuePubkeyIndex', + name: 'queuePubkeyIndex', type: 'u8', }, { @@ -3133,12 +3325,8 @@ export const IDL: LightCompressedToken = { type: 'u32', }, { - name: 'queueIndex', - type: { - option: { - defined: 'QueueIndex', - }, - }, + name: 'proveByIndex', + type: 'bool', }, ], }, diff --git a/js/compressed-token/src/index.ts b/js/compressed-token/src/index.ts index 3258e2b6af..4e8896433d 100644 --- a/js/compressed-token/src/index.ts +++ b/js/compressed-token/src/index.ts @@ -1,8 +1,7 @@ -export * from './instructions'; +export * from './actions'; +export * from './utils'; export * from './constants'; +export * from './idl'; +export * from './layout'; export * from './program'; export * from './types'; -export * from './actions'; -export * from './layout'; -export * from './idl'; -export * from './utils'; diff --git a/js/compressed-token/src/instructions/index.ts b/js/compressed-token/src/instructions/index.ts deleted file mode 100644 index afd2a74d7e..0000000000 --- a/js/compressed-token/src/instructions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './pack-compressed-token-accounts'; diff --git a/js/compressed-token/src/layout.ts b/js/compressed-token/src/layout.ts index ed691ab6c8..eba7faa70e 100644 --- a/js/compressed-token/src/layout.ts +++ b/js/compressed-token/src/layout.ts @@ -11,19 +11,26 @@ import { u16, vecU8, } from '@coral-xyz/borsh'; -import { Buffer } from 'buffer'; import { AccountMeta, PublicKey } from '@solana/web3.js'; import { CompressedTokenProgram } from './program'; import { + BatchCompressInstructionData, + CompressedTokenInstructionDataApprove, + CompressedTokenInstructionDataRevoke, CompressedTokenInstructionDataTransfer, CompressSplTokenAccountInstructionData, MintToInstructionData, } from './types'; import { + APPROVE_DISCRIMINATOR, + BATCH_COMPRESS_DISCRIMINATOR, COMPRESS_SPL_TOKEN_ACCOUNT_DISCRIMINATOR, MINT_TO_DISCRIMINATOR, + REVOKE_DISCRIMINATOR, TRANSFER_DISCRIMINATOR, } from './constants'; +import { Buffer } from 'buffer'; +import { ValidityProof } from '@lightprotocol/stateless.js'; const CompressedProofLayout = struct([ array(u8(), 32, 'a'), @@ -39,17 +46,15 @@ const PackedTokenTransferOutputDataLayout = struct([ option(vecU8(), 'tlv'), ]); -const QueueIndexLayout = struct([u8('queueId'), u16('index')]); - const InputTokenDataWithContextLayout = struct([ u64('amount'), option(u8(), 'delegateIndex'), struct( [ u8('merkleTreePubkeyIndex'), - u8('nullifierQueuePubkeyIndex'), + u8('queuePubkeyIndex'), u32('leafIndex'), - option(QueueIndexLayout, 'queueIndex'), + bool('proveByIndex'), ], 'merkleContext', ), @@ -87,6 +92,15 @@ export const mintToLayout = struct([ option(u64(), 'lamports'), ]); +export const batchCompressLayout = struct([ + vec(publicKey(), 'pubkeys'), + option(vec(u64(), 'amounts'), 'amounts'), + option(u64(), 'lamports'), + option(u64(), 'amount'), + u8('index'), + u8('bump'), +]); + export const compressSplTokenAccountInstructionDataLayout = struct([ publicKey('owner'), option(u64(), 'remainingAmount'), @@ -106,20 +120,43 @@ export function encodeMintToInstructionData( buffer, ); - return Buffer.concat([MINT_TO_DISCRIMINATOR, buffer.slice(0, len)]); + return Buffer.concat([ + new Uint8Array(MINT_TO_DISCRIMINATOR), + new Uint8Array(buffer.subarray(0, len)), + ]); } export function decodeMintToInstructionData( buffer: Buffer, ): MintToInstructionData { - const data: any = mintToLayout.decode( - buffer.slice(MINT_TO_DISCRIMINATOR.length), - ); - return { - recipients: data.recipients, - amounts: data.amounts, - lamports: data.lamports, - }; + return mintToLayout.decode( + buffer.subarray(MINT_TO_DISCRIMINATOR.length), + ) as MintToInstructionData; +} + +export function encodeBatchCompressInstructionData( + data: BatchCompressInstructionData, +): Buffer { + const buffer = Buffer.alloc(1000); + const len = batchCompressLayout.encode(data, buffer); + + const lengthBuffer = Buffer.alloc(4); + lengthBuffer.writeUInt32LE(len, 0); + + const dataBuffer = buffer.subarray(0, len); + return Buffer.concat([ + new Uint8Array(BATCH_COMPRESS_DISCRIMINATOR), + new Uint8Array(lengthBuffer), + new Uint8Array(dataBuffer), + ]); +} + +export function decodeBatchCompressInstructionData( + buffer: Buffer, +): BatchCompressInstructionData { + return batchCompressLayout.decode( + buffer.subarray(BATCH_COMPRESS_DISCRIMINATOR.length + 4), + ) as BatchCompressInstructionData; } export function encodeCompressSplTokenAccountInstructionData( @@ -136,17 +173,17 @@ export function encodeCompressSplTokenAccountInstructionData( ); return Buffer.concat([ - COMPRESS_SPL_TOKEN_ACCOUNT_DISCRIMINATOR, - buffer.slice(0, len), + new Uint8Array(COMPRESS_SPL_TOKEN_ACCOUNT_DISCRIMINATOR), + new Uint8Array(buffer.subarray(0, len)), ]); } export function decodeCompressSplTokenAccountInstructionData( buffer: Buffer, ): CompressSplTokenAccountInstructionData { - const data: any = compressSplTokenAccountInstructionDataLayout.decode( - buffer.slice(COMPRESS_SPL_TOKEN_ACCOUNT_DISCRIMINATOR.length), - ); + const data = compressSplTokenAccountInstructionDataLayout.decode( + buffer.subarray(COMPRESS_SPL_TOKEN_ACCOUNT_DISCRIMINATOR.length), + ) as CompressSplTokenAccountInstructionData; return { owner: data.owner, remainingAmount: data.remainingAmount, @@ -166,10 +203,12 @@ export function encodeTransferInstructionData( const lengthBuffer = Buffer.alloc(4); lengthBuffer.writeUInt32LE(len, 0); + const dataBuffer = buffer.subarray(0, len); + return Buffer.concat([ - TRANSFER_DISCRIMINATOR, - lengthBuffer, - buffer.slice(0, len), + new Uint8Array(TRANSFER_DISCRIMINATOR), + new Uint8Array(lengthBuffer), + new Uint8Array(dataBuffer), ]); } @@ -201,6 +240,12 @@ export type createTokenPoolAccountsLayoutParams = { tokenProgram: PublicKey; cpiAuthorityPda: PublicKey; }; + +export type addTokenPoolAccountsLayoutParams = + createTokenPoolAccountsLayoutParams & { + existingTokenPoolPda: PublicKey; + }; + export type mintToAccountsLayoutParams = BaseAccountsLayoutParams & { mint: PublicKey; tokenPoolPda: PublicKey; @@ -241,6 +286,29 @@ export const createTokenPoolAccountsLayout = ( ]; }; +export const addTokenPoolAccountsLayout = ( + accounts: addTokenPoolAccountsLayoutParams, +): AccountMeta[] => { + const { + feePayer, + tokenPoolPda, + systemProgram, + mint, + tokenProgram, + cpiAuthorityPda, + existingTokenPoolPda, + } = accounts; + return [ + { pubkey: feePayer, isSigner: true, isWritable: true }, + { pubkey: tokenPoolPda, isSigner: false, isWritable: true }, + { pubkey: existingTokenPoolPda, isSigner: false, isWritable: false }, + { pubkey: systemProgram, isSigner: false, isWritable: false }, + { pubkey: mint, isSigner: false, isWritable: true }, + { pubkey: tokenProgram, isSigner: false, isWritable: false }, + { pubkey: cpiAuthorityPda, isSigner: false, isWritable: false }, + ]; +}; + export const mintToAccountsLayout = ( accounts: mintToAccountsLayoutParams, ): AccountMeta[] => { @@ -355,85 +423,203 @@ export const transferAccountsLayout = ( return accountsList; }; -// TODO: use this layout for approve/revoke/freeze/thaw once we add them -// export const approveAccountsLayout = ( -// accounts: approveAccountsLayoutParams, -// ): AccountMeta[] => { -// const { -// feePayer, -// authority, -// cpiAuthorityPda, -// lightSystemProgram, -// registeredProgramPda, -// noopProgram, -// accountCompressionAuthority, -// accountCompressionProgram, -// selfProgram, -// systemProgram, -// } = accounts; - -// return [ -// { pubkey: feePayer, isSigner: true, isWritable: true }, -// { pubkey: authority, isSigner: true, isWritable: false }, -// { pubkey: cpiAuthorityPda, isSigner: false, isWritable: false }, -// { pubkey: lightSystemProgram, isSigner: false, isWritable: false }, -// { pubkey: registeredProgramPda, isSigner: false, isWritable: false }, -// { pubkey: noopProgram, isSigner: false, isWritable: false }, -// { -// pubkey: accountCompressionAuthority, -// isSigner: false, -// isWritable: false, -// }, -// { -// pubkey: accountCompressionProgram, -// isSigner: false, -// isWritable: false, -// }, -// { pubkey: selfProgram, isSigner: false, isWritable: false }, -// { pubkey: systemProgram, isSigner: false, isWritable: false }, -// ]; -// }; - -// export const revokeAccountsLayout = approveAccountsLayout; - -// export const freezeAccountsLayout = ( -// accounts: freezeAccountsLayoutParams, -// ): AccountMeta[] => { -// const { -// feePayer, -// authority, -// cpiAuthorityPda, -// lightSystemProgram, -// registeredProgramPda, -// noopProgram, -// accountCompressionAuthority, -// accountCompressionProgram, -// selfProgram, -// systemProgram, -// mint, -// } = accounts; - -// return [ -// { pubkey: feePayer, isSigner: true, isWritable: true }, -// { pubkey: authority, isSigner: true, isWritable: false }, -// { pubkey: cpiAuthorityPda, isSigner: false, isWritable: false }, -// { pubkey: lightSystemProgram, isSigner: false, isWritable: false }, -// { pubkey: registeredProgramPda, isSigner: false, isWritable: false }, -// { pubkey: noopProgram, isSigner: false, isWritable: false }, -// { -// pubkey: accountCompressionAuthority, -// isSigner: false, -// isWritable: false, -// }, -// { -// pubkey: accountCompressionProgram, -// isSigner: false, -// isWritable: false, -// }, -// { pubkey: selfProgram, isSigner: false, isWritable: false }, -// { pubkey: systemProgram, isSigner: false, isWritable: false }, -// { pubkey: mint, isSigner: false, isWritable: false }, -// ]; -// }; - -// export const thawAccountsLayout = freezeAccountsLayout; +export const approveAccountsLayout = ( + accounts: approveAccountsLayoutParams, +): AccountMeta[] => { + const { + feePayer, + authority, + cpiAuthorityPda, + lightSystemProgram, + registeredProgramPda, + noopProgram, + accountCompressionAuthority, + accountCompressionProgram, + selfProgram, + systemProgram, + } = accounts; + + return [ + { pubkey: feePayer, isSigner: true, isWritable: true }, + { pubkey: authority, isSigner: true, isWritable: false }, + { pubkey: cpiAuthorityPda, isSigner: false, isWritable: false }, + { pubkey: lightSystemProgram, isSigner: false, isWritable: false }, + { pubkey: registeredProgramPda, isSigner: false, isWritable: false }, + { pubkey: noopProgram, isSigner: false, isWritable: false }, + { + pubkey: accountCompressionAuthority, + isSigner: false, + isWritable: false, + }, + { + pubkey: accountCompressionProgram, + isSigner: false, + isWritable: false, + }, + { pubkey: selfProgram, isSigner: false, isWritable: false }, + { pubkey: systemProgram, isSigner: false, isWritable: false }, + ]; +}; + +export const revokeAccountsLayout = approveAccountsLayout; + +export const freezeAccountsLayout = ( + accounts: freezeAccountsLayoutParams, +): AccountMeta[] => { + const { + feePayer, + authority, + cpiAuthorityPda, + lightSystemProgram, + registeredProgramPda, + noopProgram, + accountCompressionAuthority, + accountCompressionProgram, + selfProgram, + systemProgram, + mint, + } = accounts; + + return [ + { pubkey: feePayer, isSigner: true, isWritable: true }, + { pubkey: authority, isSigner: true, isWritable: false }, + { pubkey: cpiAuthorityPda, isSigner: false, isWritable: false }, + { pubkey: lightSystemProgram, isSigner: false, isWritable: false }, + { pubkey: registeredProgramPda, isSigner: false, isWritable: false }, + { pubkey: noopProgram, isSigner: false, isWritable: false }, + { + pubkey: accountCompressionAuthority, + isSigner: false, + isWritable: false, + }, + { + pubkey: accountCompressionProgram, + isSigner: false, + isWritable: false, + }, + { pubkey: selfProgram, isSigner: false, isWritable: false }, + { pubkey: systemProgram, isSigner: false, isWritable: false }, + { pubkey: mint, isSigner: false, isWritable: false }, + ]; +}; + +export const thawAccountsLayout = freezeAccountsLayout; + +export const CompressedTokenInstructionDataApproveLayout = struct([ + struct( + [array(u8(), 32, 'a'), array(u8(), 64, 'b'), array(u8(), 32, 'c')], + 'proof', + ), + publicKey('mint'), + vec(InputTokenDataWithContextLayout, 'inputTokenDataWithContext'), + option(CpiContextLayout, 'cpiContext'), + publicKey('delegate'), + u64('delegatedAmount'), + u8('delegateMerkleTreeIndex'), + u8('changeAccountMerkleTreeIndex'), + option(u64(), 'delegateLamports'), +]); + +export const CompressedTokenInstructionDataRevokeLayout = struct([ + struct( + [array(u8(), 32, 'a'), array(u8(), 64, 'b'), array(u8(), 32, 'c')], + 'proof', + ), + publicKey('mint'), + vec(InputTokenDataWithContextLayout, 'inputTokenDataWithContext'), + option(CpiContextLayout, 'cpiContext'), + u8('outputAccountMerkleTreeIndex'), +]); + +// Approve and revoke instuctions do not support optional proof yet. +const emptyProof: ValidityProof = { + a: new Array(32).fill(0), + b: new Array(64).fill(0), + c: new Array(32).fill(0), +}; + +function isEmptyProof(proof: ValidityProof): boolean { + return ( + proof.a.every(a => a === 0) && + proof.b.every(b => b === 0) && + proof.c.every(c => c === 0) + ); +} + +export function encodeApproveInstructionData( + data: CompressedTokenInstructionDataApprove, +): Buffer { + const buffer = Buffer.alloc(1000); + + const proofOption = data.proof ?? emptyProof; + + const len = CompressedTokenInstructionDataApproveLayout.encode( + { + ...data, + proof: proofOption, + }, + buffer, + ); + + const lengthBuffer = Buffer.alloc(4); + lengthBuffer.writeUInt32LE(len, 0); + + const dataBuffer = buffer.subarray(0, len); + + return Buffer.concat([ + new Uint8Array(APPROVE_DISCRIMINATOR), + new Uint8Array(lengthBuffer), + new Uint8Array(dataBuffer), + ]); +} + +export function decodeApproveInstructionData( + buffer: Buffer, +): CompressedTokenInstructionDataApprove { + const data = CompressedTokenInstructionDataApproveLayout.decode( + buffer.subarray(APPROVE_DISCRIMINATOR.length), + ) as CompressedTokenInstructionDataApprove; + return { + ...data, + proof: isEmptyProof(data.proof!) ? null : data.proof!, + }; +} + +export function encodeRevokeInstructionData( + data: CompressedTokenInstructionDataRevoke, +): Buffer { + const buffer = Buffer.alloc(1000); + + const proofOption = data.proof ?? emptyProof; + + const len = CompressedTokenInstructionDataRevokeLayout.encode( + { + ...data, + proof: proofOption, + }, + buffer, + ); + + const lengthBuffer = Buffer.alloc(4); + lengthBuffer.writeUInt32LE(len, 0); + + const dataBuffer = buffer.subarray(0, len); + + return Buffer.concat([ + new Uint8Array(REVOKE_DISCRIMINATOR), + new Uint8Array(lengthBuffer), + new Uint8Array(dataBuffer), + ]); +} + +export function decodeRevokeInstructionData( + buffer: Buffer, +): CompressedTokenInstructionDataRevoke { + const data = CompressedTokenInstructionDataRevokeLayout.decode( + buffer.subarray(REVOKE_DISCRIMINATOR.length), + ) as CompressedTokenInstructionDataRevoke; + return { + ...data, + proof: isEmptyProof(data.proof!) ? null : data.proof!, + }; +} diff --git a/js/compressed-token/src/program.ts b/js/compressed-token/src/program.ts index e0ddef75bc..02e3eb90ae 100644 --- a/js/compressed-token/src/program.ts +++ b/js/compressed-token/src/program.ts @@ -5,10 +5,12 @@ import { Connection, AddressLookupTableProgram, AccountMeta, + ComputeBudgetProgram, } from '@solana/web3.js'; import BN from 'bn.js'; +import { Buffer } from 'buffer'; import { - CompressedProof, + ValidityProof, LightSystemProgram, ParsedTokenAccount, bn, @@ -18,6 +20,9 @@ import { validateSameOwner, validateSufficientBalance, defaultTestStateTreeAccounts, + TreeInfo, + CompressedProof, + featureFlags, } from '@lightprotocol/stateless.js'; import { MINT_SIZE, @@ -30,8 +35,9 @@ import { CPI_AUTHORITY_SEED, POOL_SEED, CREATE_TOKEN_POOL_DISCRIMINATOR, + ADD_TOKEN_POOL_DISCRIMINATOR, } from './constants'; -import { packCompressedTokenAccounts } from './instructions/pack-compressed-token-accounts'; +import { checkMint, packCompressedTokenAccounts } from './utils'; import { encodeTransferInstructionData, encodeCompressSplTokenAccountInstructionData, @@ -39,125 +45,157 @@ import { createTokenPoolAccountsLayout, mintToAccountsLayout, transferAccountsLayout, + approveAccountsLayout, + revokeAccountsLayout, + encodeApproveInstructionData, + encodeRevokeInstructionData, + addTokenPoolAccountsLayout, + encodeBatchCompressInstructionData, } from './layout'; import { + BatchCompressInstructionData, + CompressedTokenInstructionDataApprove, + CompressedTokenInstructionDataRevoke, CompressedTokenInstructionDataTransfer, + DelegatedTransfer, TokenTransferOutputData, } from './types'; +import { + checkTokenPoolInfo, + TokenPoolInfo, +} from './utils/get-token-pool-infos'; export type CompressParams = { /** - * The payer of the transaction. + * Fee payer */ payer: PublicKey; /** - * owner of the *uncompressed* token account. + * Owner of uncompressed token account */ owner: PublicKey; /** - * source (associated) token account address. + * Source SPL Token account address */ source: PublicKey; /** - * owner of the compressed token account. - * To compress to a batch of recipients, pass an array of PublicKeys. + * Recipient address(es) */ toAddress: PublicKey | PublicKey[]; /** - * Mint address of the token to compress. + * Token amount(s) to compress */ - mint: PublicKey; + amount: number | BN | number[] | BN[]; /** - * amount of tokens to compress. + * SPL Token mint address */ - amount: number | BN | number[] | BN[]; + mint: PublicKey; /** - * The state tree that the tx output should be inserted into. Defaults to a - * public state tree if unspecified. + * State tree to write to */ - outputStateTree?: PublicKey; + outputStateTreeInfo: TreeInfo; /** - * Optional: The token program ID. Default: SPL Token Program ID + * Token pool */ - tokenProgramId?: PublicKey; + tokenPoolInfo: TokenPoolInfo; }; export type CompressSplTokenAccountParams = { /** - * Tx feepayer + * Fee payer */ feePayer: PublicKey; /** - * Authority that owns the token account + * SPL Token account owner */ authority: PublicKey; /** - * Token account to compress + * SPL Token account to compress */ tokenAccount: PublicKey; /** - * Mint public key + * SPL Token mint address */ mint: PublicKey; /** - * Optional: remaining amount to leave in token account. Default: 0 + * Amount to leave in token account */ remainingAmount?: BN; /** - * The state tree that the compressed token account should be inserted into. + * State tree to write to */ - outputStateTree: PublicKey; + outputStateTreeInfo: TreeInfo; /** - * Optional: The token program ID. Default: SPL Token Program ID + * Token pool */ - tokenProgramId?: PublicKey; + tokenPoolInfo: TokenPoolInfo; }; export type DecompressParams = { /** - * The payer of the transaction. + * Fee payer */ payer: PublicKey; /** - * input state to be consumed + * Source compressed token accounts */ inputCompressedTokenAccounts: ParsedTokenAccount[]; /** - * address of **uncompressed** destination token account. + * Destination uncompressed token account */ toAddress: PublicKey; /** - * amount of tokens to decompress. + * Token amount to decompress */ amount: number | BN; /** - * The recent state root indices of the input state. The expiry is tied to - * the proof. + * Validity proof for input state + */ + recentValidityProof: ValidityProof | CompressedProof | null; + /** + * Recent state root indices */ recentInputStateRootIndices: number[]; /** - * The recent validity proof for state inclusion of the input state. It - * expires after n slots. + * Token pool(s) */ - recentValidityProof: CompressedProof; + tokenPoolInfos: TokenPoolInfo | TokenPoolInfo[]; +}; + +export type TransferParams = { /** - * The state tree that the change tx output should be inserted into. - * Defaults to a public state tree if unspecified. + * Fee payer */ - outputStateTree?: PublicKey; + payer: PublicKey; /** - * Optional: The token program ID. Default: SPL Token Program ID + * Source compressed token accounts */ - tokenProgramId?: PublicKey; + inputCompressedTokenAccounts: ParsedTokenAccount[]; + /** + * Recipient address + */ + toAddress: PublicKey; + /** + * Token amount to transfer + */ + amount: BN | number; + /** + * Validity proof for input state + */ + recentValidityProof: ValidityProof | CompressedProof | null; + /** + * Recent state root indices + */ + recentInputStateRootIndices: number[]; }; -export type TransferParams = { +export type ApproveParams = { /** - * The payer of the transaction + * Fee payer */ payer: PublicKey; /** - * The input state to be consumed + * Source compressed token accounts */ inputCompressedTokenAccounts: ParsedTokenAccount[]; /** @@ -165,26 +203,36 @@ export type TransferParams = { */ toAddress: PublicKey; /** - * Amount of tokens to transfer + * Token amount to approve */ amount: BN | number; /** - * The recent state root indices of the input state. The expiry is tied to - * the proof. - + * Validity proof for input state + */ + recentValidityProof: ValidityProof | CompressedProof | null; + /** + * Recent state root indices */ recentInputStateRootIndices: number[]; +}; + +export type RevokeParams = { /** - * The recent validity proof for state inclusion of the input state. It - * expires after n slots. + * Fee payer */ - recentValidityProof: CompressedProof; + payer: PublicKey; /** - * The state trees that the tx output should be inserted into. This can be a - * single PublicKey or an array of PublicKey. Defaults to the 0th state tree - * of input state. + * Input compressed token accounts */ - outputStateTrees?: PublicKey[] | PublicKey; + inputCompressedTokenAccounts: ParsedTokenAccount[]; + /** + * Validity proof for input state + */ + recentValidityProof: ValidityProof | CompressedProof | null; + /** + * Recent state root indices + */ + recentInputStateRootIndices: number[]; }; /** @@ -192,25 +240,25 @@ export type TransferParams = { */ export type CreateMintParams = { /** - * Tx feepayer + * Fee payer */ feePayer: PublicKey; + /** + * SPL Mint address + */ + mint: PublicKey; /** * Mint authority */ authority: PublicKey; /** - * Mint public key + * Optional: freeze authority */ - mint: PublicKey; + freezeAuthority: PublicKey | null; /** * Mint decimals */ decimals: number; - /** - * Optional: freeze authority - */ - freezeAuthority: PublicKey | null; /** * lamport amount for mint account rent exemption */ @@ -230,15 +278,15 @@ export type CreateMintParams = { */ export type MergeTokenAccountsParams = { /** - * Tx feepayer + * Fee payer */ payer: PublicKey; /** - * Owner of the token accounts to be merged + * Owner of the compressed token accounts to be merged */ owner: PublicKey; /** - * Mint public key + * SPL Token mint address */ mint: PublicKey; /** @@ -246,15 +294,11 @@ export type MergeTokenAccountsParams = { */ inputCompressedTokenAccounts: ParsedTokenAccount[]; /** - * Optional: Public key of the state tree to merge into + * Validity proof for state inclusion */ - outputStateTree: PublicKey; + recentValidityProof: ValidityProof | CompressedProof | null; /** - * Optional: Recent validity proof for state inclusion - */ - recentValidityProof: CompressedProof; - /** - * Optional: Recent state root indices of the input state + * State root indices of the input state */ recentInputStateRootIndices: number[]; }; @@ -264,44 +308,47 @@ export type MergeTokenAccountsParams = { */ export type MintToParams = { /** - * Tx feepayer + * Fee payer */ feePayer: PublicKey; /** - * Mint authority + * Token mint address */ - authority: PublicKey; + mint: PublicKey; /** - * Mint public key + * Mint authority */ - mint: PublicKey; + authority: PublicKey; /** - * The Solana Public Keys to mint to. + * Recipient address(es) */ toPubkey: PublicKey[] | PublicKey; /** - * The amount of compressed tokens to mint. + * Token amount(s) to mint */ amount: BN | BN[] | number | number[]; /** - * Public key of the state tree to mint into. Defaults to a public state - * tree if unspecified. + * State tree for minted tokens */ - merkleTree?: PublicKey; + outputStateTreeInfo: TreeInfo; /** - * Optional: The token program ID. Default: SPL Token Program ID + * Token pool */ - tokenProgramId?: PublicKey; + tokenPoolInfo: TokenPoolInfo; }; /** * Register an existing SPL mint account to the compressed token program * Creates an omnibus account for the mint */ -export type RegisterMintParams = { - /** Tx feepayer */ +export type CreateTokenPoolParams = { + /** + * Fee payer + */ feePayer: PublicKey; - /** Mint public key */ + /** + * SPL Mint address + */ mint: PublicKey; /** * Optional: The token program ID. Default: SPL Token Program ID @@ -309,14 +356,37 @@ export type RegisterMintParams = { tokenProgramId?: PublicKey; }; +export type AddTokenPoolParams = { + /** + * Fee payer + */ + feePayer: PublicKey; + /** + * Token mint address + */ + mint: PublicKey; + /** + * Token pool index + */ + poolIndex: number; + /** + * Optional: Token program ID. Default: SPL Token Program ID + */ + tokenProgramId?: PublicKey; +}; + /** * Mint from existing SPL mint to compressed token accounts */ export type ApproveAndMintToParams = { /** - * Tx feepayer + * Fee payer */ feePayer: PublicKey; + /** + * SPL Mint address + */ + mint: PublicKey; /** * Mint authority */ @@ -326,47 +396,42 @@ export type ApproveAndMintToParams = { */ authorityTokenAccount: PublicKey; /** - * Mint public key - */ - mint: PublicKey; - /** - * The Solana Public Key to mint to. + * Recipient address */ toPubkey: PublicKey; /** - * The amount of compressed tokens to mint. + * Token amount to mint */ amount: BN | number; /** - * Public key of the state tree to mint into. Defaults to a public state - * tree if unspecified. + * State tree to write to */ - merkleTree?: PublicKey; + outputStateTreeInfo: TreeInfo; /** - * Optional: The token program ID. Default: SPL Token Program ID + * Token pool */ - tokenProgramId?: PublicKey; + tokenPoolInfo: TokenPoolInfo; }; export type CreateTokenProgramLookupTableParams = { /** - * The payer of the transaction. + * Fee payer */ payer: PublicKey; /** - * The authority of the transaction. + * Authority of the transaction */ authority: PublicKey; /** - * Recently finalized Solana slot. + * Optional Mint addresses to store in the lookup table */ - recentSlot: number; + mints?: PublicKey[]; /** - * Optional Mint addresses to store in the lookup table. + * Recently finalized Solana slot */ - mints?: PublicKey[]; + recentSlot: number; /** - * Optional additional addresses to store in the lookup table. + * Optional additional addresses to store in the lookup table */ remainingAccounts?: PublicKey[]; }; @@ -406,6 +471,33 @@ export const parseTokenData = ( return { mint, currentOwner, delegate }; }; +export const parseMaybeDelegatedTransfer = ( + inputs: ParsedTokenAccount[], + outputs: TokenTransferOutputData[], +): { delegatedTransfer: DelegatedTransfer | null; authority: PublicKey } => { + if (inputs.length < 1) + throw new Error('Must supply at least one input token account.'); + + const owner = inputs[0].parsed.owner; + + const delegatedAccountsIndex = inputs.findIndex(a => a.parsed.delegate); + + /// Fast path: no delegated account used + if (delegatedAccountsIndex === -1) + return { delegatedTransfer: null, authority: owner }; + + const delegate = inputs[delegatedAccountsIndex].parsed.delegate; + const delegateChangeAccountIndex = outputs.length <= 1 ? null : 0; + + return { + delegatedTransfer: { + owner, + delegateChangeAccountIndex, + }, + authority: delegate!, + }; +}; + /** * Create the output state for a transfer transaction. * @param inputCompressedTokenAccounts Input state @@ -530,7 +622,14 @@ export class CompressedTokenProgram { : programId; } - /** @internal */ + /** + * Derive the token pool pda. + * To derive the token pool pda with bump, use {@link deriveTokenPoolPdaWithIndex}. + * + * @param mint The mint of the token pool + * + * @returns The token pool pda + */ static deriveTokenPoolPda(mint: PublicKey): PublicKey { const seeds = [POOL_SEED, mint.toBuffer()]; const [address, _] = PublicKey.findProgramAddressSync( @@ -540,6 +639,57 @@ export class CompressedTokenProgram { return address; } + /** + * Find the index and bump for a given token pool pda and mint. + * + * @param poolPda The token pool pda to find the index and bump for + * @param mint The mint of the token pool + * + * @returns The index and bump number. + */ + static findTokenPoolIndexAndBump( + poolPda: PublicKey, + mint: PublicKey, + ): [number, number] { + for (let index = 0; index < 5; index++) { + const derivedPda = this.deriveTokenPoolPdaWithIndex(mint, index); + if (derivedPda[0].equals(poolPda)) { + return [index, derivedPda[1]]; + } + } + throw new Error('Token pool not found'); + } + + /** + * Derive the token pool pda with index. + * + * @param mint The mint of the token pool + * @param index Index. starts at 0. The Protocol supports 4 indexes aka token pools + * per mint. + * + * @returns The token pool pda and bump. + */ + static deriveTokenPoolPdaWithIndex( + mint: PublicKey, + index: number, + ): [PublicKey, number] { + let seeds: Buffer[] = []; + if (index === 0) { + seeds = [Buffer.from('pool'), mint.toBuffer(), Buffer.from([])]; // legacy, 1st + } else { + seeds = [ + Buffer.from('pool'), + mint.toBuffer(), + Buffer.from([index]), + ]; + } + const [address, bump] = PublicKey.findProgramAddressSync( + seeds, + this.programId, + ); + return [address, bump]; + } + /** @internal */ static get deriveCpiAuthorityPda(): PublicKey { const [address, _] = PublicKey.findProgramAddressSync( @@ -551,23 +701,32 @@ export class CompressedTokenProgram { /** * Construct createMint instruction for compressed tokens. - * @returns [createMintAccountInstruction, initializeMintInstruction, createTokenPoolInstruction] * - * Note that `createTokenPoolInstruction` must be executed after `initializeMintInstruction`. - */ - static async createMint( - params: CreateMintParams, - ): Promise { - const { - mint, - authority, - feePayer, - rentExemptBalance, - tokenProgramId, - freezeAuthority, - mintSize, - } = params; - + * @param feePayer Fee payer. + * @param mint SPL Mint address. + * @param authority Mint authority. + * @param freezeAuthority Optional: freeze authority. + * @param decimals Decimals. + * @param rentExemptBalance Lamport amount for mint account rent exemption. + * @param tokenProgramId Optional: Token program ID. Default: SPL Token Program ID + * @param mintSize Optional: mint size. Default: MINT_SIZE + * + * @returns [createMintAccountInstruction, initializeMintInstruction, + * createTokenPoolInstruction] + * + * Note that `createTokenPoolInstruction` must be executed after + * `initializeMintInstruction`. + */ + static async createMint({ + feePayer, + mint, + authority, + freezeAuthority, + decimals, + rentExemptBalance, + tokenProgramId, + mintSize, + }: CreateMintParams): Promise { const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; /// Create and initialize SPL Mint account @@ -580,7 +739,7 @@ export class CompressedTokenProgram { }); const initializeMintInstruction = createInitializeMint2Instruction( mint, - params.decimals, + decimals, authority, freezeAuthority, tokenProgram, @@ -602,20 +761,27 @@ export class CompressedTokenProgram { /** * Enable compression for an existing SPL mint, creating an omnibus account. * For new mints, use `CompressedTokenProgram.createMint`. + * + * @param feePayer Fee payer. + * @param mint SPL Mint address. + * @param tokenProgramId Optional: Token program ID. Default: SPL + * Token Program ID + * + * @returns The createTokenPool instruction */ - static async createTokenPool( - params: RegisterMintParams, - ): Promise { - const { mint, feePayer, tokenProgramId } = params; - + static async createTokenPool({ + feePayer, + mint, + tokenProgramId, + }: CreateTokenPoolParams): Promise { const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; - const tokenPoolPda = this.deriveTokenPoolPda(mint); + const tokenPoolPda = this.deriveTokenPoolPdaWithIndex(mint, 0); const keys = createTokenPoolAccountsLayout({ mint, feePayer, - tokenPoolPda, + tokenPoolPda: tokenPoolPda[0], tokenProgram, cpiAuthorityPda: this.deriveCpiAuthorityPda, systemProgram: SystemProgram.programId, @@ -629,26 +795,89 @@ export class CompressedTokenProgram { } /** - * Construct mintTo instruction for compressed tokens - */ - static async mintTo(params: MintToParams): Promise { - const systemKeys = defaultStaticAccountsStruct(); + * Add a token pool to an existing SPL mint. For new mints, use + * {@link createTokenPool}. + * + * @param feePayer Fee payer. + * @param mint SPL Mint address. + * @param poolIndex Pool index. + * @param tokenProgramId Optional: Token program ID. Default: SPL + * Token Program ID + * + * @returns The addTokenPool instruction + */ + static async addTokenPool({ + feePayer, + mint, + poolIndex, + tokenProgramId, + }: AddTokenPoolParams): Promise { + if (poolIndex <= 0) { + throw new Error( + 'Pool index must be greater than 0. For 0, use CreateTokenPool instead.', + ); + } + if (poolIndex > 3) { + throw new Error( + `Invalid poolIndex ${poolIndex}. Max 4 pools per mint.`, + ); + } - const { + const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; + + const existingTokenPoolPda = this.deriveTokenPoolPdaWithIndex( + mint, + poolIndex - 1, + ); + const tokenPoolPda = this.deriveTokenPoolPdaWithIndex(mint, poolIndex); + + const keys = addTokenPoolAccountsLayout({ mint, feePayer, - authority, - merkleTree, - toPubkey, - amount, - tokenProgramId, - } = params; - const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; + tokenPoolPda: tokenPoolPda[0], + existingTokenPoolPda: existingTokenPoolPda[0], + tokenProgram, + cpiAuthorityPda: this.deriveCpiAuthorityPda, + systemProgram: SystemProgram.programId, + }); - const tokenPoolPda = this.deriveTokenPoolPda(mint); + return new TransactionInstruction({ + programId: this.programId, + keys, + data: Buffer.concat([ + new Uint8Array(ADD_TOKEN_POOL_DISCRIMINATOR), + new Uint8Array(Buffer.from([poolIndex])), + ]), + }); + } - const amounts = toArray(amount).map(amount => bn(amount)); + /** + * Construct mintTo instruction for compressed tokens + * + * @param feePayer Fee payer. + * @param mint SPL Mint address. + * @param authority Mint authority. + * @param toPubkey Recipient owner address. + * @param amount Amount of tokens to mint. + * @param outputStateTreeInfo State tree to write to. + * @param tokenPoolInfo Token pool info. + * + * @returns The mintTo instruction + */ + static async mintTo({ + feePayer, + mint, + authority, + toPubkey, + amount, + outputStateTreeInfo, + tokenPoolInfo, + }: MintToParams): Promise { + const systemKeys = defaultStaticAccountsStruct(); + const tokenProgram = tokenPoolInfo.tokenProgram; + checkTokenPoolInfo(tokenPoolInfo, mint); + const amounts = toArray(amount).map(amount => bn(amount)); const toPubkeys = toArray(toPubkey); if (amounts.length !== toPubkeys.length) { @@ -663,16 +892,18 @@ 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: featureFlags.isV2() + ? outputStateTreeInfo.queue + : outputStateTreeInfo.tree, selfProgram: this.programId, systemProgram: SystemProgram.programId, - solPoolPda: null, // TODO: add lamports support + solPoolPda: null, }); const data = encodeMintToInstructionData({ @@ -690,28 +921,39 @@ export class CompressedTokenProgram { /** * Mint tokens from registered SPL mint account to a compressed account - */ - static async approveAndMintTo(params: ApproveAndMintToParams) { - const { - mint, - feePayer, - authorityTokenAccount, - authority, - merkleTree, - toPubkey, - tokenProgramId, - } = params; - - const amount: bigint = BigInt(params.amount.toString()); + * + * @param feePayer Fee payer. + * @param mint SPL Mint address. + * @param authority Mint authority. + * @param authorityTokenAccount The mint authority's associated token + * account (ATA). + * @param toPubkey Recipient owner address. + * @param amount Amount of tokens to mint. + * @param outputStateTreeInfo State tree to write to. + * @param tokenPoolInfo Token pool info. + * + * @returns The mintTo instruction + */ + static async approveAndMintTo({ + feePayer, + mint, + authority, + authorityTokenAccount, + toPubkey, + amount, + outputStateTreeInfo, + tokenPoolInfo, + }: ApproveAndMintToParams) { + const amountBigInt: bigint = BigInt(amount.toString()); /// 1. Mint to existing ATA of mintAuthority. const splMintToInstruction = createMintToInstruction( mint, authorityTokenAccount, authority, - amount, + amountBigInt, [], - tokenProgramId, + tokenPoolInfo.tokenProgram, ); /// 2. Compress from mint authority ATA to recipient compressed account @@ -721,54 +963,62 @@ export class CompressedTokenProgram { source: authorityTokenAccount, toAddress: toPubkey, mint, - amount: params.amount, - outputStateTree: merkleTree, - tokenProgramId, + amount, + outputStateTreeInfo, + tokenPoolInfo, }); return [splMintToInstruction, compressInstruction]; } + /** * Construct transfer instruction for compressed tokens - */ - static async transfer( - params: TransferParams, - ): Promise { - const { - payer, - inputCompressedTokenAccounts, - recentInputStateRootIndices, - recentValidityProof, - amount, - outputStateTrees, - toAddress, - } = params; - + * + * @param payer Fee payer. + * @param inputCompressedTokenAccounts Source compressed token accounts. + * @param toAddress Recipient owner address. + * @param amount Amount of tokens to transfer. + * @param recentValidityProof Recent validity proof. + * @param recentInputStateRootIndices Recent state root indices. + * + * @returns The transfer instruction + */ + static async transfer({ + payer, + inputCompressedTokenAccounts, + toAddress, + amount, + recentValidityProof, + recentInputStateRootIndices, + }: TransferParams): Promise { const tokenTransferOutputs: TokenTransferOutputData[] = createTransferOutputState( inputCompressedTokenAccounts, toAddress, amount, ); + const { inputTokenDataWithContext, packedOutputTokenData, remainingAccountMetas, } = packCompressedTokenAccounts({ inputCompressedTokenAccounts, - outputStateTrees, rootIndices: recentInputStateRootIndices, tokenTransferOutputs, }); - const { mint, currentOwner } = parseTokenData( + const { mint } = parseTokenData(inputCompressedTokenAccounts); + + const { delegatedTransfer, authority } = parseMaybeDelegatedTransfer( inputCompressedTokenAccounts, + tokenTransferOutputs, ); const rawData: CompressedTokenInstructionDataTransfer = { proof: recentValidityProof, mint, - delegatedTransfer: null, // TODO: implement + delegatedTransfer, inputTokenDataWithContext, outputCompressedAccounts: packedOutputTokenData, compressOrDecompressAmount: null, @@ -786,7 +1036,7 @@ export class CompressedTokenProgram { } = defaultStaticAccountsStruct(); const keys = transferAccountsLayout({ feePayer: payer, - authority: currentOwner, + authority, cpiAuthorityPda: this.deriveCpiAuthorityPda, lightSystemProgram: LightSystemProgram.programId, registeredProgramPda: registeredProgramPda, @@ -810,14 +1060,24 @@ export class CompressedTokenProgram { } /** - * Create lookup table instructions for the token program's default accounts. - */ - static async createTokenProgramLookupTable( - params: CreateTokenProgramLookupTableParams, - ) { - const { authority, mints, recentSlot, payer, remainingAccounts } = - params; - + * Create lookup table instructions for the token program's default + * accounts. + * + * @param payer Fee payer. + * @param authority Authority. + * @param mints Mints. + * @param recentSlot Recent slot. + * @param remainingAccounts Remaining accounts. + * + * @returns [createInstruction, extendInstruction, option(extendInstruction2)] + */ + static async createTokenProgramLookupTable({ + payer, + authority, + mints, + recentSlot, + remainingAccounts, + }: CreateTokenProgramLookupTableParams) { const [createInstruction, lookupTableAddress] = AddressLookupTableProgram.createLookupTable({ authority, @@ -838,8 +1098,11 @@ export class CompressedTokenProgram { authority, lookupTable: lookupTableAddress, addresses: [ + SystemProgram.programId, + ComputeBudgetProgram.programId, this.deriveCpiAuthorityPda, LightSystemProgram.programId, + CompressedTokenProgram.programId, defaultStaticAccountsStruct().registeredProgramPda, defaultStaticAccountsStruct().noopProgram, defaultStaticAccountsStruct().accountCompressionAuthority, @@ -853,139 +1116,198 @@ export class CompressedTokenProgram { TOKEN_2022_PROGRAM_ID, authority, ...optionalMintKeys, - ...(remainingAccounts ?? []), ], }); + const instructions = [createInstruction, extendInstruction]; + + if (remainingAccounts && remainingAccounts.length > 0) { + for (let i = 0; i < remainingAccounts.length; i += 25) { + const chunk = remainingAccounts.slice(i, i + 25); + const extendIx = AddressLookupTableProgram.extendLookupTable({ + payer, + authority, + lookupTable: lookupTableAddress, + addresses: chunk, + }); + instructions.push(extendIx); + } + } + return { - instructions: [createInstruction, extendInstruction], + instructions, address: lookupTableAddress, }; } /** * Create compress instruction - * @returns compressInstruction - */ - static async compress( - params: CompressParams, - ): Promise { - const { - payer, - owner, - source, - toAddress, - mint, - outputStateTree, - tokenProgramId, - } = params; + * + * @param payer Fee payer. + * @param owner Owner of uncompressed token account. + * @param source Source SPL Token account address. + * @param toAddress Recipient owner address(es). + * @param amount Amount of tokens to compress. + * @param mint SPL Token mint address. + * @param outputStateTreeInfo State tree to write to. + * @param tokenPoolInfo Token pool info. + * + * @returns The compress instruction + */ + static async compress({ + payer, + owner, + source, + toAddress, + amount, + mint, + outputStateTreeInfo, + tokenPoolInfo, + }: CompressParams): Promise { + let tokenTransferOutputs: TokenTransferOutputData[]; - if (Array.isArray(params.amount) !== Array.isArray(params.toAddress)) { + const amountArray = toArray(amount); + const toAddressArray = toArray(toAddress); + + checkTokenPoolInfo(tokenPoolInfo, mint); + + if (amountArray.length !== toAddressArray.length) { throw new Error( - 'Both amount and toAddress must be arrays or both must be single values', + 'Amount and toAddress arrays must have the same length', ); } + if (featureFlags.isV2()) { + const [index, bump] = this.findTokenPoolIndexAndBump( + tokenPoolInfo.tokenPoolPda, + mint, + ); + const rawData: BatchCompressInstructionData = { + pubkeys: toAddressArray, + amounts: + amountArray.length > 1 + ? amountArray.map(amt => bn(amt)) + : null, + lamports: null, + amount: amountArray.length === 1 ? bn(amountArray[0]) : null, + index, + bump, + }; + + const data = encodeBatchCompressInstructionData(rawData); + const keys = mintToAccountsLayout({ + mint, + feePayer: payer, + authority: owner, + cpiAuthorityPda: this.deriveCpiAuthorityPda, + tokenProgram: tokenPoolInfo.tokenProgram, + tokenPoolPda: tokenPoolInfo.tokenPoolPda, + lightSystemProgram: LightSystemProgram.programId, + ...defaultStaticAccountsStruct(), + merkleTree: outputStateTreeInfo.queue, + selfProgram: this.programId, + systemProgram: SystemProgram.programId, + solPoolPda: null, + }); + keys.push({ + pubkey: source, + isWritable: true, + isSigner: false, + }); - let tokenTransferOutputs: TokenTransferOutputData[]; - - if (Array.isArray(params.amount) && Array.isArray(params.toAddress)) { - if (params.amount.length !== params.toAddress.length) { - throw new Error( - 'Amount and toAddress arrays must have the same length', - ); - } - tokenTransferOutputs = params.amount.map((amt, index) => { - const amount = bn(amt); + return new TransactionInstruction({ + programId: this.programId, + keys, + data, + }); + } else { + tokenTransferOutputs = amountArray.map((amt, index) => { + const amountBN = bn(amt); return { - owner: (params.toAddress as PublicKey[])[index], - amount, - lamports: bn(0), + owner: toAddressArray[index], + amount: amountBN, + lamports: null, tlv: null, }; }); - } else { - tokenTransferOutputs = [ - { - owner: toAddress as PublicKey, - amount: bn(params.amount as number | BN), - lamports: bn(0), - tlv: null, - }, - ]; - } - const { - inputTokenDataWithContext, - packedOutputTokenData, - remainingAccountMetas, - } = packCompressedTokenAccounts({ - inputCompressedTokenAccounts: [], - outputStateTrees: outputStateTree, - rootIndices: [], - tokenTransferOutputs, - }); - - const rawData: CompressedTokenInstructionDataTransfer = { - proof: null, - mint, - delegatedTransfer: null, // TODO: implement - inputTokenDataWithContext, - outputCompressedAccounts: packedOutputTokenData, - compressOrDecompressAmount: Array.isArray(params.amount) - ? params.amount - .map(amt => new BN(amt)) - .reduce((sum, amt) => sum.add(amt), new BN(0)) - : new BN(params.amount), - isCompress: true, - cpiContext: null, - lamportsChangeAccountMerkleTreeIndex: null, - }; - const data = encodeTransferInstructionData(rawData); - - const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; - - const keys = transferAccountsLayout({ - ...defaultStaticAccountsStruct(), - feePayer: payer, - authority: owner, - cpiAuthorityPda: this.deriveCpiAuthorityPda, - lightSystemProgram: LightSystemProgram.programId, - selfProgram: this.programId, - systemProgram: SystemProgram.programId, - tokenPoolPda: this.deriveTokenPoolPda(mint), - compressOrDecompressTokenAccount: source, - tokenProgram, - }); + const { + inputTokenDataWithContext, + packedOutputTokenData, + remainingAccountMetas, + } = packCompressedTokenAccounts({ + inputCompressedTokenAccounts: [], + outputStateTreeInfo, + rootIndices: [], + tokenTransferOutputs, + }); - keys.push(...remainingAccountMetas); + const rawData: CompressedTokenInstructionDataTransfer = { + proof: null, + mint, + delegatedTransfer: null, + inputTokenDataWithContext, + outputCompressedAccounts: packedOutputTokenData, + compressOrDecompressAmount: Array.isArray(amount) + ? amount + .map(amt => bn(amt)) + .reduce((sum, amt) => sum.add(amt), bn(0)) + : bn(amount), + isCompress: true, + cpiContext: null, + lamportsChangeAccountMerkleTreeIndex: null, + }; + const data = encodeTransferInstructionData(rawData); + const keys = transferAccountsLayout({ + ...defaultStaticAccountsStruct(), + feePayer: payer, + authority: owner, + cpiAuthorityPda: this.deriveCpiAuthorityPda, + lightSystemProgram: LightSystemProgram.programId, + selfProgram: this.programId, + systemProgram: SystemProgram.programId, + tokenPoolPda: tokenPoolInfo.tokenPoolPda, + compressOrDecompressTokenAccount: source, + tokenProgram: tokenPoolInfo.tokenProgram, + }); + keys.push(...remainingAccountMetas); - return new TransactionInstruction({ - programId: this.programId, - keys, - data, - }); + return new TransactionInstruction({ + programId: this.programId, + keys, + data, + }); + } } /** * Construct decompress instruction - */ - static async decompress( - params: DecompressParams, - ): Promise { - const { - payer, - inputCompressedTokenAccounts, - toAddress, - outputStateTree, - recentValidityProof, - recentInputStateRootIndices, - tokenProgramId, - } = params; - const amount = bn(params.amount); + * + * @param payer Fee payer. + * @param inputCompressedTokenAccounts Source compressed token accounts. + * @param toAddress Destination **uncompressed** token + * account address. (ATA) + * @param amount Amount of tokens to decompress. + * @param recentValidityProof Recent validity proof. + * @param recentInputStateRootIndices Recent state root indices. + * @param tokenPoolInfos Token pool info. + * + * @returns The decompress instruction + */ + static async decompress({ + payer, + inputCompressedTokenAccounts, + toAddress, + amount, + recentValidityProof, + recentInputStateRootIndices, + tokenPoolInfos, + }: DecompressParams): Promise { + const amountBN = bn(amount); + const tokenPoolInfosArray = toArray(tokenPoolInfos); const tokenTransferOutputs = createDecompressOutputState( inputCompressedTokenAccounts, - amount, + amountBN, ); /// Pack @@ -995,28 +1317,33 @@ export class CompressedTokenProgram { remainingAccountMetas, } = packCompressedTokenAccounts({ inputCompressedTokenAccounts, - outputStateTrees: outputStateTree, rootIndices: recentInputStateRootIndices, tokenTransferOutputs: tokenTransferOutputs, + remainingAccounts: tokenPoolInfosArray + .slice(1) + .map(info => info.tokenPoolPda), }); - const { mint, currentOwner } = parseTokenData( + const { mint } = parseTokenData(inputCompressedTokenAccounts); + const { delegatedTransfer, authority } = parseMaybeDelegatedTransfer( inputCompressedTokenAccounts, + tokenTransferOutputs, ); const rawData: CompressedTokenInstructionDataTransfer = { proof: recentValidityProof, mint, - delegatedTransfer: null, // TODO: implement + delegatedTransfer, inputTokenDataWithContext, outputCompressedAccounts: packedOutputTokenData, - compressOrDecompressAmount: amount, + compressOrDecompressAmount: amountBN, isCompress: false, cpiContext: null, lamportsChangeAccountMerkleTreeIndex: null, }; const data = encodeTransferInstructionData(rawData); - const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; + const tokenProgram = tokenPoolInfosArray[0].tokenProgram; + const { accountCompressionAuthority, noopProgram, @@ -1026,7 +1353,7 @@ export class CompressedTokenProgram { const keys = transferAccountsLayout({ feePayer: payer, - authority: currentOwner, + authority: authority, cpiAuthorityPda: this.deriveCpiAuthorityPda, lightSystemProgram: LightSystemProgram.programId, registeredProgramPda: registeredProgramPda, @@ -1034,12 +1361,11 @@ export class CompressedTokenProgram { accountCompressionAuthority: accountCompressionAuthority, accountCompressionProgram: accountCompressionProgram, selfProgram: this.programId, - tokenPoolPda: this.deriveTokenPoolPda(mint), + tokenPoolPda: tokenPoolInfosArray[0].tokenPoolPda, compressOrDecompressTokenAccount: toAddress, tokenProgram, systemProgram: SystemProgram.programId, }); - keys.push(...remainingAccountMetas); return new TransactionInstruction({ @@ -1049,31 +1375,41 @@ export class CompressedTokenProgram { }); } - static async mergeTokenAccounts( - params: MergeTokenAccountsParams, - ): Promise { - const { - payer, - owner, - inputCompressedTokenAccounts, - outputStateTree, - recentValidityProof, - recentInputStateRootIndices, - } = params; - - if (inputCompressedTokenAccounts.length > 3) { - throw new Error('Cannot merge more than 3 token accounts at once'); + /** + * Create `mergeTokenAccounts` instruction + * + * @param payer Fee payer. + * @param owner Owner of the compressed token + * accounts to be merged. + * @param inputCompressedTokenAccounts Source compressed token accounts. + * @param mint SPL Token mint address. + * @param recentValidityProof Recent validity proof. + * @param recentInputStateRootIndices Recent state root indices. + * + * @returns instruction + */ + static async mergeTokenAccounts({ + payer, + owner, + inputCompressedTokenAccounts, + mint, + recentValidityProof, + recentInputStateRootIndices, + }: MergeTokenAccountsParams): Promise { + if (inputCompressedTokenAccounts.length > 4) { + throw new Error('Cannot merge more than 4 token accounts at once'); } + checkMint(inputCompressedTokenAccounts, mint); + const ix = await this.transfer({ payer, inputCompressedTokenAccounts, toAddress: owner, amount: inputCompressedTokenAccounts.reduce( (sum, account) => sum.add(account.parsed.amount), - new BN(0), + bn(0), ), - outputStateTrees: outputStateTree, recentInputStateRootIndices, recentValidityProof, }); @@ -1081,23 +1417,34 @@ export class CompressedTokenProgram { return [ix]; } - static async compressSplTokenAccount( - params: CompressSplTokenAccountParams, - ): Promise { - const { - feePayer, - authority, - tokenAccount, - mint, - remainingAmount, - outputStateTree, - tokenProgramId, - } = params; - const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; - + /** + * Create `compressSplTokenAccount` instruction + * + * @param feePayer Fee payer. + * @param authority SPL Token account owner. + * @param tokenAccount SPL Token account to compress. + * @param mint SPL Token mint address. + * @param remainingAmount Optional: Amount to leave in token account. + * @param outputStateTreeInfo State tree to write to. + * @param tokenPoolInfo Token pool info. + * + * @returns instruction + */ + static async compressSplTokenAccount({ + feePayer, + authority, + tokenAccount, + mint, + remainingAmount, + outputStateTreeInfo, + tokenPoolInfo, + }: CompressSplTokenAccountParams): Promise { + checkTokenPoolInfo(tokenPoolInfo, mint); const remainingAccountMetas: AccountMeta[] = [ { - pubkey: outputStateTree, + pubkey: featureFlags.isV2() + ? outputStateTreeInfo.queue + : outputStateTreeInfo.tree, isSigner: false, isWritable: true, }, @@ -1114,6 +1461,7 @@ export class CompressedTokenProgram { registeredProgramPda, accountCompressionProgram, } = defaultStaticAccountsStruct(); + const keys = transferAccountsLayout({ feePayer, authority, @@ -1124,9 +1472,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,10 +1487,158 @@ export class CompressedTokenProgram { }); } - static async get_mint_program_id( + /** + * Get the program ID for a mint + * + * @param mint SPL Token mint address. + * @param connection Connection. + * + * @returns program ID + */ + static async getMintProgramId( mint: PublicKey, connection: Connection, ): Promise { return (await connection.getAccountInfo(mint))?.owner; } + + /** + * Create `approve` instruction to delegate compressed tokens. + * + * @param payer Fee payer. + * @param inputCompressedTokenAccounts Source compressed token accounts. + * @param toAddress Owner to delegate to. + * @param amount Amount of tokens to delegate. + * @param recentValidityProof Recent validity proof. + * @param recentInputStateRootIndices Recent state root indices. + * + * @returns instruction + */ + static async approve({ + payer, + inputCompressedTokenAccounts, + toAddress, + amount, + recentValidityProof, + recentInputStateRootIndices, + }: ApproveParams): Promise { + const { inputTokenDataWithContext, remainingAccountMetas } = + packCompressedTokenAccounts({ + inputCompressedTokenAccounts, + rootIndices: recentInputStateRootIndices, + tokenTransferOutputs: [], + }); + + const { mint, currentOwner } = parseTokenData( + inputCompressedTokenAccounts, + ); + + const rawData: CompressedTokenInstructionDataApprove = { + proof: recentValidityProof, + mint, + inputTokenDataWithContext, + cpiContext: null, + delegate: toAddress, + delegatedAmount: bn(amount), + delegateMerkleTreeIndex: 1, // TODO: find better solution. + changeAccountMerkleTreeIndex: 1, + delegateLamports: null, + }; + + const data = encodeApproveInstructionData(rawData); + + const { + accountCompressionAuthority, + noopProgram, + registeredProgramPda, + accountCompressionProgram, + } = defaultStaticAccountsStruct(); + + const keys = approveAccountsLayout({ + feePayer: payer, + authority: currentOwner, + cpiAuthorityPda: this.deriveCpiAuthorityPda, + lightSystemProgram: LightSystemProgram.programId, + registeredProgramPda: registeredProgramPda, + noopProgram: noopProgram, + accountCompressionAuthority: accountCompressionAuthority, + accountCompressionProgram: accountCompressionProgram, + selfProgram: this.programId, + systemProgram: SystemProgram.programId, + }); + + keys.push(...remainingAccountMetas); + + return new TransactionInstruction({ + programId: this.programId, + keys, + data, + }); + } + + /** + * Create `revoke` instruction to revoke delegation of compressed tokens. + * + * @param payer Fee payer. + * @param inputCompressedTokenAccounts Source compressed token accounts. + * @param recentValidityProof Recent validity proof. + * @param recentInputStateRootIndices Recent state root indices. + * + * @returns instruction + */ + static async revoke({ + payer, + inputCompressedTokenAccounts, + recentValidityProof, + recentInputStateRootIndices, + }: RevokeParams): Promise { + validateSameTokenOwner(inputCompressedTokenAccounts); + + const { inputTokenDataWithContext, remainingAccountMetas } = + packCompressedTokenAccounts({ + inputCompressedTokenAccounts, + rootIndices: recentInputStateRootIndices, + tokenTransferOutputs: [], + }); + + const { mint, currentOwner } = parseTokenData( + inputCompressedTokenAccounts, + ); + + const rawData: CompressedTokenInstructionDataRevoke = { + proof: recentValidityProof, + mint, + inputTokenDataWithContext, + cpiContext: null, + outputAccountMerkleTreeIndex: 2, // Because of the delegate account. + }; + const data = encodeRevokeInstructionData(rawData); + + const { + accountCompressionAuthority, + noopProgram, + registeredProgramPda, + accountCompressionProgram, + } = defaultStaticAccountsStruct(); + const keys = revokeAccountsLayout({ + feePayer: payer, + authority: currentOwner, + cpiAuthorityPda: this.deriveCpiAuthorityPda, + lightSystemProgram: LightSystemProgram.programId, + registeredProgramPda: registeredProgramPda, + noopProgram: noopProgram, + accountCompressionAuthority: accountCompressionAuthority, + accountCompressionProgram: accountCompressionProgram, + selfProgram: this.programId, + systemProgram: SystemProgram.programId, + }); + + keys.push(...remainingAccountMetas); + + return new TransactionInstruction({ + programId: this.programId, + keys, + data, + }); + } } diff --git a/js/compressed-token/src/types.ts b/js/compressed-token/src/types.ts index 208ea5b849..afa29d99da 100644 --- a/js/compressed-token/src/types.ts +++ b/js/compressed-token/src/types.ts @@ -1,15 +1,12 @@ import { PublicKey } from '@solana/web3.js'; import BN from 'bn.js'; +import { Buffer } from 'buffer'; import { - CompressedProof, + ValidityProof, PackedMerkleContext, + CompressedCpiContext, } from '@lightprotocol/stateless.js'; - -export type CompressedCpiContext = { - setContext: boolean; - firstSetContext: boolean; - cpiContextAccountIndex: number; // u8 -}; +import { TokenPoolInfo } from './utils/get-token-pool-infos'; export type TokenTransferOutputData = { /** @@ -67,6 +64,21 @@ export type DelegatedTransfer = { delegateChangeAccountIndex: number | null; }; +export type BatchCompressInstructionData = { + pubkeys: PublicKey[]; + amounts: BN[] | null; + lamports: BN | null; + amount: BN | null; + index: number; + bump: number; +}; + +// pub amounts: Option>, +// pub lamports: Option, +// pub amount: Option, +// pub index: u8, +// pub bump: u8, + export type MintToInstructionData = { recipients: PublicKey[]; amounts: BN[]; @@ -78,18 +90,23 @@ export type CompressSplTokenAccountInstructionData = { cpiContext: CompressedCpiContext | null; }; +export function isSingleTokenPoolInfo( + tokenPoolInfos: TokenPoolInfo | TokenPoolInfo[], +): tokenPoolInfos is TokenPoolInfo { + return !Array.isArray(tokenPoolInfos); +} + export type CompressedTokenInstructionDataTransfer = { /** * Validity proof */ - proof: CompressedProof | null; + proof: ValidityProof | null; /** * The mint of the transfer */ mint: PublicKey; /** * Whether the signer is a delegate - * TODO: implement delegated transfer struct */ delegatedTransfer: DelegatedTransfer | null; /** @@ -146,3 +163,23 @@ export type TokenData = { */ tlv: Buffer | null; }; + +export type CompressedTokenInstructionDataApprove = { + proof: ValidityProof | null; + mint: PublicKey; + inputTokenDataWithContext: InputTokenDataWithContext[]; + cpiContext: CompressedCpiContext | null; + delegate: PublicKey; + delegatedAmount: BN; + delegateMerkleTreeIndex: number; + changeAccountMerkleTreeIndex: number; + delegateLamports: BN | null; +}; + +export type CompressedTokenInstructionDataRevoke = { + proof: ValidityProof | null; + mint: PublicKey; + inputTokenDataWithContext: InputTokenDataWithContext[]; + cpiContext: CompressedCpiContext | null; + outputAccountMerkleTreeIndex: number; +}; 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..678fffc5c2 --- /dev/null +++ b/js/compressed-token/src/utils/get-token-pool-infos.ts @@ -0,0 +1,229 @@ +import { Commitment, PublicKey } from '@solana/web3.js'; +import { unpackAccount } from '@solana/spl-token'; +import { CompressedTokenProgram } from '../program'; +import { bn, 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 addressesAndBumps = Array.from({ length: 5 }, (_, i) => + CompressedTokenProgram.deriveTokenPoolPdaWithIndex(mint, i), + ); + + const accountInfos = await rpc.getMultipleAccountsInfo( + addressesAndBumps.map(addressAndBump => addressAndBump[0]), + 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 = addressesAndBumps.map((addressAndBump, i) => + accountInfos[i] + ? unpackAccount( + addressAndBump[0], + accountInfos[i], + accountInfos[i].owner, + ) + : null, + ); + + const tokenProgram = accountInfos[0].owner; + return parsedInfos.map((parsedInfo, i) => { + if (!parsedInfo) { + return { + mint, + tokenPoolPda: addressesAndBumps[i][0], + tokenProgram, + activity: undefined, + balance: bn(0), + isInitialized: false, + poolIndex: i, + bump: addressesAndBumps[i][1], + }; + } + + return { + mint, + tokenPoolPda: parsedInfo.address, + tokenProgram, + activity: undefined, + balance: bn(parsedInfo.amount.toString()), + isInitialized: true, + poolIndex: i, + bump: addressesAndBumps[i][1], + }; + }); +} + +export type TokenPoolActivity = { + signature: string; + amount: BN; + action: Action; +}; + +/** + * Token pool pda info. + */ +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; + /** + * The index of the token pool + */ + poolIndex: number; + /** + * The bump used to derive the token pool pda + */ + bump: number; +}; + +/** + * @internal + */ +export enum Action { + Compress = 1, + Decompress = 2, + Transfer = 3, +} + +/** + * @internal + */ +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; +}; + +/** + * For `compress` and `mintTo` instructions only. + * Select a random token pool info from the token pool infos. + * + * For `decompress`, use {@link selectTokenPoolInfosForDecompression} instead. + * + * @param infos The token pool infos + * + * @returns A random token pool info + */ +export function selectTokenPoolInfo(infos: TokenPoolInfo[]): TokenPoolInfo { + const shuffledInfos = shuffleArray(infos); + + // filter only infos that are initialized + const filteredInfos = shuffledInfos.filter(info => info.isInitialized); + + if (filteredInfos.length === 0) { + throw new Error( + 'Please pass at least one initialized token pool info.', + ); + } + + // Return a single random token pool info + return filteredInfos[0]; +} + +/** + * Select one or multiple token pool infos from the token pool infos. + * + * Use this function for `decompress`. + * + * For `compress`, `mintTo` use {@link selectTokenPoolInfo} instead. + * + * @param infos The token pool infos + * @param decompressAmount The amount of tokens to withdraw + * + * @returns Array with one or more token pool infos. + */ +export function selectTokenPoolInfosForDecompression( + infos: TokenPoolInfo[], + decompressAmount: number | BN, +): TokenPoolInfo[] { + if (infos.length === 0) { + throw new Error('Please pass at least one token pool info.'); + } + + infos = shuffleArray(infos); + // Find the first info where balance is 10x the requested amount + const sufficientBalanceInfo = infos.find(info => + info.balance.gte(bn(decompressAmount).mul(bn(10))), + ); + + // filter only infos that are initialized + infos = infos + .filter(info => info.isInitialized) + .sort((a, b) => a.poolIndex - b.poolIndex); + + const allBalancesZero = infos.every(info => info.balance.isZero()); + if (allBalancesZero) { + throw new Error( + 'All provided token pool balances are zero. Please pass recent token pool infos.', + ); + } + + // If none found, return all infos + return sufficientBalanceInfo ? [sufficientBalanceInfo] : infos; +} diff --git a/js/compressed-token/src/utils/index.ts b/js/compressed-token/src/utils/index.ts index f135001c36..7e280dc27e 100644 --- a/js/compressed-token/src/utils/index.ts +++ b/js/compressed-token/src/utils/index.ts @@ -1 +1,4 @@ +export * from './get-token-pool-infos'; export * from './select-input-accounts'; +export * from './pack-compressed-token-accounts'; +export * from './validation'; diff --git a/js/compressed-token/src/instructions/pack-compressed-token-accounts.ts b/js/compressed-token/src/utils/pack-compressed-token-accounts.ts similarity index 72% rename from js/compressed-token/src/instructions/pack-compressed-token-accounts.ts rename to js/compressed-token/src/utils/pack-compressed-token-accounts.ts index a29d6e512e..99719fec1b 100644 --- a/js/compressed-token/src/instructions/pack-compressed-token-accounts.ts +++ b/js/compressed-token/src/utils/pack-compressed-token-accounts.ts @@ -4,6 +4,9 @@ import { getIndexOrAdd, bn, padOutputStateMerkleTrees, + TreeType, + featureFlags, + TreeInfo, } from '@lightprotocol/stateless.js'; import { PublicKey, AccountMeta } from '@solana/web3.js'; import { @@ -19,7 +22,7 @@ export type PackCompressedTokenAccountsParams = { * state tree of the input state. Gets padded to the length of * outputCompressedAccounts. */ - outputStateTrees?: PublicKey[] | PublicKey; + outputStateTreeInfo?: TreeInfo; /** Optional remaining accounts to append to */ remainingAccounts?: PublicKey[]; /** @@ -42,7 +45,7 @@ export function packCompressedTokenAccounts( } { const { inputCompressedTokenAccounts, - outputStateTrees, + outputStateTreeInfo, remainingAccounts = [], rootIndices, tokenTransferOutputs, @@ -60,20 +63,19 @@ 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( (account: ParsedTokenAccount, index) => { const merkleTreePubkeyIndex = getIndexOrAdd( _remainingAccounts, - account.compressedAccount.merkleTree, + account.compressedAccount.treeInfo.tree, ); - const nullifierQueuePubkeyIndex = getIndexOrAdd( + const queuePubkeyIndex = getIndexOrAdd( _remainingAccounts, - account.compressedAccount.nullifierQueue, + account.compressedAccount.treeInfo.queue, ); packedInputTokenData.push({ @@ -81,9 +83,9 @@ export function packCompressedTokenAccounts( delegateIndex, merkleContext: { merkleTreePubkeyIndex, - nullifierQueuePubkeyIndex, + queuePubkeyIndex, leafIndex: account.compressedAccount.leafIndex, - queueIndex: null, + proveByIndex: account.compressedAccount.proveByIndex, }, rootIndex: rootIndices[index], lamports: account.compressedAccount.lamports.eq(bn(0)) @@ -94,11 +96,38 @@ export function packCompressedTokenAccounts( }, ); - /// pack output state trees + if (inputCompressedTokenAccounts.length > 0 && outputStateTreeInfo) { + throw new Error( + 'Cannot specify both input accounts and outputStateTreeInfo', + ); + } + + let treeInfo: TreeInfo; + if (inputCompressedTokenAccounts.length > 0) { + treeInfo = inputCompressedTokenAccounts[0].compressedAccount.treeInfo; + } else if (outputStateTreeInfo) { + treeInfo = outputStateTreeInfo; + } else { + throw new Error( + 'Neither input accounts nor outputStateTreeInfo are available', + ); + } + + // Use next tree if available, otherwise fall back to current tree. + // `nextTreeInfo` always takes precedence. + const activeTreeInfo = treeInfo.nextTreeInfo || treeInfo; + let activeTreeOrQueue = activeTreeInfo.tree; + + if (activeTreeInfo.treeType === TreeType.StateV2) { + if (featureFlags.isV2()) { + activeTreeOrQueue = activeTreeInfo.queue; + } else throw new Error('V2 trees are not supported yet'); + } + + // Pack output state trees const paddedOutputStateMerkleTrees = padOutputStateMerkleTrees( - outputStateTrees, + activeTreeOrQueue, tokenTransferOutputs.length, - inputCompressedTokenAccounts.map(acc => acc.compressedAccount), ); const packedOutputTokenData: PackedTokenTransferOutputData[] = []; paddedOutputStateMerkleTrees.forEach((account, index) => { diff --git a/js/compressed-token/src/utils/select-input-accounts.ts b/js/compressed-token/src/utils/select-input-accounts.ts index e64ac880d9..56dd68c156 100644 --- a/js/compressed-token/src/utils/select-input-accounts.ts +++ b/js/compressed-token/src/utils/select-input-accounts.ts @@ -6,37 +6,98 @@ export const ERROR_NO_ACCOUNTS_FOUND = 'Could not find accounts to select for transfer.'; /** - * Selects the minimum number of compressed token accounts required for a transfer, up to a specified maximum. + * Selects token accounts for approval, first trying to find an exact match, then falling back to minimum selection. * * @param {ParsedTokenAccount[]} accounts - Token accounts to choose from. - * @param {BN} transferAmount - Amount to transfer. - * @param {number} [maxInputs=4] - Max accounts to select. Default is 4. + * @param {BN} approveAmount - Amount to approve. + * @param {number} [maxInputs=4] - Max accounts to select when falling back to minimum selection. * @returns {[ * selectedAccounts: ParsedTokenAccount[], * total: BN, * totalLamports: BN | null, * maxPossibleAmount: BN * ]} - Returns: - * - selectedAccounts: Accounts chosen for transfer. + * - selectedAccounts: Accounts chosen for approval. * - total: Total amount from selected accounts. * - totalLamports: Total lamports from selected accounts. - * - maxPossibleAmount: Max transferable amount given maxInputs. + * - maxPossibleAmount: Max approvable amount given maxInputs. + */ +export function selectTokenAccountsForApprove( + accounts: ParsedTokenAccount[], + approveAmount: BN, + maxInputs: number = 4, +): [ + selectedAccounts: ParsedTokenAccount[], + total: BN, + totalLamports: BN | null, + maxPossibleAmount: BN, +] { + // First try to find an exact match + const exactMatch = accounts.find(account => + account.parsed.amount.eq(approveAmount), + ); + if (exactMatch) { + return [ + [exactMatch], + exactMatch.parsed.amount, + exactMatch.compressedAccount.lamports, + exactMatch.parsed.amount, + ]; + } + + // If no exact match, fall back to minimum selection + return selectMinCompressedTokenAccountsForTransfer( + accounts, + approveAmount, + maxInputs, + ); +} + +/** + * Selects the minimum number of compressed token accounts required for a + * decompress instruction, up to a specified maximum. * - * @example - * const accounts = [ - * { parsed: { amount: new BN(100) }, compressedAccount: { lamports: new BN(10) } }, - * { parsed: { amount: new BN(50) }, compressedAccount: { lamports: new BN(5) } }, - * { parsed: { amount: new BN(25) }, compressedAccount: { lamports: new BN(2) } }, - * ]; - * const transferAmount = new BN(75); - * const maxInputs = 2; + * @param {ParsedTokenAccount[]} accounts Token accounts to choose from. + * @param {BN} amount Amount to decompress. + * @param {number} [maxInputs=4] Max accounts to select. Default + * is 4. * - * const [selectedAccounts, total, totalLamports, maxPossibleAmount] = - * selectMinCompressedTokenAccountsForTransfer(accounts, transferAmount, maxInputs); + * @returns Returns selected accounts and their totals. + */ +export function selectMinCompressedTokenAccountsForDecompression( + accounts: ParsedTokenAccount[], + amount: BN, + maxInputs: number = 4, +): { + selectedAccounts: ParsedTokenAccount[]; + total: BN; + totalLamports: BN | null; + maxPossibleAmount: BN; +} { + const [selectedAccounts, total, totalLamports, maxPossibleAmount] = + selectMinCompressedTokenAccountsForTransfer( + accounts, + amount, + maxInputs, + ); + return { selectedAccounts, total, totalLamports, maxPossibleAmount }; +} + +/** + * Selects the minimum number of compressed token accounts required for a + * transfer or decompression instruction, up to a specified maximum. * - * console.log(selectedAccounts.length); // 2 - * console.log(total.toString()); // '150' - * console.log(totalLamports!.toString()); // '15' + * @param {ParsedTokenAccount[]} accounts Token accounts to choose from. + * @param {BN} transferAmount Amount to transfer or decompress. + * @param {number} [maxInputs=4] Max accounts to select. Default + * is 4. + * + * @returns Returns selected accounts and their totals. [ + * selectedAccounts: ParsedTokenAccount[], + * total: BN, + * totalLamports: BN | null, + * maxPossibleAmount: BN + * ] */ export function selectMinCompressedTokenAccountsForTransfer( accounts: ParsedTokenAccount[], diff --git a/js/compressed-token/src/utils/validation.ts b/js/compressed-token/src/utils/validation.ts new file mode 100644 index 0000000000..6d915ba04c --- /dev/null +++ b/js/compressed-token/src/utils/validation.ts @@ -0,0 +1,24 @@ +import { ParsedTokenAccount } from '@lightprotocol/stateless.js'; +import { PublicKey } from '@solana/web3.js'; + +/** + * Check if all input accounts belong to the same mint. + * + * @param compressedTokenAccounts The compressed token accounts + * @param mint The mint of the token pool + * @returns True if all input accounts belong to the same mint + */ +export function checkMint( + compressedTokenAccounts: ParsedTokenAccount[], + mint: PublicKey, +): boolean { + if ( + !compressedTokenAccounts.every(account => + account.parsed.mint.equals(mint), + ) + ) { + throw new Error(`All input accounts must belong to the same mint`); + } + + return true; +} 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..fe52f5d701 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, + TreeInfo, + 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: TreeInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); @@ -79,6 +88,8 @@ describe('approveAndMintTo', () => { /// Register mint await createTokenPool(rpc, payer, mint); + tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); + stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); }); it('should mintTo compressed account with external spl mint', async () => { @@ -91,7 +102,8 @@ describe('approveAndMintTo', () => { bob, mintAuthority, 1000000000, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); await assertApproveAndMintTo(rpc, mint, bn(1000000000), bob); @@ -118,6 +130,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 +141,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..34ac9a55ce 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, + TreeInfo, + 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: TreeInfo; + let tokenPoolInfo: TokenPoolInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); @@ -48,6 +57,9 @@ describe('compressSplTokenAccount', () => { ) ).mint; + stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); + tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); + alice = await newAccountWithLamports(rpc, 1e9); aliceAta = await createAssociatedTokenAccount( rpc, @@ -64,7 +76,8 @@ describe('compressSplTokenAccount', () => { alice.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); await decompress(rpc, payer, mint, bn(1000), alice, aliceAta); @@ -86,7 +99,9 @@ describe('compressSplTokenAccount', () => { mint, alice, aliceAta, - defaultTestStateTreeAccounts().merkleTree, + undefined, + stateTreeInfo, + tokenPoolInfo, ); // Get final balances @@ -138,8 +153,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 +180,9 @@ describe('compressSplTokenAccount', () => { mint, alice, aliceAta, - defaultTestStateTreeAccounts().merkleTree, remainingAmount, + stateTreeInfo, + tokenPoolInfo, ); // Get final balances @@ -224,8 +241,9 @@ describe('compressSplTokenAccount', () => { mint, alice, aliceAta, - defaultTestStateTreeAccounts().merkleTree, bn(balanceBefore.value.amount), + stateTreeInfo, + tokenPoolInfo, ); const balanceAfter = await rpc.getTokenAccountBalance(aliceAta); @@ -238,7 +256,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 +282,19 @@ 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.getStateTreeInfos(), + ); + invalidTreeInfo.tree = Keypair.generate().publicKey; + invalidTreeInfo.queue = Keypair.generate().publicKey; // Mint some tokens to ensure non-zero balance await mintToChecked( @@ -288,7 +314,9 @@ describe('compressSplTokenAccount', () => { mint, alice, aliceAta, - invalidTree, + undefined, + invalidTreeInfo, + tokenPoolInfo, ), ).rejects.toThrow(); }); @@ -307,6 +335,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 +363,7 @@ describe('compressSplTokenAccount', () => { alice.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await decompress(rpc, payer, mint, bn(1000), alice, aliceAta); @@ -350,7 +382,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..329f845063 100644 --- a/js/compressed-token/tests/e2e/compress.test.ts +++ b/js/compressed-token/tests/e2e/compress.test.ts @@ -10,12 +10,13 @@ import { ParsedTokenAccount, Rpc, bn, - defaultTestStateTreeAccounts, newAccountWithLamports, dedupeSigner, buildAndSignTx, sendAndConfirmTx, getTestRpc, + TreeInfo, + selectStateTreeInfo, } from '@lightprotocol/stateless.js'; import { compress, @@ -30,6 +31,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 +98,14 @@ describe('compress', () => { let mint: PublicKey; let mintAuthority: Keypair; let lut: PublicKey; + let stateTreeInfo: TreeInfo; + let tokenPoolInfo: TokenPoolInfo; - const { merkleTree } = defaultTestStateTreeAccounts(); + const maxBatchSize = 15; + const recipients = Array.from( + { length: maxBatchSize }, + () => Keypair.generate().publicKey, + ); beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); @@ -113,6 +125,9 @@ describe('compress', () => { ) ).mint; + stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); + tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); + bob = await newAccountWithLamports(rpc, 1e9); charlie = await newAccountWithLamports(rpc, 1e9); @@ -130,16 +145,25 @@ describe('compress', () => { bob.publicKey, mintAuthority, bn(10000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); - await decompress(rpc, payer, mint, bn(9000), bob, bobAta); + await decompress(rpc, payer, mint, bn(900), bob, bobAta); /// Setup LUT. const { address } = await createTokenProgramLookupTable( rpc, payer, payer, + [mint], + [ + payer.publicKey, + bob.publicKey, + bobAta, + stateTreeInfo.tree, + stateTreeInfo.queue, + ], ); lut = address; }, 80_000); @@ -159,7 +183,8 @@ describe('compress', () => { bob, bobAta, charlie.publicKey, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); await assertCompress( rpc, @@ -172,11 +197,6 @@ describe('compress', () => { ); }); - const maxBatchSize = 15; - const recipients = Array.from( - { length: maxBatchSize }, - () => Keypair.generate().publicKey, - ); const amounts = Array.from({ length: maxBatchSize }, (_, i) => bn(i + 1)); it('should compress to multiple (11 max without LUT) recipients with array of amounts and addresses', async () => { @@ -188,6 +208,7 @@ describe('compress', () => { ), ); + // compress to 11 recipients await compress( rpc, payer, @@ -196,7 +217,8 @@ describe('compress', () => { bob, bobAta, recipients.slice(0, 11), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); for (let i = 0; i < recipients.length; i++) { @@ -232,7 +254,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,10 +270,11 @@ describe('compress', () => { bob, bobAta, recipients, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ), ).rejects.toThrow( - 'Both amount and toAddress must be arrays or both must be single values', + 'Amount and toAddress arrays must have the same length', ); }); @@ -259,15 +283,16 @@ describe('compress', () => { const lookupTableAccount = (await rpc.getAddressLookupTable(lut)) .value!; - /// Mint to max recipients with LUT + /// Compress to max recipients with LUT const ix = await CompressedTokenProgram.compress({ - payer: payer.publicKey, + payer: bob.publicKey, owner: bob.publicKey, source: bobAta, toAddress: recipients, - amount: amounts, + amount: recipients.map(() => bn(2)), mint, - outputStateTree: defaultTestStateTreeAccounts().merkleTree, + outputStateTreeInfo: stateTreeInfo, + tokenPoolInfo, }); const { blockhash } = await rpc.getLatestBlockhash(); @@ -280,9 +305,7 @@ describe('compress', () => { additionalSigners, [lookupTableAccount], ); - const txId = await sendAndConfirmTx(rpc, tx); - - return txId; + await sendAndConfirmTx(rpc, tx); }); it('should compress from bob Token 2022 Ata -> charlie', async () => { @@ -317,6 +340,23 @@ describe('compress', () => { TOKEN_2022_PROGRAM_ID, ); + const tokenPoolInfoT22 = selectTokenPoolInfo( + await getTokenPoolInfos(rpc, token22Mint), + ); + + await expect( + mintTo( + rpc, + payer, + token22Mint, + bob.publicKey, + mintAuthority, + bn(10000), + stateTreeInfo, + tokenPoolInfo, + ), + ).rejects.toThrow(); + await mintTo( rpc, payer, @@ -324,9 +364,9 @@ describe('compress', () => { bob.publicKey, mintAuthority, bn(10000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfoT22, ); - await decompress( rpc, payer, @@ -350,7 +390,8 @@ describe('compress', () => { bob, bobToken2022Ata, charlie.publicKey, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfoT22, ); await assertCompress( rpc, diff --git a/js/compressed-token/tests/e2e/create-token-pool.test.ts b/js/compressed-token/tests/e2e/create-token-pool.test.ts index 1e285a3b66..a0ff075550 100644 --- a/js/compressed-token/tests/e2e/create-token-pool.test.ts +++ b/js/compressed-token/tests/e2e/create-token-pool.test.ts @@ -8,7 +8,7 @@ import { TOKEN_PROGRAM_ID, createInitializeMint2Instruction, } from '@solana/spl-token'; -import { createMint, createTokenPool } from '../../src/actions'; +import { addTokenPools, createMint, createTokenPool } from '../../src/actions'; import { Rpc, buildAndSignTx, @@ -19,6 +19,7 @@ import { } from '@lightprotocol/stateless.js'; import { WasmFactory } from '@lightprotocol/hasher.rs'; import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token'; +import { getTokenPoolInfos } from '../../src/utils'; /** * Assert that createTokenPool() creates system-pool account for external mint, @@ -30,9 +31,10 @@ async function assertRegisterMint( rpc: Rpc, decimals: number, poolAccount: PublicKey, + tokenProgramId: PublicKey = TOKEN_PROGRAM_ID, ) { const mintAcc = await rpc.getAccountInfo(mint); - const unpackedMint = unpackMint(mint, mintAcc); + const unpackedMint = unpackMint(mint, mintAcc, tokenProgramId); expect(unpackedMint.mintAuthority?.toString()).toBe(authority.toString()); expect(unpackedMint.supply).toBe(0n); @@ -43,7 +45,11 @@ async function assertRegisterMint( /// Pool (omnibus) account is a regular SPL Token account const poolAccountInfo = await rpc.getAccountInfo(poolAccount); - const unpackedPoolAccount = unpackAccount(poolAccount, poolAccountInfo); + const unpackedPoolAccount = unpackAccount( + poolAccount, + poolAccountInfo, + tokenProgramId, + ); expect(unpackedPoolAccount.mint.equals(mint)).toBe(true); expect(unpackedPoolAccount.amount).toBe(0n); expect( @@ -68,7 +74,7 @@ async function createTestSplMint( fromPubkey: payer.publicKey, lamports: rentExemptBalance, newAccountPubkey: mintKeypair.publicKey, - programId: TOKEN_PROGRAM_ID, + programId: isToken22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID, space: MINT_SIZE, }); const initializeMintInstruction = createInitializeMint2Instruction( @@ -149,6 +155,7 @@ describe('createTokenPool', () => { payer, token22MintKeypair, token22MintAuthority, + true, ); const poolAccount = @@ -165,11 +172,17 @@ describe('createTokenPool', () => { TEST_TOKEN_DECIMALS, token22MintKeypair, undefined, - true, + TOKEN_2022_PROGRAM_ID, ), ).rejects.toThrow(); - await createTokenPool(rpc, payer, token22Mint); + await createTokenPool( + rpc, + payer, + token22Mint, + undefined, + TOKEN_2022_PROGRAM_ID, + ); await assertRegisterMint( token22Mint, @@ -177,11 +190,18 @@ describe('createTokenPool', () => { rpc, TEST_TOKEN_DECIMALS, poolAccount, + TOKEN_2022_PROGRAM_ID, ); /// Mint already registered await expect( - createTokenPool(rpc, payer, token22Mint), + createTokenPool( + rpc, + payer, + token22Mint, + undefined, + TOKEN_2022_PROGRAM_ID, + ), ).rejects.toThrow(); }); @@ -201,4 +221,193 @@ describe('createTokenPool', () => { poolAccount, ); }); + + it('should allow adding multiple token pools for the same mint (idempotent)', async () => { + // Create a new mint + const newMintKeypair = Keypair.generate(); + const newMint = newMintKeypair.publicKey; + const newMintAuthority = Keypair.generate(); + + // Create external SPL mint + await createTestSplMint(rpc, payer, newMintKeypair, newMintAuthority); + + // First call to createTokenPool + await createTokenPool(rpc, payer, newMint, undefined); + + // Verify first pool creation + const poolAccount = CompressedTokenProgram.deriveTokenPoolPda(newMint); + await assertRegisterMint( + newMint, + newMintAuthority.publicKey, + rpc, + TEST_TOKEN_DECIMALS, + poolAccount, + ); + + // Add multiple token pools + await addTokenPools(rpc, payer, newMint, 1); + await addTokenPools(rpc, payer, newMint, 1); + await addTokenPools(rpc, payer, newMint, 3); + // Verify all pools are created correctly + const [poolAccount2] = + CompressedTokenProgram.deriveTokenPoolPdaWithIndex(newMint, 1); + const [poolAccount3] = + CompressedTokenProgram.deriveTokenPoolPdaWithIndex(newMint, 2); + const [poolAccount4] = + CompressedTokenProgram.deriveTokenPoolPdaWithIndex(newMint, 3); + const [poolAccount5] = + CompressedTokenProgram.deriveTokenPoolPdaWithIndex(newMint, 4); + + await assertRegisterMint( + newMint, + newMintAuthority.publicKey, + rpc, + TEST_TOKEN_DECIMALS, + poolAccount2, + ); + await assertRegisterMint( + newMint, + newMintAuthority.publicKey, + rpc, + TEST_TOKEN_DECIMALS, + poolAccount3, + ); + await assertRegisterMint( + newMint, + newMintAuthority.publicKey, + rpc, + TEST_TOKEN_DECIMALS, + poolAccount4, + ); + + // Verify pool 5 doesn't exist + const poolAccount5Info = await rpc.getAccountInfo(poolAccount5); + expect(poolAccount5Info).toBeNull(); + + // Verify pool infos + const infos = await getTokenPoolInfos(rpc, newMint); + + expect(infos.length).toBe(5); + for (let i = 0; i < infos.length; i++) { + expect(infos[i].poolIndex).toBe(i); + if (i === 4) { + expect(infos[i].isInitialized).toBe(false); + } else { + expect(infos[i].isInitialized).toBe(true); + } + } + }); + + it('should allow adding multiple token pools for token22 mint', async () => { + // Create a new token22 mint + const newMintKeypair = Keypair.generate(); + const newMint = newMintKeypair.publicKey; + const newMintAuthority = Keypair.generate(); + + // Create external SPL Token 2022 mint + await createTestSplMint( + rpc, + payer, + newMintKeypair, + newMintAuthority, + true, // isToken22 + ); + + // First call to createTokenPool + await createTokenPool( + rpc, + payer, + newMint, + undefined, + TOKEN_2022_PROGRAM_ID, + ); + + // Verify first pool creation + const poolAccount = CompressedTokenProgram.deriveTokenPoolPda(newMint); + await assertRegisterMint( + newMint, + newMintAuthority.publicKey, + rpc, + TEST_TOKEN_DECIMALS, + poolAccount, + TOKEN_2022_PROGRAM_ID, + ); + + // Add multiple token pools + await addTokenPools( + rpc, + payer, + newMint, + 1, + undefined, + TOKEN_2022_PROGRAM_ID, + ); + await addTokenPools( + rpc, + payer, + newMint, + 1, + undefined, + TOKEN_2022_PROGRAM_ID, + ); + await addTokenPools( + rpc, + payer, + newMint, + 3, + undefined, + TOKEN_2022_PROGRAM_ID, + ); + + // Verify all pools are created correctly + const [poolAccount2] = + CompressedTokenProgram.deriveTokenPoolPdaWithIndex(newMint, 1); + const [poolAccount3] = + CompressedTokenProgram.deriveTokenPoolPdaWithIndex(newMint, 2); + const [poolAccount4] = + CompressedTokenProgram.deriveTokenPoolPdaWithIndex(newMint, 3); + const [poolAccount5] = + CompressedTokenProgram.deriveTokenPoolPdaWithIndex(newMint, 4); + + await assertRegisterMint( + newMint, + newMintAuthority.publicKey, + rpc, + TEST_TOKEN_DECIMALS, + poolAccount2, + TOKEN_2022_PROGRAM_ID, + ); + await assertRegisterMint( + newMint, + newMintAuthority.publicKey, + rpc, + TEST_TOKEN_DECIMALS, + poolAccount3, + TOKEN_2022_PROGRAM_ID, + ); + await assertRegisterMint( + newMint, + newMintAuthority.publicKey, + rpc, + TEST_TOKEN_DECIMALS, + poolAccount4, + TOKEN_2022_PROGRAM_ID, + ); + + // Verify pool 5 doesn't exist + const poolAccount5Info = await rpc.getAccountInfo(poolAccount5); + expect(poolAccount5Info).toBeNull(); + + // Verify pool infos + const infos = await getTokenPoolInfos(rpc, newMint); + expect(infos.length).toBe(5); + for (let i = 0; i < infos.length; i++) { + expect(infos[i].poolIndex).toBe(i); + if (i === 4) { + expect(infos[i].isInitialized).toBe(false); + } else { + expect(infos[i].isInitialized).toBe(true); + } + } + }); }); diff --git a/js/compressed-token/tests/e2e/decompress-delegated.test.ts b/js/compressed-token/tests/e2e/decompress-delegated.test.ts new file mode 100644 index 0000000000..f1b62f65e2 --- /dev/null +++ b/js/compressed-token/tests/e2e/decompress-delegated.test.ts @@ -0,0 +1,208 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import { PublicKey, Keypair, Signer } from '@solana/web3.js'; +import BN from 'bn.js'; +import { + Rpc, + bn, + newAccountWithLamports, + getTestRpc, + TreeInfo, + selectStateTreeInfo, + ParsedTokenAccount, +} from '@lightprotocol/stateless.js'; +import { WasmFactory } from '@lightprotocol/hasher.rs'; +import { + createMint, + mintTo, + approve, + decompressDelegated, +} from '../../src/actions'; +import { createAssociatedTokenAccount } from '@solana/spl-token'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + selectTokenPoolInfosForDecompression, + TokenPoolInfo, +} from '../../src/utils/get-token-pool-infos'; + +interface BalanceInfo { + delegate: ParsedTokenAccount[]; + owner: ParsedTokenAccount[]; + recipient: { value: { amount: string } }; +} + +async function getBalances( + rpc: Rpc, + delegate: PublicKey, + owner: PublicKey, + recipient: PublicKey, + mint: PublicKey, +): Promise { + return { + delegate: ( + await rpc.getCompressedTokenAccountsByDelegate(delegate, { mint }) + ).items, + owner: (await rpc.getCompressedTokenAccountsByOwner(owner, { mint })) + .items, + recipient: await rpc.getTokenAccountBalance(recipient), + }; +} + +function calculateBalanceSum(accounts: ParsedTokenAccount[]): BN { + return accounts.reduce( + (acc, curr) => bn(acc).add(curr.parsed.amount), + bn(0), + ); +} + +async function assertDecompress( + rpc: Rpc, + initialBalances: BalanceInfo, + recipient: PublicKey, + mint: PublicKey, + amount: BN, + delegate: PublicKey, + owner: PublicKey, +) { + const finalBalances = await getBalances( + rpc, + delegate, + owner, + recipient, + mint, + ); + + // Check recipient balance + const expectedRecipientBalance = bn( + initialBalances.recipient.value.amount, + ).add(amount); + const actualRecipientBalance = bn(finalBalances.recipient.value.amount); + expect(actualRecipientBalance.toString()).toBe( + expectedRecipientBalance.toString(), + ); + + // Check delegate and owner balances + const initialDelegateSum = calculateBalanceSum(initialBalances.delegate); + const finalDelegateSum = calculateBalanceSum(finalBalances.delegate); + const finalOwnerSum = calculateBalanceSum(finalBalances.owner); + + expect(finalDelegateSum.add(finalOwnerSum).toString()).toBe( + initialDelegateSum.sub(amount).toString(), + ); +} + +const TEST_TOKEN_DECIMALS = 2; +const TEST_AMOUNT = bn(5); +const INITIAL_MINT_AMOUNT = bn(1000); + +describe('decompressDelegated', () => { + let rpc: Rpc; + let payer: Signer; + let bob: Signer; + let charlie: Signer; + let charlieAta: PublicKey; + let mint: PublicKey; + let mintAuthority: Keypair; + let stateTreeInfo: TreeInfo; + 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(); + + mint = ( + await createMint( + rpc, + payer, + mintAuthority.publicKey, + TEST_TOKEN_DECIMALS, + mintKeypair, + ) + ).mint; + + stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); + tokenPoolInfos = await getTokenPoolInfos(rpc, mint); + + charlieAta = await createAssociatedTokenAccount( + rpc, + payer, + mint, + charlie.publicKey, + ); + + const randInfo = selectTokenPoolInfo(tokenPoolInfos); + await mintTo( + rpc, + payer, + mint, + payer.publicKey, + mintAuthority, + INITIAL_MINT_AMOUNT, + stateTreeInfo, + randInfo, + ); + + await approve( + rpc, + payer, + mint, + INITIAL_MINT_AMOUNT.toNumber(), + payer, + bob.publicKey, + ); + }); + + it('should decompress from bob -> charlieAta and leave no delegated remainder', async () => { + const initialBalances = await getBalances( + rpc, + bob.publicKey, + payer.publicKey, + charlieAta, + mint, + ); + tokenPoolInfos = await getTokenPoolInfos(rpc, mint); + + await decompressDelegated( + rpc, + bob, + mint, + TEST_AMOUNT, + bob, + charlieAta, + selectTokenPoolInfosForDecompression(tokenPoolInfos, TEST_AMOUNT), + ); + + await assertDecompress( + rpc, + initialBalances, + charlieAta, + mint, + TEST_AMOUNT, + bob.publicKey, + payer.publicKey, + ); + tokenPoolInfos = await getTokenPoolInfos(rpc, mint); + + await expect( + decompressDelegated( + rpc, + bob, + mint, + TEST_AMOUNT, + bob, + charlieAta, + selectTokenPoolInfosForDecompression( + tokenPoolInfos, + TEST_AMOUNT, + ), + ), + ).rejects.toThrowError( + 'Could not find accounts to select for transfer.', + ); + }); +}); diff --git a/js/compressed-token/tests/e2e/decompress.test.ts b/js/compressed-token/tests/e2e/decompress.test.ts index aaa6b72ef9..b3ec1400ca 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, + selectStateTreeInfo, + TreeInfo, } 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, + selectTokenPoolInfosForDecompression, + 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: TreeInfo; + 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,8 @@ describe('decompress', () => { ) ).mint; - bob = await newAccountWithLamports(rpc, 1e9); - charlie = await newAccountWithLamports(rpc, 1e9); + stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); + tokenPoolInfos = await getTokenPoolInfos(rpc, mint); charlieAta = await createAssociatedTokenAccount( rpc, @@ -102,7 +112,8 @@ describe('decompress', () => { bob.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + selectTokenPoolInfo(tokenPoolInfos), ); }); @@ -118,6 +129,8 @@ describe('decompress', () => { mint, }); + tokenPoolInfos = await getTokenPoolInfos(rpc, mint); + await decompress( rpc, payer, @@ -125,7 +138,7 @@ describe('decompress', () => { bn(5), bob, charlieAta, - merkleTree, + selectTokenPoolInfosForDecompression(tokenPoolInfos, bn(5)), ); await assertDecompress( diff --git a/js/compressed-token/tests/e2e/delegate.test.ts b/js/compressed-token/tests/e2e/delegate.test.ts new file mode 100644 index 0000000000..e60b25515f --- /dev/null +++ b/js/compressed-token/tests/e2e/delegate.test.ts @@ -0,0 +1,455 @@ +import { describe, it, expect, beforeAll, beforeEach } from 'vitest'; +import { PublicKey, Keypair, Signer } from '@solana/web3.js'; +import BN from 'bn.js'; +import { + Rpc, + bn, + newAccountWithLamports, + getTestRpc, + TreeInfo, + selectStateTreeInfo, + ParsedTokenAccount, +} from '@lightprotocol/stateless.js'; +import { WasmFactory } from '@lightprotocol/hasher.rs'; +import { + createMint, + mintTo, + approve, + revoke, + transfer, + transferDelegated, +} from '../../src/actions'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + TokenPoolInfo, +} from '../../src/utils/get-token-pool-infos'; + +/** + * Verifies token delegation by checking pre and post account counts and balances. + */ +async function assertDelegate( + rpc: Rpc, + refMint: PublicKey, + refAmount: BN, + refOwner: PublicKey, + refDelegate: PublicKey, + preOwnerAccounts: ParsedTokenAccount[], + preDelegateAccounts: ParsedTokenAccount[], + newOwnerCount: number, + newDelegateCount: number, +) { + const ownerPostCompressedTokenAccounts = ( + await rpc.getCompressedTokenAccountsByOwner(refOwner, { + mint: refMint, + }) + ).items; + + const delegateCompressedTokenAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(refDelegate, { + mint: refMint, + }) + ).items; + + expect(ownerPostCompressedTokenAccounts.length).toBe(newOwnerCount); + expect(delegateCompressedTokenAccounts.length).toBe(newDelegateCount); + + // Calculate pre and post balances + const preOwnerBalance = preOwnerAccounts.reduce( + (sum, acc) => sum.add(acc.parsed.amount), + bn(0), + ); + const postOwnerBalance = ownerPostCompressedTokenAccounts.reduce( + (sum, acc) => sum.add(acc.parsed.amount), + bn(0), + ); + const preDelegateBalance = preDelegateAccounts.reduce( + (sum, acc) => sum.add(acc.parsed.amount), + bn(0), + ); + const postDelegateBalance = delegateCompressedTokenAccounts.reduce( + (sum, acc) => sum.add(acc.parsed.amount), + bn(0), + ); + + // Checks + const ownerBalanceCheck = preOwnerBalance.eq(postOwnerBalance); + if (!ownerBalanceCheck) { + console.log('Owner balance check failed:'); + console.log('preOwnerBalance:', preOwnerBalance.toString()); + console.log('refAmount:', refAmount.toString()); + console.log('postOwnerBalance:', postOwnerBalance.toString()); + } + expect(ownerBalanceCheck).toBe(true); + + const delegateBalanceCheck = preDelegateBalance + .add(refAmount) + .eq(postDelegateBalance); + if (!delegateBalanceCheck) { + console.log('Delegate balance check failed:'); + console.log('preDelegateBalance:', preDelegateBalance.toString()); + console.log('refAmount:', refAmount.toString()); + console.log('postDelegateBalance:', postDelegateBalance.toString()); + } + expect(delegateBalanceCheck).toBe(true); + + // Check that delegate is set correctly + expect( + delegateCompressedTokenAccounts.every(acc => + acc.parsed.delegate?.equals(refDelegate), + ), + ).toBe(true); +} + +const TEST_TOKEN_DECIMALS = 2; + +describe('delegate', () => { + let rpc: Rpc; + let payer: Signer; + let bob: Signer; + let mint: PublicKey; + let mintAuthority: Keypair; + let stateTreeInfo: TreeInfo; + let tokenPoolInfo: TokenPoolInfo; + + beforeAll(async () => { + const lightWasm = await WasmFactory.getInstance(); + rpc = await getTestRpc(lightWasm); + payer = await newAccountWithLamports(rpc, 1e9); + mintAuthority = Keypair.generate(); + const mintKeypair = Keypair.generate(); + + stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); + + mint = ( + await createMint( + rpc, + payer, + mintAuthority.publicKey, + TEST_TOKEN_DECIMALS, + mintKeypair, + ) + ).mint; + + tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); + }); + + beforeEach(async () => { + bob = await newAccountWithLamports(rpc, 1e9); + + await mintTo( + rpc, + payer, + mint, + payer.publicKey, + mintAuthority, + bn(500), + stateTreeInfo, + tokenPoolInfo, + ); + await mintTo( + rpc, + payer, + mint, + payer.publicKey, + mintAuthority, + bn(500), + stateTreeInfo, + tokenPoolInfo, + ); + }); + + it('should approve and revoke all tokens', async () => { + const payerPreCompressedTokenAccounts = ( + await rpc.getCompressedTokenAccountsByOwner(payer.publicKey, { + mint, + }) + ).items; + + const totalAmount = payerPreCompressedTokenAccounts.reduce( + (sum, acc) => sum.add(acc.parsed.amount), + bn(0), + ); + + const preDelegateAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(bob.publicKey, { + mint, + }) + ).items; + const txId = await approve( + rpc, + payer, + mint, + totalAmount, + payer, + bob.publicKey, + ); + console.log('txid approve ', txId); + await assertDelegate( + rpc, + mint, + totalAmount, + payer.publicKey, + bob.publicKey, + payerPreCompressedTokenAccounts, + preDelegateAccounts, + 1, + 1, + ); + + const delegatedAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(bob.publicKey, { + mint, + }) + ).items; + + await revoke(rpc, payer, delegatedAccounts, payer); + + const payerPostCompressedTokenAccounts = ( + await rpc.getCompressedTokenAccountsByOwner(payer.publicKey, { + mint, + }) + ).items; + + const postAmount = payerPostCompressedTokenAccounts.reduce( + (sum, acc) => sum.add(acc.parsed.amount), + bn(0), + ); + expect(postAmount.eq(totalAmount)).toBe(true); + + const bobPostCompressedTokenAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(bob.publicKey, { + mint, + }) + ).items; + expect(bobPostCompressedTokenAccounts.length).toBe(0); + }); + + it('should approve and revoke partial amount', async () => { + const payerPreCompressedTokenAccounts = ( + await rpc.getCompressedTokenAccountsByOwner(payer.publicKey, { + mint, + }) + ).items; + + const totalAmount = payerPreCompressedTokenAccounts.reduce( + (sum, acc) => sum.add(acc.parsed.amount), + bn(0), + ); + + const preDelegateAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(bob.publicKey, { + mint, + }) + ).items; + + const delegateAmount = bn(700); + await approve(rpc, payer, mint, delegateAmount, payer, bob.publicKey); + + await assertDelegate( + rpc, + mint, + delegateAmount, + payer.publicKey, + bob.publicKey, + payerPreCompressedTokenAccounts, + preDelegateAccounts, + payerPreCompressedTokenAccounts.length + 1, + 1, + ); + + const delegatedAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(bob.publicKey, { + mint, + }) + ).items; + + await revoke(rpc, payer, delegatedAccounts, payer); + + const payerPostCompressedTokenAccounts = ( + await rpc.getCompressedTokenAccountsByOwner(payer.publicKey, { + mint, + }) + ).items; + + const bobPostCompressedTokenAccountsDelegate = ( + await rpc.getCompressedTokenAccountsByDelegate(bob.publicKey, { + mint, + }) + ).items; + + expect(bobPostCompressedTokenAccountsDelegate.length).toBe(0); + + expect(payerPostCompressedTokenAccounts.length).toBe( + payerPreCompressedTokenAccounts.length + 1, + ); + + const postAmount = payerPostCompressedTokenAccounts.reduce( + (sum, acc) => sum.add(acc.parsed.amount), + bn(0), + ); + expect(postAmount.eq(totalAmount)).toBe(true); + }); + + it('should approve and revoke single token account', async () => { + const payerPreCompressedTokenAccounts = ( + await rpc.getCompressedTokenAccountsByOwner(payer.publicKey, { + mint, + }) + ).items; + + const firstAccountAmount = + payerPreCompressedTokenAccounts[0].parsed.amount; + + const preDelegateAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(bob.publicKey, { + mint, + }) + ).items; + + await approve( + rpc, + payer, + mint, + firstAccountAmount, + payer, + bob.publicKey, + ); + + await assertDelegate( + rpc, + mint, + firstAccountAmount, + payer.publicKey, + bob.publicKey, + payerPreCompressedTokenAccounts, + preDelegateAccounts, + payerPreCompressedTokenAccounts.length, + 1, + ); + + const delegatedAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(bob.publicKey, { + mint, + }) + ).items; + + await revoke(rpc, payer, delegatedAccounts, payer); + + const payerPostCompressedTokenAccounts = ( + await rpc.getCompressedTokenAccountsByOwner(payer.publicKey, { + mint, + }) + ).items; + + const bobPostCompressedTokenAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(bob.publicKey, { + mint, + }) + ).items; + + expect(bobPostCompressedTokenAccounts.length).toBe(0); + + const postAmount = payerPostCompressedTokenAccounts.reduce( + (sum, acc) => sum.add(acc.parsed.amount), + bn(0), + ); + const totalAmount = payerPreCompressedTokenAccounts.reduce( + (sum, acc) => sum.add(acc.parsed.amount), + bn(0), + ); + expect(postAmount.eq(totalAmount)).toBe(true); + }); + + it('should approve and revoke when payer is not owner', async () => { + const owner = await newAccountWithLamports(rpc, 1e9); + await mintTo( + rpc, + payer, + mint, + owner.publicKey, + mintAuthority, + bn(500), + stateTreeInfo, + tokenPoolInfo, + ); + const charlie = await newAccountWithLamports(rpc, 1e9); + const payerPreCompressedTokenAccounts = ( + await rpc.getCompressedTokenAccountsByOwner(owner.publicKey, { + mint, + }) + ).items; + + const totalAmount = payerPreCompressedTokenAccounts.reduce( + (sum, acc) => sum.add(acc.parsed.amount), + bn(0), + ); + + const preDelegateAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(charlie.publicKey, { + mint, + }) + ).items; + + await approve(rpc, bob, mint, totalAmount, owner, charlie.publicKey); + + await assertDelegate( + rpc, + mint, + totalAmount, + owner.publicKey, + charlie.publicKey, + payerPreCompressedTokenAccounts, + preDelegateAccounts, + 1, + 1, + ); + + const delegatedAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(charlie.publicKey, { + mint, + }) + ).items; + + await revoke(rpc, bob, delegatedAccounts, owner); + + const ownerPostCompressedTokenAccounts = ( + await rpc.getCompressedTokenAccountsByOwner(owner.publicKey, { + mint, + }) + ).items; + + const postAmount = ownerPostCompressedTokenAccounts.reduce( + (sum, acc) => sum.add(acc.parsed.amount), + bn(0), + ); + expect(postAmount.eq(totalAmount)).toBe(true); + }); + + it('should fail when non-owner tries to approve or revoke', async () => { + const payerPreCompressedTokenAccounts = ( + await rpc.getCompressedTokenAccountsByOwner(payer.publicKey, { + mint, + }) + ).items; + + const totalAmount = payerPreCompressedTokenAccounts.reduce( + (sum, acc) => sum.add(acc.parsed.amount), + bn(0), + ); + + await expect( + approve(rpc, bob, mint, totalAmount, payer, bob.publicKey), + ).rejects.toThrowError(); + + const delegatedAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(bob.publicKey, { + mint, + }) + ).items; + + await expect( + revoke(rpc, bob, delegatedAccounts, payer), + ).rejects.toThrowError(); + }); +}); diff --git a/js/compressed-token/tests/e2e/layout.test.ts b/js/compressed-token/tests/e2e/layout.test.ts index f198509db2..db68165ffe 100644 --- a/js/compressed-token/tests/e2e/layout.test.ts +++ b/js/compressed-token/tests/e2e/layout.test.ts @@ -7,6 +7,15 @@ import { Wallet, } from '@coral-xyz/anchor'; import BN from 'bn.js'; +import { + bn, + InputTokenDataWithContext, + PackedMerkleContext, + ValidityProof, + COMPRESSED_TOKEN_PROGRAM_ID, + defaultStaticAccountsStruct, + LightSystemProgram, +} from '@lightprotocol/stateless.js'; import { encodeMintToInstructionData, decodeMintToInstructionData, @@ -20,15 +29,15 @@ import { createTokenPoolAccountsLayout, transferAccountsLayout, CompressedTokenProgram, + CompressedTokenInstructionDataTransfer, + PackedTokenTransferOutputData, + selectTokenPoolInfo, + selectTokenPoolInfosForDecompression, } from '../../src/'; import { Keypair } from '@solana/web3.js'; import { Connection } from '@solana/web3.js'; import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token'; import { SystemProgram } from '@solana/web3.js'; -import { - defaultStaticAccountsStruct, - LightSystemProgram, -} from '@lightprotocol/stateless.js'; const getTestProgram = (): Program => { const mockKeypair = Keypair.generate(); @@ -41,24 +50,31 @@ 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) { - if (typeof ref !== typeof val) { - console.log(`Type mismatch: ${typeof ref} !== ${typeof val}`); - return false; - } + if (ref === null && val === null) return true; + if (ref === null || val === null) return false; - if (ref instanceof BN && val instanceof BN) { - return ref.eq(val); + if (ref instanceof BN || val instanceof BN) { + if (!(ref instanceof BN) || !(val instanceof BN)) { + val = bn(val); + const result = ref.toString().trim() === val.toString().trim(); + if (!result) { + console.log( + `BN mismatch: ${ref.toString()} !== ${val.toString()}`, + ); + } + return result; + } + const result = ref.toString().trim() === val.toString().trim(); + if (!result) { + console.log(`BN mismatch: ${ref.toString()} !== ${val.toString()}`); + } + return result; } - if (typeof ref === 'object' && ref !== null && val !== null) { + if (typeof ref === 'object' && typeof val === 'object') { const refKeys = Object.keys(ref); const valKeys = Object.keys(val); @@ -135,51 +151,51 @@ describe('layout', () => { 185, 153, 246, 199, 206, 47, 210, 17, 10, 66, 68, 132, 229, 12, 67, 166, 168, 229, 156, 90, 30, ], - }, + } as ValidityProof, mint: new PublicKey( 'Bwuvv7NXd59zXRvWRCXcPLvwZ2dfedyQ9XZyqDghRFxv', ), delegatedTransfer: null, inputTokenDataWithContext: [ { - amount: new BN('03e8', 16), + amount: new BN(1000), delegateIndex: null, merkleContext: { merkleTreePubkeyIndex: 0, - nullifierQueuePubkeyIndex: 1, + queuePubkeyIndex: 1, leafIndex: 10, - queueIndex: null, - }, + proveByIndex: false, + } as PackedMerkleContext, rootIndex: 11, lamports: null, tlv: null, - }, + } as InputTokenDataWithContext, ], outputCompressedAccounts: [ { owner: new PublicKey( 'ARaDUvjovQDvFTMqaNAu9f2j1MpqJ5rhDAnDFrnyKbwg', ), - amount: new BN('012c', 16), + amount: new BN(300), lamports: null, merkleTreeIndex: 0, tlv: null, - }, + } as PackedTokenTransferOutputData, { owner: new PublicKey( 'GWYLPLzCCAVxq12UvBSpU4F8pcsmmRYQobPxkGz67ZVx', ), - amount: new BN('02bc', 16), + amount: new BN(700), lamports: null, merkleTreeIndex: 0, tlv: null, - }, + } as PackedTokenTransferOutputData, ], compressOrDecompressAmount: null, isCompress: false, cpiContext: null, lamportsChangeAccountMerkleTreeIndex: null, - }, + } as CompressedTokenInstructionDataTransfer, }, { description: 'with compressOrDecompressAmount', @@ -195,7 +211,7 @@ describe('layout', () => { isCompress: true, cpiContext: null, lamportsChangeAccountMerkleTreeIndex: null, - }, + } as CompressedTokenInstructionDataTransfer, }, { description: 'with delegatedTransfer', @@ -244,16 +260,16 @@ describe('layout', () => { delegatedTransfer: null, inputTokenDataWithContext: [ { - amount: new BN(1000), + amount: bn(1000), delegateIndex: 2, merkleContext: { merkleTreePubkeyIndex: 1, - nullifierQueuePubkeyIndex: 2, + queuePubkeyIndex: 2, leafIndex: 3, - queueIndex: { queueId: 0, index: 4 }, + proveByIndex: false, }, rootIndex: 5, - lamports: new BN(2000), + lamports: bn(2000), tlv: Buffer.from([1, 2, 3]), }, ], @@ -278,8 +294,8 @@ describe('layout', () => { owner: new PublicKey( 'ARaDUvjovQDvFTMqaNAu9f2j1MpqJ5rhDAnDFrnyKbwg', ), - amount: new BN(3000), - lamports: new BN(4000), + amount: bn(3000), + lamports: bn(4000), merkleTreeIndex: 1, tlv: Buffer.from([4, 5, 6]), }, @@ -350,8 +366,10 @@ describe('layout', () => { 'CompressedTokenInstructionDataTransfer', data, ); + const encoded = encodeTransferInstructionData(data); const decoded = decodeTransferInstructionData(encoded); + expect(deepEqual(decoded, data)).toBe(true); expect(anchorEncodedData).toEqual( encoded.slice(IX_DISCRIMINATOR + LENGTH_DISCRIMINATOR), @@ -370,7 +388,7 @@ describe('layout', () => { '6ASf5EcmmEHTgDJ4X4ZT5vT6iHVJBXPg5AN5YoTCpGWt', ), ], - amounts: [new BN(1000)], + amounts: [bn(1000)], lamports: null, }, }, @@ -385,7 +403,7 @@ describe('layout', () => { '8ASf5EcmmEHTgDJ4X4ZT5vT6iHVJBXPg5AN5YoTCpGWs', ), ], - amounts: [new BN(1000), new BN(2000)], + amounts: [bn(1000), bn(2000)], lamports: null, }, }, @@ -397,8 +415,8 @@ describe('layout', () => { '6ASf5EcmmEHTgDJ4X4ZT5vT6iHVJBXPg5AN5YoTCpGWt', ), ], - amounts: [new BN(1000)], - lamports: new BN(500), + amounts: [bn(1000)], + lamports: bn(500), }, }, ]; @@ -445,7 +463,7 @@ describe('layout', () => { owner: new PublicKey( 'CPMzHV9PsUeb5pFmyrj9nEoDwtL8CcyUKQzJXJxYRnT7', ), - remainingAmount: new BN(110), + remainingAmount: bn(110), cpiContext: null, }, }, @@ -455,7 +473,7 @@ describe('layout', () => { owner: new PublicKey( 'CPMzHV9PsUeb5pFmyrj9nEoDwtL8CcyUKQzJXJxYRnT7', ), - remainingAmount: new BN(110), + remainingAmount: bn(110), cpiContext: { setContext: true, firstSetContext: true, @@ -739,3 +757,121 @@ describe('layout', () => { }); }); }); + +describe('selectTokenPoolInfo', () => { + const infos = [ + { + mint: new PublicKey('GyFUUg2iDsGZpaxceUNQAdXfFXzraekDzbBjhS7bkTA6'), + tokenPoolPda: new PublicKey( + '5d77eGcKa1CDRJrHeohyT1igCCPX9SYWqBd6NZqsWMyt', + ), + tokenProgram: new PublicKey( + 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + ), + activity: undefined, + balance: new BN(1e9), + isInitialized: true, + poolIndex: 0, + bump: 0, + }, + { + mint: new PublicKey('GyFUUg2iDsGZpaxceUNQAdXfFXzraekDzbBjhS7bkTA6'), + tokenPoolPda: new PublicKey( + 'CqZ5Wv44cEn2R88hrftMdWowiyPhAuLLRzj1BXyq2Kz7', + ), + tokenProgram: new PublicKey( + 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + ), + activity: undefined, + balance: new BN(1.5e9), + isInitialized: true, + poolIndex: 1, + bump: 0, + }, + { + mint: new PublicKey('GyFUUg2iDsGZpaxceUNQAdXfFXzraekDzbBjhS7bkTA6'), + tokenPoolPda: new PublicKey( + '4ne3Bk9g8gKMWjTbDNc8Sigmec2FJWUjWAraMjJcQDTS', + ), + tokenProgram: new PublicKey( + 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + ), + activity: undefined, + balance: new BN(10), + isInitialized: true, + poolIndex: 2, + bump: 0, + }, + { + mint: new PublicKey('GyFUUg2iDsGZpaxceUNQAdXfFXzraekDzbBjhS7bkTA6'), + tokenPoolPda: new PublicKey( + 'Evr8a5qf2JSAf9DHF5L8qvmrdxtKWZJY9c61VkvfpTZA', + ), + tokenProgram: new PublicKey( + 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + ), + activity: undefined, + balance: new BN(10), + isInitialized: true, + poolIndex: 3, + bump: 0, + }, + { + mint: new PublicKey('GyFUUg2iDsGZpaxceUNQAdXfFXzraekDzbBjhS7bkTA6'), + tokenPoolPda: new PublicKey( + 'B6XrUD6K5VQZaG7m7fVwaf7JWbJXad8PTQdzzGcHdf7E', + ), + tokenProgram: new PublicKey( + 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + ), + activity: undefined, + balance: new BN(0), + isInitialized: false, + poolIndex: 4, + bump: 0, + }, + ]; + + it('should return the correct token pool info', () => { + for (let i = 0; i < 10000; i++) { + const tokenPoolInfo = selectTokenPoolInfo(infos); + expect(tokenPoolInfo.poolIndex).not.toBe(4); + expect(tokenPoolInfo.isInitialized).toBe(true); + } + + const decompressedInfos = selectTokenPoolInfosForDecompression( + infos, + new BN(1e9), + ); + expect(decompressedInfos.length).toBe(4); + expect(decompressedInfos[0].poolIndex).toBe(0); + expect(decompressedInfos[1].poolIndex).toBe(1); + expect(decompressedInfos[2].poolIndex).toBe(2); + expect(decompressedInfos[3].poolIndex).toBe(3); + const decompressedInfos2 = selectTokenPoolInfosForDecompression( + infos, + new BN(1.51e8), + ); + expect(decompressedInfos2.length).toBe(4); + expect(decompressedInfos2[0].poolIndex).toBe(0); + expect(decompressedInfos2[1].poolIndex).toBe(1); + expect(decompressedInfos2[2].poolIndex).toBe(2); + expect(decompressedInfos2[3].poolIndex).toBe(3); + + const decompressedInfos3 = selectTokenPoolInfosForDecompression( + infos, + new BN(1.5e8), + ); + expect(decompressedInfos3.length).toBe(1); + expect(decompressedInfos3[0].poolIndex).toBe(1); + + for (let i = 0; i < 1000; i++) { + const decompressedInfos4 = selectTokenPoolInfosForDecompression( + infos, + new BN(1), + ); + expect(decompressedInfos4.length).toBe(1); + expect(decompressedInfos4[0].poolIndex).not.toBe(4); + } + }); +}); 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..e63a4a7434 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, + TreeInfo, + 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: TreeInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); @@ -26,6 +28,8 @@ describe('mergeTokenAccounts', () => { mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); + stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); + mint = ( await createMint( rpc, @@ -48,7 +52,7 @@ describe('mergeTokenAccounts', () => { owner.publicKey, mintAuthority, bn(100), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); } }); @@ -60,13 +64,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); const postAccounts = await rpc.getCompressedTokenAccountsByOwner( owner.publicKey, @@ -85,7 +83,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); console.log('First merge succeeded'); const postFirstMergeAccounts = @@ -100,13 +98,7 @@ describe('mergeTokenAccounts', () => { // Second merge attempt try { - await mergeTokenAccounts( - rpc, - payer, - mint, - owner, - defaultTestStateTreeAccounts().merkleTree, - ); + await mergeTokenAccounts(rpc, payer, mint, owner); 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 00b877e1d9..741639679b 100644 --- a/js/compressed-token/tests/e2e/mint-to.test.ts +++ b/js/compressed-token/tests/e2e/mint-to.test.ts @@ -11,21 +11,26 @@ import { createTokenProgramLookupTable, mintTo, } from '../../src/actions'; - import { getTestKeypair, newAccountWithLamports, bn, - defaultTestStateTreeAccounts, Rpc, sendAndConfirmTx, buildAndSignTx, dedupeSigner, getTestRpc, + TreeInfo, + 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 +67,8 @@ describe('mintTo', () => { let mint: PublicKey; let mintAuthority: Keypair; let lut: PublicKey; - - const { merkleTree } = defaultTestStateTreeAccounts(); + let stateTreeInfo: TreeInfo; + let tokenPoolInfo: TokenPoolInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); @@ -83,6 +88,9 @@ describe('mintTo', () => { ) ).mint; + stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); + tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); + /// Setup LUT. const { address } = await createTokenProgramLookupTable( rpc, @@ -94,16 +102,22 @@ describe('mintTo', () => { }, 80_000); it('should mint to bob', async () => { + console.log('statetreeinfo', stateTreeInfo); + console.log('tokenpoolinfo', tokenPoolInfo); + console.log('all state tree infos', await rpc.getStateTreeInfos()); + const amount = bn(1000); - await mintTo( + const txId = await mintTo( rpc, payer, mint, bob.publicKey, mintAuthority, amount, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); + console.log('txId', txId); await assertMintTo(rpc, mint, amount, bob.publicKey); @@ -121,7 +135,8 @@ describe('mintTo', () => { bob.publicKey, mintAuthority, amount, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); }); @@ -142,7 +157,8 @@ describe('mintTo', () => { recipients.slice(0, 3), mintAuthority, amounts.slice(0, 3), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); /// Mint to 10 recipients @@ -153,9 +169,10 @@ describe('mintTo', () => { recipients.slice(0, 10), mintAuthority, amounts.slice(0, 10), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ); - + console.log('txId 10 recipients', tx); // Uneven amounts await expect( mintTo( @@ -165,7 +182,8 @@ describe('mintTo', () => { recipients, mintAuthority, amounts.slice(0, 2), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, + tokenPoolInfo, ), ).rejects.toThrowError( /Amount and toPubkey arrays must have the same length/, @@ -182,7 +200,8 @@ describe('mintTo', () => { authority: mintAuthority.publicKey, amount: amounts, toPubkey: recipients, - merkleTree: defaultTestStateTreeAccounts().merkleTree, + outputStateTreeInfo: stateTreeInfo, + tokenPoolInfo, }); const { blockhash } = await rpc.getLatestBlockhash(); @@ -195,7 +214,7 @@ describe('mintTo', () => { additionalSigners, [lookupTableAccount], ); - - return await sendAndConfirmTx(rpc, tx); + const txId = await sendAndConfirmTx(rpc, tx); + console.log('txId 22 recipients', txId); }); }); diff --git a/js/compressed-token/tests/e2e/multi-pool.test.ts b/js/compressed-token/tests/e2e/multi-pool.test.ts new file mode 100644 index 0000000000..2773c3ad14 --- /dev/null +++ b/js/compressed-token/tests/e2e/multi-pool.test.ts @@ -0,0 +1,194 @@ +import { describe, it, expect, beforeAll, assert } from 'vitest'; +import { CompressedTokenProgram } from '../../src/program'; +import { PublicKey, Signer, Keypair, SystemProgram } from '@solana/web3.js'; +import { + MINT_SIZE, + TOKEN_PROGRAM_ID, + createInitializeMint2Instruction, + getOrCreateAssociatedTokenAccount, + mintTo, +} from '@solana/spl-token'; +import { + addTokenPools, + compress, + createMint, + createTokenPool, + decompress, +} from '../../src/actions'; +import { + Rpc, + buildAndSignTx, + dedupeSigner, + newAccountWithLamports, + sendAndConfirmTx, + getTestRpc, + selectStateTreeInfo, +} from '@lightprotocol/stateless.js'; +import { WasmFactory } from '@lightprotocol/hasher.rs'; +import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + selectTokenPoolInfosForDecompression, +} from '../../src/utils'; + +async function createTestSplMint( + rpc: Rpc, + payer: Signer, + mintKeypair: Signer, + mintAuthority: Keypair, + isToken22?: boolean, +) { + const rentExemptBalance = + await rpc.getMinimumBalanceForRentExemption(MINT_SIZE); + + const createMintAccountInstruction = SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + lamports: rentExemptBalance, + newAccountPubkey: mintKeypair.publicKey, + programId: isToken22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID, + space: MINT_SIZE, + }); + const initializeMintInstruction = createInitializeMint2Instruction( + mintKeypair.publicKey, + TEST_TOKEN_DECIMALS, + mintAuthority.publicKey, + null, + isToken22 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID, + ); + const { blockhash } = await rpc.getLatestBlockhash(); + + const tx = buildAndSignTx( + [createMintAccountInstruction, initializeMintInstruction], + payer, + blockhash, + dedupeSigner(payer, [mintKeypair]), + ); + await sendAndConfirmTx(rpc, tx); +} + +const TEST_TOKEN_DECIMALS = 2; +describe('multi-pool', () => { + let rpc: Rpc; + let payer: Signer; + let mintKeypair: Keypair; + let mint: PublicKey; + let mintAuthority: Keypair; + + let bob: Signer; + let bobAta: PublicKey; + let charlie: Signer; + let charlieAta: PublicKey; + + beforeAll(async () => { + const lightWasm = await WasmFactory.getInstance(); + rpc = await getTestRpc(lightWasm); + payer = await newAccountWithLamports(rpc); + mintAuthority = Keypair.generate(); + mintKeypair = Keypair.generate(); + mint = mintKeypair.publicKey; + + /// Create external SPL mint + await createTestSplMint(rpc, payer, mintKeypair, mintAuthority); + + bob = await newAccountWithLamports(rpc); + bobAta = ( + await getOrCreateAssociatedTokenAccount( + rpc, + payer, + mint, + bob.publicKey, + ) + ).address; + charlie = await newAccountWithLamports(rpc); + charlieAta = ( + await getOrCreateAssociatedTokenAccount( + rpc, + payer, + mint, + charlie.publicKey, + ) + ).address; + await mintTo(rpc, payer, mint, bobAta, mintAuthority, BigInt(1000)); + }); + + it('should register 4 pools', async () => { + const poolAccount = CompressedTokenProgram.deriveTokenPoolPda(mint); + + assert(mint.equals(mintKeypair.publicKey)); + + /// Mint already exists externally + await expect( + createMint( + rpc, + payer, + mintAuthority.publicKey, + TEST_TOKEN_DECIMALS, + mintKeypair, + ), + ).rejects.toThrow(); + + await createTokenPool(rpc, payer, mint); + await addTokenPools(rpc, payer, mint, 3); + + const stateTreeInfos = await rpc.getStateTreeInfos(); + const info = selectStateTreeInfo(stateTreeInfos); + + const tokenPoolInfos = await getTokenPoolInfos(rpc, mint); + + expect(tokenPoolInfos.length).toBe(5); + expect(tokenPoolInfos[4].poolIndex).toBe(4); + expect(tokenPoolInfos[4].isInitialized).toBe(false); + const tokenPoolInfo = selectTokenPoolInfo(tokenPoolInfos); + + await compress( + rpc, + payer, + mint, + 100, + bob, + bobAta, + charlie.publicKey, + info, + tokenPoolInfo, + ); + + const tokenPoolInfos2 = await getTokenPoolInfos(rpc, mint); + const tokenPoolInfosForDecompression = + selectTokenPoolInfosForDecompression(tokenPoolInfos2, 10); + + await decompress( + rpc, + payer, + mint, + 10, + charlie, + charlieAta, + tokenPoolInfosForDecompression, + ); + + const tokenPoolInfos3 = await getTokenPoolInfos(rpc, mint); + const tokenPoolInfosForDecompression3 = + selectTokenPoolInfosForDecompression(tokenPoolInfos3, 90); + + expect(tokenPoolInfosForDecompression3.length).toBe(4); + + await decompress( + rpc, + payer, + mint, + 90, + charlie, + charlieAta, + tokenPoolInfosForDecompression3, + ); + + const tokenPoolInfos4 = await getTokenPoolInfos(rpc, mint); + expect(tokenPoolInfos4.length).toBe(5); + expect(() => { + selectTokenPoolInfosForDecompression(tokenPoolInfos4, 1); + }).toThrowError( + 'All provided token pool balances are zero. Please pass recent token pool infos.', + ); + }); +}); 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..254b777cfd 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,20 @@ -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, + TreeInfo, + featureFlags, + 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; @@ -23,15 +26,14 @@ describe('rpc-multi-trees', () => { let charlie: Signer; let mint: PublicKey; let mintAuthority: Keypair; - let treeAndQueue: { tree: PublicKey; queue: PublicKey }; + + let stateTreeInfo: TreeInfo; + let stateTreeInfo2: TreeInfo; + 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 +48,15 @@ describe('rpc-multi-trees', () => { ) ).mint; + stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); + + if (featureFlags.isV2()) { + stateTreeInfo2 = stateTreeInfo; + } else { + stateTreeInfo2 = (await rpc.getStateTreeInfos())[1]; + } + tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); + bob = await newAccountWithLamports(rpc, 1e9, 256); charlie = await newAccountWithLamports(rpc, 1e9, 256); @@ -56,7 +67,8 @@ describe('rpc-multi-trees', () => { bob.publicKey, mintAuthority, bn(1000), - treeAndQueue.tree, + stateTreeInfo2, + tokenPoolInfo, ); // should auto land in same tree @@ -76,45 +88,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.treeInfo.tree.toBase58() === + stateTreeInfo2.tree.toBase58(), + ).toBe(true); + expect( + receiverAccounts[0].compressedAccount.treeInfo.tree.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.treeInfo.tree.toBase58()).toBe( + stateTreeInfo2.tree.toBase58(), + ); + expect(senderAccount.treeInfo.queue.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 +121,7 @@ describe('rpc-multi-trees', () => { bob.publicKey, mintAuthority, bn(1042), - otherTree, + stateTreeInfo, ); const senderAccounts = await rpc.getCompressedTokenAccountsByOwner( @@ -131,18 +130,18 @@ describe('rpc-multi-trees', () => { ); const previousAccount = senderAccounts.items.find( account => - account.compressedAccount.merkleTree.toBase58() === - previousTree.toBase58(), + account.compressedAccount.treeInfo.tree.toBase58() === + stateTreeInfo2.tree.toBase58(), ); const newlyMintedAccount = senderAccounts.items.find( account => - account.compressedAccount.merkleTree.toBase58() === - otherTree.toBase58(), + account.compressedAccount.treeInfo.tree.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..6bdcdfc7c1 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, + TreeInfo, + 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: TreeInfo; + 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,8 @@ describe('rpc-interop token', () => { ) ).mint; - bob = await newAccountWithLamports(rpc, 1e9, 256); - charlie = await newAccountWithLamports(rpc, 1e9, 256); + stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); + tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); await mintTo( rpc, @@ -51,7 +61,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 +263,10 @@ describe('rpc-interop token', () => { ) ).mint; + const tokenPoolInfo2 = selectTokenPoolInfo( + await getTokenPoolInfos(rpc, mint2), + ); + await mintTo( rpc, payer, @@ -259,7 +274,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/select-accounts.test.ts b/js/compressed-token/tests/e2e/select-accounts.test.ts index 21377a0c2d..4a989c3639 100644 --- a/js/compressed-token/tests/e2e/select-accounts.test.ts +++ b/js/compressed-token/tests/e2e/select-accounts.test.ts @@ -1,6 +1,5 @@ -import { describe, it, expect, beforeAll, beforeEach, assert } from 'vitest'; - -import BN from 'bn.js'; +import { bn } from '@lightprotocol/stateless.js'; +import { describe, it, expect } from 'vitest'; import { ParsedTokenAccount } from '@lightprotocol/stateless.js'; import { @@ -15,19 +14,19 @@ describe('selectMinCompressedTokenAccountsForTransfer', () => { it('min: should select the largest account for a valid transfer where 1 account is enough', () => { const accounts = [ { - parsed: { amount: new BN(100) }, - compressedAccount: { lamports: new BN(10) }, + parsed: { amount: bn(100) }, + compressedAccount: { lamports: bn(10) }, }, { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectMinCompressedTokenAccountsForTransfer( @@ -36,19 +35,19 @@ describe('selectMinCompressedTokenAccountsForTransfer', () => { ); expect(selectedAccounts.length).toBe(1); - expect(total.eq(new BN(100))).toBe(true); - expect(totalLamports!.eq(new BN(10))).toBe(true); - expect(maxPossibleAmount.eq(new BN(175))).toBe(true); + expect(total.eq(bn(100))).toBe(true); + expect(totalLamports!.eq(bn(10))).toBe(true); + expect(maxPossibleAmount.eq(bn(175))).toBe(true); }); it('min: throws if there is not enough balance', () => { const accounts = [ { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); expect(() => selectMinCompressedTokenAccountsForTransfer( @@ -63,19 +62,19 @@ describe('selectMinCompressedTokenAccountsForTransfer', () => { it('min: should select multiple accounts if needed', () => { const accounts = [ { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectMinCompressedTokenAccountsForTransfer( @@ -84,14 +83,14 @@ describe('selectMinCompressedTokenAccountsForTransfer', () => { ); expect(selectedAccounts.length).toBe(2); - expect(total.eq(new BN(80))).toBe(true); - expect(totalLamports!.eq(new BN(8))).toBe(true); - expect(maxPossibleAmount.eq(new BN(105))).toBe(true); + expect(total.eq(bn(80))).toBe(true); + expect(totalLamports!.eq(bn(8))).toBe(true); + expect(maxPossibleAmount.eq(bn(105))).toBe(true); }); it('min: should handle empty accounts array', () => { const accounts: ParsedTokenAccount[] = []; - const transferAmount = new BN(75); + const transferAmount = bn(75); expect(() => selectMinCompressedTokenAccountsForTransfer( @@ -104,19 +103,19 @@ describe('selectMinCompressedTokenAccountsForTransfer', () => { it('min: should ignore accounts with zero balance', () => { const accounts = [ { - parsed: { amount: new BN(0) }, - compressedAccount: { lamports: new BN(0) }, + parsed: { amount: bn(0) }, + compressedAccount: { lamports: bn(0) }, }, { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectMinCompressedTokenAccountsForTransfer( @@ -125,27 +124,27 @@ describe('selectMinCompressedTokenAccountsForTransfer', () => { ); expect(selectedAccounts.length).toBe(2); - expect(total.eq(new BN(75))).toBe(true); - expect(totalLamports!.eq(new BN(7))).toBe(true); - expect(maxPossibleAmount.eq(new BN(75))).toBe(true); + expect(total.eq(bn(75))).toBe(true); + expect(totalLamports!.eq(bn(7))).toBe(true); + expect(maxPossibleAmount.eq(bn(75))).toBe(true); }); it('min: should handle large numbers', () => { const accounts = [ { - parsed: { amount: new BN('1000000000000000000') }, - compressedAccount: { lamports: new BN('100000000000000000') }, + parsed: { amount: bn('1000000000000000000') }, + compressedAccount: { lamports: bn('100000000000000000') }, }, { - parsed: { amount: new BN('500000000000000000') }, - compressedAccount: { lamports: new BN('50000000000000000') }, + parsed: { amount: bn('500000000000000000') }, + compressedAccount: { lamports: bn('50000000000000000') }, }, { - parsed: { amount: new BN('250000000000000000') }, - compressedAccount: { lamports: new BN('25000000000000000') }, + parsed: { amount: bn('250000000000000000') }, + compressedAccount: { lamports: bn('25000000000000000') }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN('750000000000000000'); + const transferAmount = bn('750000000000000000'); const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectMinCompressedTokenAccountsForTransfer( @@ -154,27 +153,27 @@ describe('selectMinCompressedTokenAccountsForTransfer', () => { ); expect(selectedAccounts.length).toBe(1); - expect(total.eq(new BN('1000000000000000000'))).toBe(true); - expect(totalLamports!.eq(new BN('100000000000000000'))).toBe(true); - expect(maxPossibleAmount.eq(new BN('1750000000000000000'))).toBe(true); + expect(total.eq(bn('1000000000000000000'))).toBe(true); + expect(totalLamports!.eq(bn('100000000000000000'))).toBe(true); + expect(maxPossibleAmount.eq(bn('1750000000000000000'))).toBe(true); }); it('min: should handle max inputs equal to accounts length', () => { const accounts = [ { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const maxInputs = 3; const [selectedAccounts, total, totalLamports, maxPossibleAmount] = @@ -185,27 +184,27 @@ describe('selectMinCompressedTokenAccountsForTransfer', () => { ); expect(selectedAccounts.length).toBe(2); - expect(total.eq(new BN(80))).toBe(true); - expect(totalLamports!.eq(new BN(8))).toBe(true); - expect(maxPossibleAmount.eq(new BN(105))).toBe(true); + expect(total.eq(bn(80))).toBe(true); + expect(totalLamports!.eq(bn(8))).toBe(true); + expect(maxPossibleAmount.eq(bn(105))).toBe(true); }); it('min: should handle max inputs less than accounts length', () => { const accounts = [ { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const maxInputs = 2; const [selectedAccounts, total, totalLamports, maxPossibleAmount] = @@ -216,27 +215,27 @@ describe('selectMinCompressedTokenAccountsForTransfer', () => { ); expect(selectedAccounts.length).toBe(2); - expect(total.eq(new BN(80))).toBe(true); - expect(totalLamports!.eq(new BN(8))).toBe(true); - expect(maxPossibleAmount.eq(new BN(80))).toBe(true); + expect(total.eq(bn(80))).toBe(true); + expect(totalLamports!.eq(bn(8))).toBe(true); + expect(maxPossibleAmount.eq(bn(80))).toBe(true); }); it('min: should throw if not enough accounts selected because of maxInputs lower than what WOULD be available', () => { const accounts = [ { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(100); + const transferAmount = bn(100); const maxInputs = 2; expect(() => @@ -255,19 +254,19 @@ describe('selectMinCompressedTokenAccountsForTransferorPartial', () => { it('min orPartial: should select the largest account for a valid transfer where 1 account is enough', () => { const accounts = [ { - parsed: { amount: new BN(100) }, - compressedAccount: { lamports: new BN(10) }, + parsed: { amount: bn(100) }, + compressedAccount: { lamports: bn(10) }, }, { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectMinCompressedTokenAccountsForTransferOrPartial( @@ -276,19 +275,19 @@ describe('selectMinCompressedTokenAccountsForTransferorPartial', () => { ); expect(selectedAccounts.length).toBe(1); - expect(total.eq(new BN(100))).toBe(true); - expect(totalLamports!.eq(new BN(10))).toBe(true); - expect(maxPossibleAmount.eq(new BN(175))).toBe(true); + expect(total.eq(bn(100))).toBe(true); + expect(totalLamports!.eq(bn(10))).toBe(true); + expect(maxPossibleAmount.eq(bn(175))).toBe(true); }); it('min orPartial: should return the maximum possible amount if there is not enough balance', () => { const accounts = [ { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectMinCompressedTokenAccountsForTransferOrPartial( @@ -297,27 +296,27 @@ describe('selectMinCompressedTokenAccountsForTransferorPartial', () => { ); expect(selectedAccounts.length).toBe(1); - expect(total.eq(new BN(30))).toBe(true); - expect(totalLamports!.eq(new BN(3))).toBe(true); - expect(maxPossibleAmount.eq(new BN(30))).toBe(true); + expect(total.eq(bn(30))).toBe(true); + expect(totalLamports!.eq(bn(3))).toBe(true); + expect(maxPossibleAmount.eq(bn(30))).toBe(true); }); it('min orPartial: should select multiple accounts if needed', () => { const accounts = [ { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectMinCompressedTokenAccountsForTransferOrPartial( @@ -326,14 +325,14 @@ describe('selectMinCompressedTokenAccountsForTransferorPartial', () => { ); expect(selectedAccounts.length).toBe(2); - expect(total.eq(new BN(80))).toBe(true); - expect(totalLamports!.eq(new BN(8))).toBe(true); - expect(maxPossibleAmount.eq(new BN(105))).toBe(true); + expect(total.eq(bn(80))).toBe(true); + expect(totalLamports!.eq(bn(8))).toBe(true); + expect(maxPossibleAmount.eq(bn(105))).toBe(true); }); it('min orPartial: should handle empty accounts array', () => { const accounts: ParsedTokenAccount[] = []; - const transferAmount = new BN(75); + const transferAmount = bn(75); expect(() => selectMinCompressedTokenAccountsForTransferOrPartial( @@ -346,19 +345,19 @@ describe('selectMinCompressedTokenAccountsForTransferorPartial', () => { it('min orPartial: should ignore accounts with zero balance', () => { const accounts = [ { - parsed: { amount: new BN(0) }, - compressedAccount: { lamports: new BN(0) }, + parsed: { amount: bn(0) }, + compressedAccount: { lamports: bn(0) }, }, { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectMinCompressedTokenAccountsForTransferOrPartial( @@ -367,27 +366,27 @@ describe('selectMinCompressedTokenAccountsForTransferorPartial', () => { ); expect(selectedAccounts.length).toBe(2); - expect(total.eq(new BN(75))).toBe(true); - expect(totalLamports!.eq(new BN(7))).toBe(true); - expect(maxPossibleAmount.eq(new BN(75))).toBe(true); + expect(total.eq(bn(75))).toBe(true); + expect(totalLamports!.eq(bn(7))).toBe(true); + expect(maxPossibleAmount.eq(bn(75))).toBe(true); }); it('min orPartial: should handle large numbers', () => { const accounts = [ { - parsed: { amount: new BN('1000000000000000000') }, - compressedAccount: { lamports: new BN('100000000000000000') }, + parsed: { amount: bn('1000000000000000000') }, + compressedAccount: { lamports: bn('100000000000000000') }, }, { - parsed: { amount: new BN('500000000000000000') }, - compressedAccount: { lamports: new BN('50000000000000000') }, + parsed: { amount: bn('500000000000000000') }, + compressedAccount: { lamports: bn('50000000000000000') }, }, { - parsed: { amount: new BN('250000000000000000') }, - compressedAccount: { lamports: new BN('25000000000000000') }, + parsed: { amount: bn('250000000000000000') }, + compressedAccount: { lamports: bn('25000000000000000') }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN('750000000000000000'); + const transferAmount = bn('750000000000000000'); const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectMinCompressedTokenAccountsForTransferOrPartial( @@ -396,31 +395,31 @@ describe('selectMinCompressedTokenAccountsForTransferorPartial', () => { ); expect(selectedAccounts.length).toBe(1); - expect(total.eq(new BN('1000000000000000000'))).toBe(true); - expect(totalLamports!.eq(new BN('100000000000000000'))).toBe(true); - expect(maxPossibleAmount.eq(new BN('1750000000000000000'))).toBe(true); + expect(total.eq(bn('1000000000000000000'))).toBe(true); + expect(totalLamports!.eq(bn('100000000000000000'))).toBe(true); + expect(maxPossibleAmount.eq(bn('1750000000000000000'))).toBe(true); }); it('min orPartial: should handle max inputs equal to accounts length', () => { const accounts = [ { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, { - parsed: { amount: new BN(10) }, - compressedAccount: { lamports: new BN(1) }, + parsed: { amount: bn(10) }, + compressedAccount: { lamports: bn(1) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const maxInputs = 3; const [selectedAccounts, total, totalLamports, maxPossibleAmount] = @@ -431,27 +430,27 @@ describe('selectMinCompressedTokenAccountsForTransferorPartial', () => { ); expect(selectedAccounts.length).toBe(2); - expect(total.eq(new BN(80))).toBe(true); - expect(totalLamports!.eq(new BN(8))).toBe(true); - expect(maxPossibleAmount.eq(new BN(105))).toBe(true); + expect(total.eq(bn(80))).toBe(true); + expect(totalLamports!.eq(bn(8))).toBe(true); + expect(maxPossibleAmount.eq(bn(105))).toBe(true); }); it('min orPartial: should handle max inputs less than accounts length', () => { const accounts = [ { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const maxInputs = 2; const [selectedAccounts, total, totalLamports, maxPossibleAmount] = @@ -462,27 +461,27 @@ describe('selectMinCompressedTokenAccountsForTransferorPartial', () => { ); expect(selectedAccounts.length).toBe(2); - expect(total.eq(new BN(80))).toBe(true); - expect(totalLamports!.eq(new BN(8))).toBe(true); - expect(maxPossibleAmount.eq(new BN(80))).toBe(true); + expect(total.eq(bn(80))).toBe(true); + expect(totalLamports!.eq(bn(8))).toBe(true); + expect(maxPossibleAmount.eq(bn(80))).toBe(true); }); it('min orPartial: should succeed and select 2 accounts with total 80', () => { const accounts = [ { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(100); + const transferAmount = bn(100); const maxInputs = 2; const [selectedAccounts, total, totalLamports, maxPossibleAmount] = @@ -493,9 +492,9 @@ describe('selectMinCompressedTokenAccountsForTransferorPartial', () => { ); expect(selectedAccounts.length).toBe(2); - expect(total.eq(new BN(80))).toBe(true); - expect(totalLamports!.eq(new BN(8))).toBe(true); - expect(maxPossibleAmount.eq(new BN(80))).toBe(true); + expect(total.eq(bn(80))).toBe(true); + expect(totalLamports!.eq(bn(8))).toBe(true); + expect(maxPossibleAmount.eq(bn(80))).toBe(true); }); }); @@ -503,19 +502,19 @@ describe('selectSmartCompressedTokenAccountsForTransfer', () => { it('smart: should select largest and smallest accounts for a valid transfer where 1 account is enough', () => { const accounts = [ { - parsed: { amount: new BN(100) }, - compressedAccount: { lamports: new BN(10) }, + parsed: { amount: bn(100) }, + compressedAccount: { lamports: bn(10) }, }, { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectSmartCompressedTokenAccountsForTransfer( @@ -524,19 +523,19 @@ describe('selectSmartCompressedTokenAccountsForTransfer', () => { ); expect(selectedAccounts.length).toBe(2); - expect(total.eq(new BN(125))).toBe(true); - expect(totalLamports!.eq(new BN(12))).toBe(true); - expect(maxPossibleAmount.eq(new BN(175))).toBe(true); + expect(total.eq(bn(125))).toBe(true); + expect(totalLamports!.eq(bn(12))).toBe(true); + expect(maxPossibleAmount.eq(bn(175))).toBe(true); }); it('smart: throws if there is not enough balance', () => { const accounts = [ { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); expect(() => selectSmartCompressedTokenAccountsForTransfer( @@ -549,19 +548,19 @@ describe('selectSmartCompressedTokenAccountsForTransfer', () => { it('smart: should select 3 accounts if 2 are needed', () => { const accounts = [ { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectSmartCompressedTokenAccountsForTransfer( @@ -570,14 +569,14 @@ describe('selectSmartCompressedTokenAccountsForTransfer', () => { ); expect(selectedAccounts.length).toBe(3); - expect(total.eq(new BN(105))).toBe(true); - expect(totalLamports!.eq(new BN(10))).toBe(true); - expect(maxPossibleAmount.eq(new BN(105))).toBe(true); + expect(total.eq(bn(105))).toBe(true); + expect(totalLamports!.eq(bn(10))).toBe(true); + expect(maxPossibleAmount.eq(bn(105))).toBe(true); }); it('smart: should handle empty accounts array', () => { const accounts: ParsedTokenAccount[] = []; - const transferAmount = new BN(75); + const transferAmount = bn(75); expect(() => selectSmartCompressedTokenAccountsForTransfer( @@ -590,19 +589,19 @@ describe('selectSmartCompressedTokenAccountsForTransfer', () => { it('smart: should ignore accounts with zero balance', () => { const accounts = [ { - parsed: { amount: new BN(0) }, - compressedAccount: { lamports: new BN(0) }, + parsed: { amount: bn(0) }, + compressedAccount: { lamports: bn(0) }, }, { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectSmartCompressedTokenAccountsForTransfer( @@ -611,27 +610,27 @@ describe('selectSmartCompressedTokenAccountsForTransfer', () => { ); expect(selectedAccounts.length).toBe(2); - expect(total.eq(new BN(75))).toBe(true); - expect(totalLamports!.eq(new BN(7))).toBe(true); - expect(maxPossibleAmount.eq(new BN(75))).toBe(true); + expect(total.eq(bn(75))).toBe(true); + expect(totalLamports!.eq(bn(7))).toBe(true); + expect(maxPossibleAmount.eq(bn(75))).toBe(true); }); it('smart: should handle large numbers', () => { const accounts = [ { - parsed: { amount: new BN('1000000000000000000') }, - compressedAccount: { lamports: new BN('100000000000000000') }, + parsed: { amount: bn('1000000000000000000') }, + compressedAccount: { lamports: bn('100000000000000000') }, }, { - parsed: { amount: new BN('500000000000000000') }, - compressedAccount: { lamports: new BN('50000000000000000') }, + parsed: { amount: bn('500000000000000000') }, + compressedAccount: { lamports: bn('50000000000000000') }, }, { - parsed: { amount: new BN('250000000000000000') }, - compressedAccount: { lamports: new BN('25000000000000000') }, + parsed: { amount: bn('250000000000000000') }, + compressedAccount: { lamports: bn('25000000000000000') }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN('750000000000000000'); + const transferAmount = bn('750000000000000000'); const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectSmartCompressedTokenAccountsForTransfer( @@ -640,27 +639,27 @@ describe('selectSmartCompressedTokenAccountsForTransfer', () => { ); expect(selectedAccounts.length).toBe(2); - expect(total.eq(new BN('1250000000000000000'))).toBe(true); - expect(totalLamports!.eq(new BN('125000000000000000'))).toBe(true); - expect(maxPossibleAmount.eq(new BN('1750000000000000000'))).toBe(true); + expect(total.eq(bn('1250000000000000000'))).toBe(true); + expect(totalLamports!.eq(bn('125000000000000000'))).toBe(true); + expect(maxPossibleAmount.eq(bn('1750000000000000000'))).toBe(true); }); it('smart: should handle max inputs equal to accounts length', () => { const accounts = [ { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const maxInputs = 3; const [selectedAccounts, total, totalLamports, maxPossibleAmount] = @@ -671,27 +670,27 @@ describe('selectSmartCompressedTokenAccountsForTransfer', () => { ); expect(selectedAccounts.length).toBe(3); - expect(total.eq(new BN(105))).toBe(true); - expect(totalLamports!.eq(new BN(10))).toBe(true); - expect(maxPossibleAmount.eq(new BN(105))).toBe(true); + expect(total.eq(bn(105))).toBe(true); + expect(totalLamports!.eq(bn(10))).toBe(true); + expect(maxPossibleAmount.eq(bn(105))).toBe(true); }); it('smart: should throw if not enough accounts selected because of maxInputs lower than what WOULD be available', () => { const accounts = [ { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(100); + const transferAmount = bn(100); const maxInputs = 2; expect(() => @@ -708,19 +707,19 @@ describe('selectSmartCompressedTokenAccountsForTransfer', () => { it('smart: should handle max inputs less than accounts length', () => { const accounts = [ { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const maxInputs = 2; const [selectedAccounts, total, totalLamports, maxPossibleAmount] = @@ -731,27 +730,27 @@ describe('selectSmartCompressedTokenAccountsForTransfer', () => { ); expect(selectedAccounts.length).toBe(2); - expect(total.eq(new BN(80))).toBe(true); - expect(totalLamports!.eq(new BN(8))).toBe(true); - expect(maxPossibleAmount.eq(new BN(80))).toBe(true); + expect(total.eq(bn(80))).toBe(true); + expect(totalLamports!.eq(bn(8))).toBe(true); + expect(maxPossibleAmount.eq(bn(80))).toBe(true); }); it('smart: should throw if not enough accounts selected because of maxInputs lower than what WOULD be available', () => { const accounts = [ { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(100); + const transferAmount = bn(100); const maxInputs = 2; expect(() => @@ -770,19 +769,19 @@ describe('selectSmartCompressedTokenAccountsForTransferOrPartial', () => { it('smart-orPartial: should select 2 accounts for a valid transfer where 1 account is enough', () => { const accounts = [ { - parsed: { amount: new BN(100) }, - compressedAccount: { lamports: new BN(10) }, + parsed: { amount: bn(100) }, + compressedAccount: { lamports: bn(10) }, }, { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectSmartCompressedTokenAccountsForTransferOrPartial( @@ -791,19 +790,19 @@ describe('selectSmartCompressedTokenAccountsForTransferOrPartial', () => { ); expect(selectedAccounts.length).toBe(2); - expect(total.eq(new BN(125))).toBe(true); - expect(totalLamports!.eq(new BN(12))).toBe(true); - expect(maxPossibleAmount.eq(new BN(175))).toBe(true); + expect(total.eq(bn(125))).toBe(true); + expect(totalLamports!.eq(bn(12))).toBe(true); + expect(maxPossibleAmount.eq(bn(175))).toBe(true); }); it('smart-orPartial: should return the maximum possible amount if there is not enough balance', () => { const accounts = [ { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectSmartCompressedTokenAccountsForTransferOrPartial( @@ -812,27 +811,27 @@ describe('selectSmartCompressedTokenAccountsForTransferOrPartial', () => { ); expect(selectedAccounts.length).toBe(1); - expect(total.eq(new BN(30))).toBe(true); - expect(totalLamports!.eq(new BN(3))).toBe(true); - expect(maxPossibleAmount.eq(new BN(30))).toBe(true); + expect(total.eq(bn(30))).toBe(true); + expect(totalLamports!.eq(bn(3))).toBe(true); + expect(maxPossibleAmount.eq(bn(30))).toBe(true); }); it('smart-orPartial: should select multiple accounts if needed', () => { const accounts = [ { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(75); + const transferAmount = bn(75); const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectSmartCompressedTokenAccountsForTransferOrPartial( @@ -841,14 +840,14 @@ describe('selectSmartCompressedTokenAccountsForTransferOrPartial', () => { ); expect(selectedAccounts.length).toBe(3); - expect(total.eq(new BN(105))).toBe(true); - expect(totalLamports!.eq(new BN(10))).toBe(true); - expect(maxPossibleAmount.eq(new BN(105))).toBe(true); + expect(total.eq(bn(105))).toBe(true); + expect(totalLamports!.eq(bn(10))).toBe(true); + expect(maxPossibleAmount.eq(bn(105))).toBe(true); }); it('smart-orPartial: should handle empty accounts array', () => { const accounts: ParsedTokenAccount[] = []; - const transferAmount = new BN(75); + const transferAmount = bn(75); expect(() => selectSmartCompressedTokenAccountsForTransferOrPartial( @@ -861,19 +860,19 @@ describe('selectSmartCompressedTokenAccountsForTransferOrPartial', () => { it('smart-orPartial: should throw if not enough accounts selected because of maxInputs lower than what WOULD be available', () => { const accounts = [ { - parsed: { amount: new BN(50) }, - compressedAccount: { lamports: new BN(5) }, + parsed: { amount: bn(50) }, + compressedAccount: { lamports: bn(5) }, }, { - parsed: { amount: new BN(30) }, - compressedAccount: { lamports: new BN(3) }, + parsed: { amount: bn(30) }, + compressedAccount: { lamports: bn(3) }, }, { - parsed: { amount: new BN(25) }, - compressedAccount: { lamports: new BN(2) }, + parsed: { amount: bn(25) }, + compressedAccount: { lamports: bn(2) }, }, ] as ParsedTokenAccount[]; - const transferAmount = new BN(100); + const transferAmount = bn(100); const maxInputs = 2; const [selectedAccounts, total, totalLamports, maxPossibleAmount] = selectSmartCompressedTokenAccountsForTransferOrPartial( @@ -883,8 +882,8 @@ describe('selectSmartCompressedTokenAccountsForTransferOrPartial', () => { ); expect(selectedAccounts.length).toBe(2); - expect(total.eq(new BN(80))).toBe(true); - expect(totalLamports!.eq(new BN(8))).toBe(true); - expect(maxPossibleAmount.eq(new BN(80))).toBe(true); + expect(total.eq(bn(80))).toBe(true); + expect(totalLamports!.eq(bn(8))).toBe(true); + expect(maxPossibleAmount.eq(bn(80))).toBe(true); }); }); diff --git a/js/compressed-token/tests/e2e/transfer-delegated.test.ts b/js/compressed-token/tests/e2e/transfer-delegated.test.ts new file mode 100644 index 0000000000..be2d66d2da --- /dev/null +++ b/js/compressed-token/tests/e2e/transfer-delegated.test.ts @@ -0,0 +1,411 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import { PublicKey, Keypair, Signer } from '@solana/web3.js'; +import BN from 'bn.js'; +import { + Rpc, + bn, + newAccountWithLamports, + getTestRpc, + TreeInfo, + selectStateTreeInfo, + ParsedTokenAccount, +} from '@lightprotocol/stateless.js'; +import { WasmFactory } from '@lightprotocol/hasher.rs'; +import { + createMint, + mintTo, + approve, + transferDelegated, +} from '../../src/actions'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + TokenPoolInfo, +} from '../../src/utils/get-token-pool-infos'; + +const assertPostTransfer = async ( + rpc: Rpc, + refMint: PublicKey, + refAmountDelegated: BN, + refAmountTransferred: BN, + refOwner: PublicKey, + refDelegate: PublicKey, + refRecipient: PublicKey, + preOwnerAccounts: ParsedTokenAccount[], + preDelegateAccounts: ParsedTokenAccount[], + preRecipientAccounts: ParsedTokenAccount[], + newOwnerCount: number, + newDelegateCount: number, + newRecipientCount: number, +) => { + const getAmount = async (type: 'delegate' | 'owner', pubkey: PublicKey) => { + const accounts = + type === 'delegate' + ? await rpc.getCompressedTokenAccountsByDelegate(pubkey, { + mint: refMint, + }) + : await rpc.getCompressedTokenAccountsByOwner(pubkey, { + mint: refMint, + }); + return accounts.items.reduce( + (acc, account) => acc.add(account.parsed.amount), + bn(0), + ); + }; + + const postTransferDelegateAmount = await getAmount('delegate', refDelegate); + const postTransferOwnerAmount = await getAmount('owner', refOwner); + const postTransferRecipientAmount = await getAmount('owner', refRecipient); + + const logError = ( + label: string, + expected: BN | number, + actual: BN | number, + ) => { + console.error(`${label}:`); + console.error(` Expected: ${expected.toString()}`); + console.error(` Actual: ${actual.toString()}`); + }; + + const totalAmount = postTransferDelegateAmount.add( + postTransferRecipientAmount, + ); + if (!totalAmount.eq(refAmountDelegated)) { + logError('Total amount', refAmountDelegated, totalAmount); + } + expect(totalAmount.eq(refAmountDelegated)).toBe(true); + + const preDelegateTotal = preDelegateAccounts.reduce( + (acc, account) => acc.add(account.parsed.amount), + bn(0), + ); + if ( + !preDelegateTotal.eq( + postTransferDelegateAmount.add(refAmountTransferred), + ) + ) { + logError( + 'Delegate amount', + preDelegateTotal, + postTransferDelegateAmount.add(refAmountTransferred), + ); + } + expect( + preDelegateTotal.eq( + postTransferDelegateAmount.add(refAmountTransferred), + ), + ).toBe(true); + + // pre owner amount - ref amount = post owner amount + const preOwnerTotal = preOwnerAccounts.reduce( + (acc, account) => acc.add(account.parsed.amount), + bn(0), + ); + const expectedOwnerAmount = preOwnerTotal.sub(refAmountTransferred); + if (!expectedOwnerAmount.eq(postTransferOwnerAmount)) { + logError('Owner amount', expectedOwnerAmount, postTransferOwnerAmount); + } + expect(expectedOwnerAmount.eq(postTransferOwnerAmount)).toBe(true); + + const preRecipientTotal = preRecipientAccounts.reduce( + (acc, account) => acc.add(account.parsed.amount), + bn(0), + ); + const expectedRecipientAmount = preRecipientTotal.add(refAmountTransferred); + if (!expectedRecipientAmount.eq(postTransferRecipientAmount)) { + logError( + 'Recipient amount', + expectedRecipientAmount, + postTransferRecipientAmount, + ); + } + expect(expectedRecipientAmount.eq(postTransferRecipientAmount)).toBe(true); + + const postDelegateAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(refDelegate, { + mint: refMint, + }) + ).items; + if (postDelegateAccounts.length !== newDelegateCount) { + logError( + 'Delegate accounts count', + newDelegateCount, + postDelegateAccounts.length, + ); + } + expect(postDelegateAccounts.length).toBe(newDelegateCount); + + const postOwnerAccounts = ( + await rpc.getCompressedTokenAccountsByOwner(refOwner, { + mint: refMint, + }) + ).items; + if (postOwnerAccounts.length !== newOwnerCount) { + logError( + 'Owner accounts count', + newOwnerCount, + postOwnerAccounts.length, + ); + } + expect(postOwnerAccounts.length).toBe(newOwnerCount); + + const postRecipientAccounts = ( + await rpc.getCompressedTokenAccountsByOwner(refRecipient, { + mint: refMint, + }) + ).items; + if (postRecipientAccounts.length !== newRecipientCount) { + logError( + 'Recipient accounts count', + newRecipientCount, + postRecipientAccounts.length, + ); + } + expect(postRecipientAccounts.length).toBe(newRecipientCount); +}; + +const TEST_TOKEN_DECIMALS = 2; + +describe('transferDelegated', () => { + let rpc: Rpc; + let payer: Signer; + let bob: Signer; + let mint: PublicKey; + let mintAuthority: Keypair; + let stateTreeInfo: TreeInfo; + let tokenPoolInfo: TokenPoolInfo; + + beforeAll(async () => { + const lightWasm = await WasmFactory.getInstance(); + rpc = await getTestRpc(lightWasm); + payer = await newAccountWithLamports(rpc, 1e9); + bob = await newAccountWithLamports(rpc, 1e9); + mintAuthority = Keypair.generate(); + const mintKeypair = Keypair.generate(); + + stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); + + mint = ( + await createMint( + rpc, + payer, + mintAuthority.publicKey, + TEST_TOKEN_DECIMALS, + mintKeypair, + ) + ).mint; + + tokenPoolInfo = selectTokenPoolInfo(await getTokenPoolInfos(rpc, mint)); + + await mintTo( + rpc, + payer, + mint, + payer.publicKey, + mintAuthority, + bn(1000), + stateTreeInfo, + tokenPoolInfo, + ); + + await approve(rpc, payer, mint, 1000, payer, bob.publicKey); + }); + + it('should transfer one delegated account', async () => { + const charlie = await newAccountWithLamports(rpc, 1e9); + const payerPreCompressedTokenAccounts = ( + await rpc.getCompressedTokenAccountsByOwner(payer.publicKey, { + mint, + }) + ).items; + + const preDelegateAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(bob.publicKey, { + mint, + }) + ).items; + + await transferDelegated(rpc, bob, mint, 1000, bob, charlie.publicKey); + + await assertPostTransfer( + rpc, + mint, + bn(1000), + bn(1000), + payer.publicKey, + bob.publicKey, + charlie.publicKey, + payerPreCompressedTokenAccounts, + preDelegateAccounts, + [], // charlie + 0, + 0, + 1, + ); + }); + + it('should transfer using two delegated accounts', async () => { + const newMintKeypair = Keypair.generate(); + const newMint = ( + await createMint( + rpc, + payer, + mintAuthority.publicKey, + TEST_TOKEN_DECIMALS, + newMintKeypair, + ) + ).mint; + + const newTokenPoolInfo = selectTokenPoolInfo( + await getTokenPoolInfos(rpc, newMint), + ); + + await mintTo( + rpc, + payer, + newMint, + payer.publicKey, + mintAuthority, + bn(500), + stateTreeInfo, + newTokenPoolInfo, + ); + + await mintTo( + rpc, + payer, + newMint, + payer.publicKey, + mintAuthority, + bn(600), + stateTreeInfo, + newTokenPoolInfo, + ); + + await approve(rpc, payer, newMint, 1100, payer, bob.publicKey); + + const dave = await newAccountWithLamports(rpc, 1e9); + const payerPreCompressedTokenAccounts = ( + await rpc.getCompressedTokenAccountsByOwner(payer.publicKey, { + mint: newMint, + }) + ).items; + + const preDelegateAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(bob.publicKey, { + mint: newMint, + }) + ).items; + + await transferDelegated(rpc, bob, newMint, 1100, bob, dave.publicKey); + + await assertPostTransfer( + rpc, + newMint, + bn(1100), + bn(1100), + payer.publicKey, + bob.publicKey, + dave.publicKey, + payerPreCompressedTokenAccounts, + preDelegateAccounts, + [], + 0, + 0, + 1, + ); + }); + + let newMint: PublicKey; + let eve: Signer; + it('should transfer a partial amount leaving a remainder', async () => { + const newMintKeypair = Keypair.generate(); + newMint = ( + await createMint( + rpc, + payer, + mintAuthority.publicKey, + TEST_TOKEN_DECIMALS, + newMintKeypair, + ) + ).mint; + + const newTokenPoolInfo = selectTokenPoolInfo( + await getTokenPoolInfos(rpc, newMint), + ); + + await mintTo( + rpc, + payer, + newMint, + payer.publicKey, + mintAuthority, + bn(1000), + stateTreeInfo, + newTokenPoolInfo, + ); + + await approve(rpc, payer, newMint, 1000, payer, bob.publicKey); + + eve = await newAccountWithLamports(rpc, 1e9); + const payerPreCompressedTokenAccounts = ( + await rpc.getCompressedTokenAccountsByOwner(payer.publicKey, { + mint: newMint, + }) + ).items; + + const preDelegateAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(bob.publicKey, { + mint: newMint, + }) + ).items; + + const transferAmount = 600; + await transferDelegated( + rpc, + bob, + newMint, + transferAmount, + bob, + eve.publicKey, + ); + + const postDelegateAccounts = ( + await rpc.getCompressedTokenAccountsByDelegate(bob.publicKey, { + mint: newMint, + }) + ).items; + + await assertPostTransfer( + rpc, + newMint, + bn(1000), + bn(600), + payer.publicKey, + bob.publicKey, + eve.publicKey, + payerPreCompressedTokenAccounts, + preDelegateAccounts, + [], + 1, + 1, + 1, + ); + + const remainingDelegatedAmount = postDelegateAccounts.reduce( + (acc, account) => acc.add(account.parsed.amount), + bn(0), + ); + + expect(remainingDelegatedAmount.eq(bn(400))).toBe(true); + }); + + it('should fail to transfer more than the remaining delegated amount', async () => { + // Try to transfer more than the remaining delegated amount + await expect( + transferDelegated(rpc, bob, newMint, 500, bob, eve.publicKey), + ).rejects.toThrowError( + 'Insufficient balance for transfer. Required: 500, available: 400.', + ); + }); +}); diff --git a/js/compressed-token/tests/e2e/transfer.test.ts b/js/compressed-token/tests/e2e/transfer.test.ts index 94ece860d4..92aa5c4773 100644 --- a/js/compressed-token/tests/e2e/transfer.test.ts +++ b/js/compressed-token/tests/e2e/transfer.test.ts @@ -4,29 +4,23 @@ 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, + TreeInfo, + 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,31 +76,39 @@ 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; describe('transfer', () => { - let rpc: TestRpc; + let rpc: TestRpc | Rpc; let payer: Signer; let bob: Signer; let charlie: Signer; let mint: PublicKey; let mintAuthority: Keypair; - const { merkleTree } = defaultTestStateTreeAccounts(); + + let stateTreeInfo: TreeInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); rpc = await getTestRpc(lightWasm); + // rpc = createRpc(); payer = await newAccountWithLamports(rpc, 1e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); + stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); + mint = ( await createMint( rpc, @@ -129,7 +131,7 @@ describe('transfer', () => { bob.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); }); @@ -142,16 +144,15 @@ describe('transfer', () => { }) ).items; - await transfer( + const txid = await transfer( rpc, payer, mint, bn(700), bob, charlie.publicKey, - merkleTree, ); - + console.log('txid transfer ', txid); await assertTransfer( rpc, bobPreCompressedTokenAccounts, @@ -169,16 +170,15 @@ describe('transfer', () => { await rpc.getCompressedTokenAccountsByOwner(bob.publicKey, { mint, }); - await transfer( + const txid2 = await transfer( rpc, payer, mint, bn(200), bob, charlie.publicKey, - merkleTree, ); - + console.log('txid transfer 2 ', txid2); await assertTransfer( rpc, bobPreCompressedTokenAccounts2.items, @@ -197,15 +197,7 @@ describe('transfer', () => { mint, }); - await transfer( - rpc, - payer, - mint, - bn(5), - charlie, - bob.publicKey, - merkleTree, - ); + await transfer(rpc, payer, mint, bn(5), charlie, bob.publicKey); await assertTransfer( rpc, @@ -224,6 +216,7 @@ describe('transfer', () => { await rpc.getCompressedTokenAccountsByOwner(charlie.publicKey, { mint, }); + await transfer(rpc, payer, mint, bn(700), charlie, bob.publicKey); await assertTransfer( @@ -238,15 +231,7 @@ describe('transfer', () => { ); await expect( - transfer( - rpc, - payer, - mint, - 10000, - bob, - charlie.publicKey, - merkleTree, - ), + transfer(rpc, payer, mint, 10000, bob, charlie.publicKey), ).rejects.toThrow('Insufficient balance for transfer'); }); @@ -276,7 +261,7 @@ describe('transfer', () => { bob.publicKey, mintAuthority, bn(1000), - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); /// send 700 from bob -> charlie @@ -288,15 +273,7 @@ describe('transfer', () => { }) ).items; - await transfer( - rpc, - payer, - mint, - bn(700), - bob, - charlie.publicKey, - defaultTestStateTreeAccounts().merkleTree, - ); + await transfer(rpc, payer, mint, bn(700), bob, charlie.publicKey); await assertTransfer( rpc, @@ -319,12 +296,15 @@ describe('e2e transfer with multiple accounts', () => { let mint: PublicKey; let mintAuthority: Keypair; + let stateTreeInfo: TreeInfo; + beforeAll(async () => { rpc = await getTestRpc(await WasmFactory.getInstance()); payer = await newAccountWithLamports(rpc, 1e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); + stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); mint = ( await createMint( rpc, @@ -349,8 +329,8 @@ describe('e2e transfer with multiple accounts', () => { mint, sender.publicKey, mintAuthority, - new BN(25), - defaultTestStateTreeAccounts().merkleTree, + bn(25), + stateTreeInfo, ); await mintTo( rpc, @@ -358,8 +338,8 @@ describe('e2e transfer with multiple accounts', () => { mint, sender.publicKey, mintAuthority, - new BN(25), - defaultTestStateTreeAccounts().merkleTree, + bn(25), + stateTreeInfo, ); await mintTo( rpc, @@ -367,8 +347,8 @@ describe('e2e transfer with multiple accounts', () => { mint, sender.publicKey, mintAuthority, - new BN(25), - defaultTestStateTreeAccounts().merkleTree, + bn(25), + stateTreeInfo, ); await mintTo( rpc, @@ -376,8 +356,8 @@ describe('e2e transfer with multiple accounts', () => { mint, sender.publicKey, mintAuthority, - new BN(25), - defaultTestStateTreeAccounts().merkleTree, + bn(25), + stateTreeInfo, ); const senderAccounts = await rpc.getCompressedTokenAccountsByOwner( @@ -387,11 +367,11 @@ describe('e2e transfer with multiple accounts', () => { expect(senderAccounts.items.length).toBe(4); const totalAmount = senderAccounts.items.reduce( (sum, account) => sum.add(account.parsed.amount), - new BN(0), + bn(0), ); - expect(totalAmount.eq(new BN(100))).toBe(true); + expect(totalAmount.eq(bn(100))).toBe(true); - const transferAmount = new BN(100); + const transferAmount = bn(100); await transferHelper( rpc, @@ -400,7 +380,6 @@ describe('e2e transfer with multiple accounts', () => { sender, transferAmount, recipient, - defaultTestStateTreeAccounts().merkleTree, ); assertTransfer( @@ -421,7 +400,6 @@ async function transferHelper( owner: Signer, amount: BN, toAddress: PublicKey, - merkleTree: PublicKey, ) { const compressedTokenAccounts = await rpc.getCompressedTokenAccountsByOwner( owner.publicKey, @@ -444,7 +422,6 @@ async function transferHelper( amount, recentInputStateRootIndices: proof.rootIndices, recentValidityProof: proof.compressedProof, - outputStateTrees: merkleTree, }); const { blockhash } = await rpc.getLatestBlockhash(); diff --git a/js/compressed-token/tsconfig.json b/js/compressed-token/tsconfig.json index f3440f50d0..c666926b18 100644 --- a/js/compressed-token/tsconfig.json +++ b/js/compressed-token/tsconfig.json @@ -13,7 +13,7 @@ "lib": ["ESNext", "DOM"], "types": ["node"], "skipLibCheck": false, - "typeRoots": ["types/", "./node_modules/@types"] + "typeRoots": ["./types", "./node_modules/@types"] }, "include": ["./src/**/*.ts", "rollup.config.js"] } diff --git a/js/compressed-token/vitest.config.ts b/js/compressed-token/vitest.config.ts index e2fbcf1374..26a728edbe 100644 --- a/js/compressed-token/vitest.config.ts +++ b/js/compressed-token/vitest.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ exclude: ['src/program.ts'], testTimeout: 350000, hookTimeout: 100000, + reporters: ['verbose'], }, define: { 'import.meta.vitest': false, diff --git a/js/stateless.js/CHANGELOG.md b/js/stateless.js/CHANGELOG.md index f5986184a8..231faf672a 100644 --- a/js/stateless.js/CHANGELOG.md +++ b/js/stateless.js/CHANGELOG.md @@ -1,5 +1,130 @@ # 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 + +- Renamed `ActiveTreeBundle` to `StateTreeInfo` +- Updated `StateTreeInfo` internal structure: `{ tree: PublicKey, queue: PublicKey, cpiContext: PublicKey | null, treeType: TreeType }` +- Replaced `pickRandomTreeAndQueue` with `selectStateTreeInfo` +- Use `selectStateTreeInfo` for tree selection instead of `pickRandomTreeAndQueue` + +### Deprecations + +- `rpc.getValidityProof` is now deprecated, use `rpc.getValidityProofV0` instead. +- `CompressedProof` and `CompressedProofWithContext` were renamed to `ValidityProof` and `ValidityProofWithContext` + +### Migration Guide + +1. Update Type References if you use them: + +```typescript +// Old code +const bundle: ActiveTreeBundle = { + tree: publicKey, + queue: publicKey, + cpiContext: publicKey, +}; + +// New code +const info: StateTreeInfo = { + tree: publicKey, + queue: publicKey, // Now required + cpiContext: publicKey, + treeType: TreeType.StateV1, // New required field +}; +``` + +2. Update Method Calls: + +```typescript +// Old code +const ix = LightSystemProgram.compress({ + outputStateTree: bundle, +}); + +// New code +const ix = LightSystemProgram.compress({ + outputStateTree: info, +}); +``` + +3. Tree Fetching & Selection: + +```typescript +// Old code +const bundles = await connection.getCachedActiveStateTreeInfo(); +const { tree, queue } = pickRandomTreeAndQueue(bundles); + +// New code +const infos = await rpc.getStateTreeInfos(); +const selectedInfo = selectStateTreeInfo(info); +``` + +4. RPC Changes: + +```typescript +// Old code +// Still works, but will do one additional RPC call. +// const proof = await rpc.getValidityProof(hash[], address[]); +const proof = await rpc.getValidityProofV0( + inputAccounts.map(account => (bn(account.hash)), +); + +// New code +// const proof = await rpc.getValidityProofV0(HashWithTree[], AddressWithTree[]); +const proof = await rpc.getValidityProofV0( + inputAccounts.map(account => ({ + hash: account.hash, + tree: account.treeInfo.tree, + queue: account.treeInfo.queue, + })), +); +``` + +```typescript +// new type +export interface HashWithTree { + hash: BN254; + tree: PublicKey; + queue: PublicKey; +} +``` + +5. Other breaking changes: + +**MerkleContext, MerkleContextWithMerkleProof, CompressedAccountWithMerkleContext** now use `treeInfo`: + +```typescript +/** + * Context for compressed account stored in a state tree + */ +export type MerkleContext = { + /** + * Tree info + */ + treeInfo: StateTreeInfo; + /** + * Poseidon hash of the account. Stored as leaf in state tree + */ + hash: BN; + /** + * Position of `hash` in the State tree + */ + leafIndex: number; + /** + * Whether the account can be proven by index or by merkle proof + */ + proveByIndex: boolean; +}; +``` + ## [0.20.5-0.20.9] - 2025-02-24 ### Bumped to latest compressed-token sdk @@ -21,7 +146,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..f5430e0bb9 100644 --- a/js/stateless.js/package.json +++ b/js/stateless.js/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/stateless.js", - "version": "0.20.9", + "version": "0.21.0", "description": "JavaScript API for Light & ZK Compression", "sideEffects": false, "main": "dist/cjs/node/index.cjs", @@ -88,15 +88,17 @@ "test": "pnpm test:unit:all && pnpm test:e2e:all", "test-all": "vitest run", "test:unit:all": "vitest run tests/unit --reporter=verbose", + "test:unit:tree-info": "vitest run tests/unit/utils/tree-info.test.ts --reporter=verbose", "test:conversions": "vitest run tests/unit/utils/conversion.test.ts --reporter=verbose", "test-validator": "./../../cli/test_bin/run test-validator --prover-run-mode rpc", + "test-validator-skip-prover": "./../../cli/test_bin/run test-validator --skip-prover", "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", + "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 && pnpm test-validator-skip-prover && vitest run tests/e2e/rpc-multi-trees.test.ts && vitest run tests/e2e/layout.test.ts && vitest run tests/e2e/safe-conversion.test.ts", "test:index": "vitest run tests/e2e/program.test.ts", "test:e2e:layout": "vitest run tests/e2e/layout.test.ts --reporter=verbose", "test:e2e:safe-conversion": "vitest run tests/e2e/safe-conversion.test.ts --reporter=verbose", diff --git a/js/stateless.js/src/actions/compress.ts b/js/stateless.js/src/actions/compress.ts index 3a3c13edcb..3477678d2d 100644 --- a/js/stateless.js/src/actions/compress.ts +++ b/js/stateless.js/src/actions/compress.ts @@ -5,47 +5,49 @@ import { Signer, TransactionSignature, } 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 { TreeInfo } 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?: TreeInfo, 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.getStateTreeInfos(); + 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..6f69521bfe 100644 --- a/js/stateless.js/src/actions/create-account.ts +++ b/js/stateless.js/src/actions/create-account.ts @@ -9,72 +9,67 @@ 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 { getDefaultAddressTreeInfo } from '../constants'; +import { AddressTreeInfo, bn, TreeInfo } 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?: TreeInfo, 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.getStateTreeInfos(); + outputStateTreeInfo = selectStateTreeInfo(stateTreeInfo); } const proof = await rpc.getValidityProofV0(undefined, [ { address: bn(address.toBytes()), - tree: addressTree, - queue: addressQueue, + tree, + queue, }, ]); const params: NewAddressParams = { seed: seed, addressMerkleTreeRootIndex: proof.rootIndices[0], - addressMerkleTreePubkey: proof.merkleTrees[0], - addressQueuePubkey: proof.nullifierQueues[0], + addressMerkleTreePubkey: proof.treeInfos[0].tree, + addressQueuePubkey: proof.treeInfos[0].queue, }; const ix = await LightSystemProgram.createAccount({ @@ -83,7 +78,7 @@ export async function createAccount( newAddress: Array.from(address.toBytes()), recentValidityProof: proof.compressedProof, programId, - outputStateTree, + outputStateTreeInfo, }); const tx = buildAndSignTx( @@ -101,32 +96,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?: TreeInfo, confirmOptions?: ConfirmOptions, ): Promise { lamports = bn(lamports); @@ -140,36 +131,25 @@ export async function createAccountWithLamports( lamports, ); - if (!outputStateTree) { - const stateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); - const { tree } = pickRandomTreeAndQueue(stateTreeInfo); - outputStateTree = tree; - } - 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)), + inputAccounts.map(account => 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: proof.rootIndices[proof.rootIndices.length - 1], addressMerkleTreePubkey: - proof.merkleTrees[proof.merkleTrees.length - 1], - addressQueuePubkey: - proof.nullifierQueues[proof.nullifierQueues.length - 1], + proof.treeInfos[proof.treeInfos.length - 1].tree, + addressQueuePubkey: proof.treeInfos[proof.treeInfos.length - 1].queue, }; const ix = await LightSystemProgram.createAccount({ @@ -179,8 +159,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..bcb635712f 100644 --- a/js/stateless.js/src/actions/decompress.ts +++ b/js/stateless.js/src/actions/decompress.ts @@ -14,27 +14,21 @@ import { CompressedAccountWithMerkleContext, 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 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, confirmOptions?: ConfirmOptions, ): Promise { - /// TODO: use dynamic state tree and nullifier queue - const userCompressedAccountsWithMerkleContext: CompressedAccountWithMerkleContext[] = (await rpc.getCompressedAccountsByOwner(payer.publicKey)).items; @@ -58,7 +52,6 @@ export async function decompress( const ix = await LightSystemProgram.decompress({ payer: payer.publicKey, toAddress: recipient, - outputStateTree: outputStateTree, 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..44a681e343 100644 --- a/js/stateless.js/src/actions/transfer.ts +++ b/js/stateless.js/src/actions/transfer.ts @@ -5,31 +5,25 @@ import { Signer, TransactionSignature, } from '@solana/web3.js'; - import BN from 'bn.js'; import { LightSystemProgram, selectMinCompressedSolAccountsForTransfer, } from '../programs'; import { Rpc } from '../rpc'; - -import { bn, CompressedAccountWithMerkleContext } from '../state'; +import { bn, CompressedAccountWithMerkleContext, TreeInfo } from '../state'; import { buildAndSignTx, 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 confirmOptions Options for confirming the transaction * * @return Signature of the confirmed transaction */ @@ -39,7 +33,6 @@ export async function transfer( lamports: number | BN, owner: Signer, toAddress: PublicKey, - merkleTree?: PublicKey, confirmOptions?: ConfirmOptions, ): Promise { let accumulatedLamports = bn(0); @@ -53,7 +46,7 @@ export async function transfer( filters: undefined, dataSlice: undefined, cursor, - limit: new BN(batchSize), + limit: bn(batchSize), }; const batch = await rpc.getCompressedAccountsByOwner( @@ -62,7 +55,7 @@ export async function transfer( ); for (const account of batch.items) { - if (account.lamports.gt(new BN(0))) { + if (account.lamports.gt(bn(0))) { compressedAccounts.push(account); accumulatedLamports = accumulatedLamports.add(account.lamports); } @@ -95,12 +88,11 @@ export async function transfer( lamports, recentInputStateRootIndices: proof.rootIndices, recentValidityProof: proof.compressedProof, - outputStateTrees: merkleTree, }); const { blockhash } = await rpc.getLatestBlockhash(); const signedTx = buildAndSignTx( - [ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), ix], + [ComputeBudgetProgram.setComputeUnitLimit({ units: 350_000 }), ix], payer, blockhash, ); diff --git a/js/stateless.js/src/constants.ts b/js/stateless.js/src/constants.ts index ac64ecbc68..4f58008a76 100644 --- a/js/stateless.js/src/constants.ts +++ b/js/stateless.js/src/constants.ts @@ -1,7 +1,25 @@ import BN from 'bn.js'; import { Buffer } from 'buffer'; import { ConfirmOptions, PublicKey } from '@solana/web3.js'; -import { ActiveTreeBundle, TreeType } from './state/types'; +import { TreeInfo, TreeType } from './state/types'; + +/** +/** + * @internal + * Feature flags. Only use if you know what you are doing. + */ +export const featureFlags = { + version: 'V2' as 'V1' | 'V2', + isV2: () => featureFlags.version.toUpperCase() === 'V2', +}; + +/** + * Returns the correct endpoint name for the current API version. E.g. + * versionedEndpoint('getCompressedAccount') -> 'getCompressedAccount' (V1) + * or 'getCompressedAccountV2' (V2) + */ +export const versionedEndpoint = (base: string) => + featureFlags.version.toUpperCase() === 'V1' ? base : `${base}V2`; export const FIELD_SIZE = new BN( '21888242871839275222246405745257275088548364400416034343698204186575808495617', @@ -32,22 +50,18 @@ export const INSERT_INTO_QUEUES_DISCRIMINATOR = Buffer.from([ 180, 143, 159, 153, 35, 46, 248, 163, ]); -// TODO: implement properly export const noopProgram = 'noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV'; -export const lightProgram = 'SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7'; -export const accountCompressionProgram = // also: merkletree program +export const lightSystemProgram = 'SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7'; +export const accountCompressionProgram = 'compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq'; export const getRegisteredProgramPda = () => - new PublicKey('35hkDgaAKwMCaxRz2ocSZ6NaUrtKkyNqU6c4RV3tYJRh'); // TODO: better labelling. gov authority pda + new PublicKey('35hkDgaAKwMCaxRz2ocSZ6NaUrtKkyNqU6c4RV3tYJRh'); export const getAccountCompressionAuthority = () => PublicKey.findProgramAddressSync( [Buffer.from('cpi_authority')], - new PublicKey( - // TODO: can add check to ensure its consistent with the idl - lightProgram, - ), + new PublicKey(lightSystemProgram), )[0]; export const defaultStaticAccounts = () => [ @@ -56,6 +70,7 @@ export const defaultStaticAccounts = () => [ new PublicKey(accountCompressionProgram), new PublicKey(getAccountCompressionAuthority()), ]; + export const defaultStaticAccountsStruct = () => { return { registeredProgramPda: new PublicKey(getRegisteredProgramPda()), @@ -70,7 +85,7 @@ export const defaultStaticAccountsStruct = () => { export type StateTreeLUTPair = { stateTreeLookupTable: PublicKey; - nullifyTable: PublicKey; + nullifyLookupTable: PublicKey; }; /** @@ -86,7 +101,7 @@ export const defaultStateTreeLookupTables = (): { stateTreeLookupTable: new PublicKey( stateTreeLookupTableMainnet, ), - nullifyTable: new PublicKey( + nullifyLookupTable: new PublicKey( nullifiedStateTreeLookupTableMainnet, ), }, @@ -94,7 +109,7 @@ export const defaultStateTreeLookupTables = (): { devnet: [ { stateTreeLookupTable: new PublicKey(stateTreeLookupTableDevnet), - nullifyTable: new PublicKey( + nullifyLookupTable: new PublicKey( nullifiedStateTreeLookupTableDevnet, ), }, @@ -112,26 +127,48 @@ export const isLocalTest = (url: string) => { /** * @internal */ -export const localTestActiveStateTreeInfo = (): ActiveTreeBundle[] => { +export const localTestActiveStateTreeInfo = (): TreeInfo[] => { return [ { tree: new PublicKey(merkletreePubkey), queue: new PublicKey(nullifierQueuePubkey), cpiContext: new PublicKey(cpiContextPubkey), - treeType: TreeType.State, + treeType: TreeType.StateV1, + nextTreeInfo: null, }, { tree: new PublicKey(merkleTree2Pubkey), queue: new PublicKey(nullifierQueue2Pubkey), cpiContext: new PublicKey(cpiContext2Pubkey), - treeType: TreeType.State, + treeType: TreeType.StateV1, + nextTreeInfo: null, }, - ]; + { + tree: new PublicKey(batchMerkleTree), + queue: new PublicKey(batchQueue), + cpiContext: PublicKey.default, + treeType: TreeType.StateV2, + nextTreeInfo: null, + }, + ].filter(info => + featureFlags.isV2() ? true : info.treeType === TreeType.StateV1, + ); }; +export const getDefaultAddressTreeInfo = () => { + return { + tree: new PublicKey(addressTree), + queue: new PublicKey(addressQueue), + cpiContext: null, + treeType: TreeType.AddressV1, + nextTreeInfo: null, + }; +}; /** + * @deprecated use {@link rpc.getStateTreeInfos} 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 getAllStateTreeInfos}. */ export const defaultTestStateTreeAccounts = () => { return { @@ -153,15 +190,18 @@ export const defaultTestStateTreeAccounts2 = () => { }; }; +export const COMPRESSED_TOKEN_PROGRAM_ID = new PublicKey( + 'cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m', +); export const stateTreeLookupTableMainnet = '7i86eQs3GSqHjN47WdWLTCGMW6gde1q96G2EVnUyK2st'; export const nullifiedStateTreeLookupTableMainnet = 'H9QD4u1fG7KmkAzn2tDXhheushxFe1EcrjGGyEFXeMqT'; export const stateTreeLookupTableDevnet = - '8n8rH2bFRVA6cSGNDpgqcKHCndbFCT1bXxAQG89ejVsh'; + 'Dk9mNkbiZXJZ4By8DfSP6HEE4ojZzRvucwpawLeuwq8q'; // '8n8rH2bFRVA6cSGNDpgqcKHCndbFCT1bXxAQG89ejVsh'; export const nullifiedStateTreeLookupTableDevnet = - '5dhaJLBjnVBQFErr8oiCJmcVsx3Zj6xDekGB2zULPsnP'; + 'AXbHzp1NgjLvpfnD6JRTTovXZ7APUCdtWZFCRr5tCxse'; // '5dhaJLBjnVBQFErr8oiCJmcVsx3Zj6xDekGB2zULPsnP'; export const nullifierQueuePubkey = 'nfq1NvQDJ2GEgnS8zt9prAe8rjjpAW1zFkrvZoBR148'; @@ -176,6 +216,10 @@ export const nullifierQueue2Pubkey = 'nfq2hgS7NYemXsFaFUCe3EMXSDSfnZnAe27jC6aPP1X'; export const cpiContext2Pubkey = 'cpi2cdhkH5roePvcudTgUL8ppEBfTay1desGh8G8QxK'; +// V2 testing. +export const batchMerkleTree = 'HLKs5NJ8FXkJg8BrzJt56adFYYuwg5etzDtBbQYTsixu'; // v2 merkle tree (includes nullifier queue) +export const batchQueue = '6L7SzhYB3anwEQ9cphpJ1U7Scwj57bx2xueReg7R9cKU'; // v2 output queue + export const confirmConfig: ConfirmOptions = { commitment: 'confirmed', preflightCommitment: 'confirmed', @@ -202,7 +246,9 @@ export const TRANSACTION_MERKLE_TREE_ROLLOVER_THRESHOLD = new BN( * * Is charged per output compressed account. */ -export const STATE_MERKLE_TREE_ROLLOVER_FEE = new BN(300); +export const STATE_MERKLE_TREE_ROLLOVER_FEE = featureFlags.isV2() + ? new BN(1) + : new BN(300); /** * Fee to provide continous funding for the address queue and address Merkle tree. @@ -211,7 +257,9 @@ export const STATE_MERKLE_TREE_ROLLOVER_FEE = new BN(300); * * Is charged per newly created address. */ -export const ADDRESS_QUEUE_ROLLOVER_FEE = new BN(392); +export const ADDRESS_QUEUE_ROLLOVER_FEE = featureFlags.isV2() + ? new BN(392) + : new BN(392); /** * Is charged if the transaction nullifies at least one compressed account. diff --git a/js/stateless.js/src/index.ts b/js/stateless.js/src/index.ts index e6c43e9623..847d0127f9 100644 --- a/js/stateless.js/src/index.ts +++ b/js/stateless.js/src/index.ts @@ -1,11 +1,9 @@ export * from './actions'; -export * from './instruction'; export * from './programs'; export * from './state'; +export * from './test-helpers'; export * from './utils'; export * from './constants'; export * from './errors'; export * from './rpc-interface'; export * from './rpc'; -export * from './test-helpers'; -export { LightSystemProgram as LightSystemProgramIDL, IDL } from './idl'; diff --git a/js/stateless.js/src/instruction/index.ts b/js/stateless.js/src/instruction/index.ts deleted file mode 100644 index 15194e2e3a..0000000000 --- a/js/stateless.js/src/instruction/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './pack-compressed-accounts'; diff --git a/js/stateless.js/src/programs/index.ts b/js/stateless.js/src/programs/index.ts index 6d5d8dd768..4b58cda615 100644 --- a/js/stateless.js/src/programs/index.ts +++ b/js/stateless.js/src/programs/index.ts @@ -1,2 +1 @@ export * from './system'; -export * from './layout'; diff --git a/js/stateless.js/src/idl.ts b/js/stateless.js/src/programs/system/idl.ts similarity index 97% rename from js/stateless.js/src/idl.ts rename to js/stateless.js/src/programs/system/idl.ts index d678680478..1c9a4f1f07 100644 --- a/js/stateless.js/src/idl.ts +++ b/js/stateless.js/src/programs/system/idl.ts @@ -698,7 +698,7 @@ export type LightSystemProgram = { type: 'u8'; }, { - name: 'nullifierQueuePubkeyIndex'; + name: 'queuePubkeyIndex'; type: 'u8'; }, { @@ -706,30 +706,8 @@ export type LightSystemProgram = { type: 'u32'; }, { - name: 'queueIndex'; - type: { - option: { - defined: 'QueueIndex'; - }; - }; - }, - ]; - }; - }, - { - name: 'QueueIndex'; - type: { - kind: 'struct'; - fields: [ - { - name: 'queueId'; - docs: ['Id of queue in queue account.']; - type: 'u8'; - }, - { - name: 'index'; - docs: ['Index of compressed account hash in queue.']; - type: 'u16'; + name: 'proveByIndex'; + type: 'bool'; }, ]; }; @@ -1721,7 +1699,7 @@ export const IDL: LightSystemProgram = { type: 'u8', }, { - name: 'nullifierQueuePubkeyIndex', + name: 'queuePubkeyIndex', type: 'u8', }, { @@ -1729,30 +1707,8 @@ export const IDL: LightSystemProgram = { type: 'u32', }, { - name: 'queueIndex', - type: { - option: { - defined: 'QueueIndex', - }, - }, - }, - ], - }, - }, - { - name: 'QueueIndex', - type: { - kind: 'struct', - fields: [ - { - name: 'queueId', - docs: ['Id of queue in queue account.'], - type: 'u8', - }, - { - name: 'index', - docs: ['Index of compressed account hash in queue.'], - type: 'u16', + name: 'proveByIndex', + type: 'bool', }, ], }, diff --git a/js/stateless.js/src/programs/system/index.ts b/js/stateless.js/src/programs/system/index.ts new file mode 100644 index 0000000000..4a986aab03 --- /dev/null +++ b/js/stateless.js/src/programs/system/index.ts @@ -0,0 +1,5 @@ +export { LightSystemProgram as LightSystemProgramIDL, IDL } from './idl'; +export * from './layout'; +export * from './pack'; +export * from './program'; +export * from './select-compressed-accounts'; diff --git a/js/stateless.js/src/programs/layout.ts b/js/stateless.js/src/programs/system/layout.ts similarity index 91% rename from js/stateless.js/src/programs/layout.ts rename to js/stateless.js/src/programs/system/layout.ts index 66186bc351..1ea5d117e7 100644 --- a/js/stateless.js/src/programs/layout.ts +++ b/js/stateless.js/src/programs/system/layout.ts @@ -15,13 +15,16 @@ import { vecU8, } from '@coral-xyz/borsh'; import { + bn, InstructionDataInvoke, InstructionDataInvokeCpi, PublicTransactionEvent, -} from '../state'; -import { LightSystemProgram } from './system'; -import { INVOKE_CPI_DISCRIMINATOR, INVOKE_DISCRIMINATOR } from '../constants'; -import { BN } from 'bn.js'; +} from '../../state'; +import { LightSystemProgram } from '.'; +import { + INVOKE_CPI_DISCRIMINATOR, + INVOKE_DISCRIMINATOR, +} from '../../constants'; export const CompressedAccountLayout = struct( [ @@ -43,9 +46,9 @@ export const CompressedAccountLayout = struct( export const MerkleContextLayout = struct( [ u8('merkleTreePubkeyIndex'), - u8('nullifierQueuePubkeyIndex'), + u8('queuePubkeyIndex'), u32('leafIndex'), - option(struct([u8('queueId'), u16('index')]), 'queueIndex'), + bool('proveByIndex'), ], 'merkleContext', ); @@ -94,10 +97,14 @@ export function encodeInstructionDataInvoke( ): Buffer { const buffer = Buffer.alloc(1000); const len = InstructionDataInvokeLayout.encode(data, buffer); - const dataBuffer = Buffer.from(buffer.slice(0, len)); + const dataBuffer = Buffer.from(new Uint8Array(buffer.slice(0, len))); const lengthBuffer = Buffer.alloc(4); lengthBuffer.writeUInt32LE(len, 0); - return Buffer.concat([INVOKE_DISCRIMINATOR, lengthBuffer, dataBuffer]); + return Buffer.concat([ + new Uint8Array(INVOKE_DISCRIMINATOR), + new Uint8Array(lengthBuffer), + new Uint8Array(dataBuffer), + ]); } export const InstructionDataInvokeCpiLayout: Layout = @@ -246,14 +253,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; }; @@ -500,7 +534,7 @@ export function convertToPublicTransactionEvent( invokeData?.outputCompressedAccounts[index] ?.compressedAccount.owner || PublicKey.default, ), - lamports: new BN( + lamports: bn( invokeData?.outputCompressedAccounts[index] ?.compressedAccount.lamports || 0, ), @@ -520,9 +554,11 @@ export function convertToPublicTransactionEvent( data: convertByteArray( Buffer.from( - invokeData.outputCompressedAccounts[ - index - ].compressedAccount.data.data, + new Uint8Array( + invokeData.outputCompressedAccounts[ + index + ].compressedAccount.data.data, + ), ), ) ?? [], dataHash: convertByteArray( @@ -538,19 +574,21 @@ export function convertToPublicTransactionEvent( }), ), outputLeafIndices: decoded.output_leaf_indices, - sequenceNumbers: decoded.sequence_numbers.map((sn: any) => ({ - tree_pubkey: new PublicKey(sn.tree_pubkey), - queue_pubkey: new PublicKey(sn.queue_pubkey), - tree_type: new BN(sn.tree_type), - seq: new BN(sn.seq), - })), + sequenceNumbers: decoded.sequence_numbers.map((sn: any) => { + return { + tree_pubkey: sn.tree_pubkey, + queue_pubkey: sn.queue_pubkey, + tree_type: sn.tree_type, + seq: sn.seq, + }; + }), pubkeyArray: remainingAccounts .slice(2) .filter(pk => !pk.equals(PublicKey.default)), isCompress: invokeData?.isCompress || false, - relayFee: invokeData?.relayFee ? new BN(invokeData.relayFee) : null, + relayFee: invokeData?.relayFee ? bn(invokeData.relayFee) : null, compressOrDecompressLamports: invokeData?.compressOrDecompressLamports - ? new BN(invokeData.compressOrDecompressLamports) + ? bn(invokeData.compressOrDecompressLamports) : null, message: null, }; diff --git a/js/stateless.js/src/instruction/pack-compressed-accounts.ts b/js/stateless.js/src/programs/system/pack.ts similarity index 57% rename from js/stateless.js/src/instruction/pack-compressed-accounts.ts rename to js/stateless.js/src/programs/system/pack.ts index 66b6270579..37c749ab88 100644 --- a/js/stateless.js/src/instruction/pack-compressed-accounts.ts +++ b/js/stateless.js/src/programs/system/pack.ts @@ -3,9 +3,12 @@ import { CompressedAccount, OutputCompressedAccountWithPackedContext, PackedCompressedAccountWithMerkleContext, -} from '../state'; -import { CompressedAccountWithMerkleContext } from '../state/compressed-account'; -import { toArray } from '../utils/conversion'; + TreeInfo, + TreeType, +} from '../../state'; +import { CompressedAccountWithMerkleContext } from '../../state/compressed-account'; +import { toArray } from '../../utils/conversion'; +import { featureFlags } from '../../constants'; /** * @internal Finds the index of a PublicKey in an array, or adds it if not @@ -29,52 +32,29 @@ export function getIndexOrAdd( * @internal * Pads output state trees with the 0th state tree of the input state. * - * @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 numberOfOutputCompressedAccounts The number of output compressed - * accounts. - * @param inputCompressedAccountsWithMerkleContext The input compressed accounts - * with merkle context. + * @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 numberOfOutputCompressedAccounts The number of output + * compressed accounts. * * @returns Padded output state trees. */ export function padOutputStateMerkleTrees( - outputStateMerkleTrees: PublicKey[] | PublicKey | undefined, + outputStateMerkleTrees: PublicKey, numberOfOutputCompressedAccounts: number, - inputCompressedAccountsWithMerkleContext: CompressedAccountWithMerkleContext[], ): PublicKey[] { if (numberOfOutputCompressedAccounts <= 0) { return []; } - /// Default: use the 0th state tree of input state for all output accounts - if (outputStateMerkleTrees === undefined) { - if (inputCompressedAccountsWithMerkleContext.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', - ); - } - return new Array(numberOfOutputCompressedAccounts).fill( - inputCompressedAccountsWithMerkleContext[0].merkleTree, - ); - /// Align the number of output state trees with the number of output - /// accounts, and fill up with 0th output state tree - } else { - /// Into array - const treesArray = toArray(outputStateMerkleTrees); - if (treesArray.length >= numberOfOutputCompressedAccounts) { - return treesArray.slice(0, numberOfOutputCompressedAccounts); - } else { - return treesArray.concat( - new Array( - numberOfOutputCompressedAccounts - treesArray.length, - ).fill(treesArray[0]), - ); - } - } + return new Array(numberOfOutputCompressedAccounts).fill( + outputStateMerkleTrees, + ); } export function toAccountMetas(remainingAccounts: PublicKey[]): AccountMeta[] { @@ -98,11 +78,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 +89,7 @@ export function packCompressedAccounts( inputCompressedAccounts: CompressedAccountWithMerkleContext[], inputStateRootIndices: number[], outputCompressedAccounts: CompressedAccount[], - outputStateMerkleTrees?: PublicKey[] | PublicKey, + outputStateTreeInfo?: TreeInfo, remainingAccounts: PublicKey[] = [], ): { packedInputCompressedAccounts: PackedCompressedAccountWithMerkleContext[]; @@ -130,12 +108,12 @@ export function packCompressedAccounts( inputCompressedAccounts.forEach((account, index) => { const merkleTreePubkeyIndex = getIndexOrAdd( _remainingAccounts, - account.merkleTree, + account.treeInfo.tree, ); - const nullifierQueuePubkeyIndex = getIndexOrAdd( + const queuePubkeyIndex = getIndexOrAdd( _remainingAccounts, - account.nullifierQueue, + account.treeInfo.queue, ); packedInputCompressedAccounts.push({ @@ -147,28 +125,45 @@ export function packCompressedAccounts( }, merkleContext: { merkleTreePubkeyIndex, - nullifierQueuePubkeyIndex, + queuePubkeyIndex, leafIndex: account.leafIndex, - queueIndex: null, + proveByIndex: account.proveByIndex, }, rootIndex: inputStateRootIndices[index], readOnly: false, }); }); + if (inputCompressedAccounts.length > 0 && outputStateTreeInfo) { + throw new Error( + 'Cannot specify both input accounts and outputStateTreeInfo', + ); + } - if ( - outputStateMerkleTrees === undefined && - inputCompressedAccounts.length === 0 - ) { + let treeInfo: TreeInfo; + if (inputCompressedAccounts.length > 0) { + treeInfo = inputCompressedAccounts[0].treeInfo; + } else if (outputStateTreeInfo) { + treeInfo = outputStateTreeInfo; + } else { throw new Error( - 'No input compressed accounts nor output state trees provided. Please pass in at least one of the following: outputStateMerkleTree or inputCompressedAccount', + 'Neither input accounts nor outputStateTreeInfo are available', ); } + + // Use next tree if available, otherwise fall back to current tree. + // `nextTreeInfo` always takes precedence. + const activeTreeInfo = treeInfo.nextTreeInfo || treeInfo; + let activeTreeOrQueue = activeTreeInfo.tree; + + if (activeTreeInfo.treeType === TreeType.StateV2) { + if (featureFlags.isV2()) { + activeTreeOrQueue = activeTreeInfo.queue; + } else throw new Error('V2 trees are not supported yet'); + } /// output const paddedOutputStateMerkleTrees = padOutputStateMerkleTrees( - outputStateMerkleTrees, + activeTreeOrQueue, outputCompressedAccounts.length, - inputCompressedAccounts, ); outputCompressedAccounts.forEach((account, index) => { diff --git a/js/stateless.js/src/programs/system.ts b/js/stateless.js/src/programs/system/program.ts similarity index 82% rename from js/stateless.js/src/programs/system.ts rename to js/stateless.js/src/programs/system/program.ts index 401a9117c4..ea8a26e1bc 100644 --- a/js/stateless.js/src/programs/system.ts +++ b/js/stateless.js/src/programs/system/program.ts @@ -8,18 +8,22 @@ import { Buffer } from 'buffer'; import { CompressedAccount, CompressedAccountWithMerkleContext, - CompressedProof, + ValidityProof, InstructionDataInvoke, + TreeInfo, bn, createCompressedAccount, -} from '../state'; -import { packCompressedAccounts, toAccountMetas } from '../instruction'; -import { defaultStaticAccountsStruct } from '../constants'; +} from '../../state'; +import { + packCompressedAccounts, + toAccountMetas, +} from '../../programs/system/pack'; +import { defaultStaticAccountsStruct } from '../../constants'; import { validateSameOwner, validateSufficientBalance, -} from '../utils/validation'; -import { packNewAddressParams, NewAddressParams } from '../utils'; +} from '../../utils/validation'; +import { packNewAddressParams, NewAddressParams } from '../../utils'; import { encodeInstructionDataInvoke, invokeAccountsLayout } from './layout'; export const sumUpLamports = ( @@ -40,21 +44,24 @@ type CreateAccountWithSeedParams = { */ payer: PublicKey; /** - * Address params for the new compressed account + * Address params for the new compressed account. */ newAddressParams: NewAddressParams; + /** + * Address of the new compressed account + */ newAddress: number[]; /** * Recent validity proof proving that there's no existing compressed account * registered with newAccountAddress */ - recentValidityProof: CompressedProof; + recentValidityProof: ValidityProof | null; /** * State tree pubkey. Defaults to a public state tree if unspecified. */ - outputStateTree?: PublicKey; + outputStateTreeInfo?: TreeInfo; /** - * Public key of the program to assign as the owner of the created account + * Public key of the program to assign as the owner of the created account. */ programId?: PublicKey; /** @@ -86,36 +93,25 @@ type TransferParams = { */ inputCompressedAccounts: CompressedAccountWithMerkleContext[]; /** - * Recipient address + * Recipient address. */ toAddress: PublicKey; /** - * amount of lamports to transfer. + * Amount of lamports to transfer. */ lamports: number | BN; /** * The recent state root indices of the input state. The expiry is tied to * the proof. - * - * TODO: Add support for passing recent-values after instruction creation. */ recentInputStateRootIndices: number[]; /** - * The recent validity proof for state inclusion of the input state. It - * expires after n slots. + * The recent validity proof for state inclusion of the input state. Expires + * after n slots. */ - recentValidityProof: CompressedProof; - /** - * The state trees that the tx output should be inserted into. This can be a - * single PublicKey or an array of PublicKey. Defaults to the 0th state tree - * of input state. - */ - outputStateTrees?: PublicKey[] | PublicKey; + recentValidityProof: ValidityProof | null; }; -/// TODO: -/// - add option to compress to another owner -/// - add option to merge with input state /** * Defines the parameters for the transfer method */ @@ -133,10 +129,9 @@ type CompressParams = { */ lamports: number | BN; /** - * The state tree that the tx output should be inserted into. Defaults to a - * public state tree if unspecified. + * The state tree that the tx output should be inserted into. */ - outputStateTree?: PublicKey; + outputStateTreeInfo: TreeInfo; }; /** @@ -162,21 +157,13 @@ type DecompressParams = { /** * The recent state root indices of the input state. The expiry is tied to * the proof. - * - * TODO: Add support for passing recent-values after instruction creation. */ recentInputStateRootIndices: number[]; /** * The recent validity proof for state inclusion of the input state. It * expires after n slots. */ - recentValidityProof: CompressedProof; - /** - * The state trees that the tx output should be inserted into. This can be a - * single PublicKey or an array of PublicKey. Defaults to the 0th state tree - * of input state. - */ - outputStateTree?: PublicKey; + recentValidityProof: ValidityProof | null; }; const SOL_POOL_PDA_SEED = Buffer.from('sol_pool_pda'); @@ -188,7 +175,7 @@ export class LightSystemProgram { constructor() {} /** - * Public key that identifies the CompressedPda program + * The LightSystemProgram program ID. */ static programId: PublicKey = new PublicKey( 'SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7', @@ -228,7 +215,6 @@ export class LightSystemProgram { const outputCompressedAccounts: CompressedAccount[] = [ createCompressedAccount( inputCompressedAccounts[0].owner, - changeLamports, ), createCompressedAccount(toAddress, lamports), @@ -297,15 +283,13 @@ export class LightSystemProgram { /** * Creates instruction to create compressed account with PDA. * Cannot write data. - * - * TODO: support transfer of lamports to the new account. */ static async createAccount({ payer, newAddressParams, newAddress, recentValidityProof, - outputStateTree, + outputStateTreeInfo, inputCompressedAccounts, inputStateRootIndices, lamports, @@ -326,7 +310,9 @@ export class LightSystemProgram { inputCompressedAccounts ?? [], inputStateRootIndices ?? [], outputCompressedAccounts, - outputStateTree, + !inputCompressedAccounts || inputCompressedAccounts.length === 0 + ? outputStateTreeInfo + : undefined, ); const { newAddressParamsPacked, remainingAccounts } = @@ -372,7 +358,6 @@ export class LightSystemProgram { lamports, recentInputStateRootIndices, recentValidityProof, - outputStateTrees, }: TransferParams): Promise { /// Create output state const outputCompressedAccounts = this.createTransferOutputState( @@ -390,7 +375,6 @@ export class LightSystemProgram { inputCompressedAccounts, recentInputStateRootIndices, outputCompressedAccounts, - outputStateTrees, ); /// Encode instruction data @@ -429,12 +413,11 @@ export class LightSystemProgram { * Creates a transaction instruction that transfers compressed lamports from * one owner to another. */ - // TODO: add support for non-fee-payer owner static async compress({ payer, toAddress, lamports, - outputStateTree, + outputStateTreeInfo, }: CompressParams): Promise { /// Create output state lamports = bn(lamports); @@ -453,7 +436,7 @@ export class LightSystemProgram { [], [], [outputCompressedAccount], - outputStateTree, + outputStateTreeInfo, ); /// Encode instruction data @@ -463,7 +446,6 @@ export class LightSystemProgram { packedInputCompressedAccounts, outputCompressedAccounts: packedOutputCompressedAccounts, relayFee: null, - /// TODO: here and on-chain: option or similar. newAddressParams: [], compressOrDecompressLamports: lamports, isCompress: true, @@ -499,7 +481,6 @@ export class LightSystemProgram { lamports, recentInputStateRootIndices, recentValidityProof, - outputStateTree, }: DecompressParams): Promise { /// Create output state lamports = bn(lamports); @@ -518,7 +499,6 @@ export class LightSystemProgram { inputCompressedAccounts, recentInputStateRootIndices, outputCompressedAccounts, - outputStateTree, ); /// Encode instruction data const rawInputs: InstructionDataInvoke = { @@ -550,36 +530,3 @@ export class LightSystemProgram { }); } } - -/** - * Selects the minimal number of compressed SOL accounts for a transfer. - * - * 1. Sorts the accounts by amount in descending order - * 2. Accumulates the amount until it is greater than or equal to the transfer - * amount - */ -export function selectMinCompressedSolAccountsForTransfer( - accounts: CompressedAccountWithMerkleContext[], - transferLamports: BN | number, -): [selectedAccounts: CompressedAccountWithMerkleContext[], total: BN] { - let accumulatedLamports = bn(0); - transferLamports = bn(transferLamports); - - const selectedAccounts: CompressedAccountWithMerkleContext[] = []; - - accounts.sort((a, b) => b.lamports.cmp(a.lamports)); - - for (const account of accounts) { - if (accumulatedLamports.gte(bn(transferLamports))) break; - accumulatedLamports = accumulatedLamports.add(account.lamports); - selectedAccounts.push(account); - } - - if (accumulatedLamports.lt(bn(transferLamports))) { - throw new Error( - `Insufficient balance for transfer. Required: ${transferLamports.toString()}, available: ${accumulatedLamports.toString()}`, - ); - } - - return [selectedAccounts, accumulatedLamports]; -} diff --git a/js/stateless.js/src/programs/system/select-compressed-accounts.ts b/js/stateless.js/src/programs/system/select-compressed-accounts.ts new file mode 100644 index 0000000000..d1eb7b2adb --- /dev/null +++ b/js/stateless.js/src/programs/system/select-compressed-accounts.ts @@ -0,0 +1,38 @@ +import BN from 'bn.js'; + +import { CompressedAccountWithMerkleContext } from '../../state'; + +import { bn } from '../../state'; + +/** + * Selects the minimal number of compressed SOL accounts for a transfer. + * + * 1. Sorts the accounts by amount in descending order + * 2. Accumulates the amount until it is greater than or equal to the transfer + * amount + */ +export function selectMinCompressedSolAccountsForTransfer( + accounts: CompressedAccountWithMerkleContext[], + transferLamports: BN | number, +): [selectedAccounts: CompressedAccountWithMerkleContext[], total: BN] { + let accumulatedLamports = bn(0); + transferLamports = bn(transferLamports); + + const selectedAccounts: CompressedAccountWithMerkleContext[] = []; + + accounts.sort((a, b) => b.lamports.cmp(a.lamports)); + + for (const account of accounts) { + if (accumulatedLamports.gte(bn(transferLamports))) break; + accumulatedLamports = accumulatedLamports.add(account.lamports); + selectedAccounts.push(account); + } + + if (accumulatedLamports.lt(bn(transferLamports))) { + throw new Error( + `Insufficient balance for transfer. Required: ${transferLamports.toString()}, available: ${accumulatedLamports.toString()}`, + ); + } + + return [selectedAccounts, accumulatedLamports]; +} diff --git a/js/stateless.js/src/rpc-interface.ts b/js/stateless.js/src/rpc-interface.ts index f5312d3a63..89376fa00f 100644 --- a/js/stateless.js/src/rpc-interface.ts +++ b/js/stateless.js/src/rpc-interface.ts @@ -13,15 +13,20 @@ import { any, nullable, Struct, + boolean, + optional, } from 'superstruct'; import { BN254, createBN254, - CompressedProof, + ValidityProof, CompressedAccountWithMerkleContext, MerkleContextWithMerkleProof, bn, TokenData, + TreeInfo, + AddressTreeInfo, + CompressedProof, } from './state'; import BN from 'bn.js'; @@ -67,6 +72,34 @@ export interface SignatureWithMetadata { slot: number; } +/** + * Account hash and associated state tree info. + */ +export interface HashWithTreeInfo { + /** + * Account hash. + */ + hash: BN254; + /** + * State tree info. + */ + stateTreeInfo: TreeInfo; +} + +/** + * Address and associated address tree info. + */ +export interface AddressWithTreeInfo { + /** + * Address. + */ + address: BN254; + /** + * Address tree info. + */ + addressTreeInfo: AddressTreeInfo; +} + export interface HashWithTree { hash: BN254; tree: PublicKey; @@ -79,6 +112,11 @@ export interface AddressWithTree { queue: PublicKey; } +export interface AddressWithTreeInfo { + address: BN254; + treeInfo: AddressTreeInfo; +} + export interface CompressedTransaction { compressionInfo: { closedAccounts: { @@ -114,7 +152,46 @@ export interface HexInputsForProver { leaf: string; } -// TODO: Rename Compressed -> ValidityProof +/** + * Validity proof with context. + * + * You can request proofs via `rpc.getValidityProof` or + * `rpc.getValidityProofV0`. + */ +export type ValidityProofWithContext = { + /** + * Validity proof. + */ + compressedProof: ValidityProof | null; + /** + * Roots. + */ + roots: BN[]; + /** + * Root indices. + */ + rootIndices: number[]; + /** + * Leaf indices. + */ + leafIndices: number[]; + /** + * Leaves. + */ + leaves: BN[]; + /** + * Tree infos. + */ + treeInfos: TreeInfo[]; + /** + * Whether to prove by indices. + */ + proveByIndices: boolean[]; +}; + +/** + * @deprecated use {@link ValidityProofWithContext} instead + */ export type CompressedProofWithContext = { compressedProof: CompressedProof; roots: BN[]; @@ -213,9 +290,9 @@ const BNFromStringOrNumber = coerce( if (!Number.isSafeInteger(value)) { throw new Error(`Unsafe integer. Precision loss: ${value}`); } - return new BN(value); // Safe number → BN + return bn(value); // Safe number → BN } - return new BN(value, 10); // String → BN + return bn(value, 10); // String → BN }, ); @@ -293,6 +370,20 @@ export function jsonRpcResultAndContext(value: Struct) { ) as Struct>, null>; } +const NextTreeInfoResultV2 = pick({ + treeType: number(), + tree: PublicKeyFromString, + queue: PublicKeyFromString, + cpiContext: nullable(PublicKeyFromString), +}); +const TreeInfoResultV2 = pick({ + treeType: number(), + tree: PublicKeyFromString, + queue: PublicKeyFromString, + cpiContext: nullable(PublicKeyFromString), + nextTreeContext: optional(nullable(NextTreeInfoResultV2)), +}); + /** * @internal */ @@ -314,6 +405,25 @@ export const CompressedAccountResult = pick({ slotCreated: BNFromStringOrNumber, }); +export const CompressedAccountResultV2 = pick({ + address: nullable(ArrayFromString), + hash: BN254FromString, + data: nullable( + pick({ + data: Base64EncodedCompressedAccountDataResult, + dataHash: BN254FromString, + discriminator: BNFromStringOrNumber, + }), + ), + lamports: BNFromStringOrNumber, + owner: PublicKeyFromString, + leafIndex: number(), + seq: nullable(BNFromStringOrNumber), + slotCreated: BNFromStringOrNumber, + merkleContext: TreeInfoResultV2, + proveByIndex: boolean(), +}); + export const TokenDataResult = pick({ mint: PublicKeyFromString, owner: PublicKeyFromString, @@ -330,6 +440,14 @@ export const CompressedTokenAccountResult = pick({ account: CompressedAccountResult, }); +/** + * @internal + */ +export const CompressedTokenAccountResultV2 = pick({ + tokenData: TokenDataResult, + account: CompressedAccountResultV2, +}); + /** * @internal */ @@ -337,6 +455,13 @@ export const MultipleCompressedAccountsResult = pick({ items: array(CompressedAccountResult), }); +/** + * @internal + */ +export const MultipleCompressedAccountsResultV2 = pick({ + items: array(CompressedAccountResultV2), +}); + /** * @internal */ @@ -345,6 +470,13 @@ export const CompressedAccountsByOwnerResult = pick({ cursor: nullable(string()), }); +/** + * @internal + */ +export const CompressedAccountsByOwnerResultV2 = pick({ + items: array(CompressedAccountResultV2), + cursor: nullable(string()), +}); /** * @internal */ @@ -353,6 +485,14 @@ export const CompressedTokenAccountsByOwnerOrDelegateResult = pick({ cursor: nullable(string()), }); +/** + * @internal + */ +export const CompressedTokenAccountsByOwnerOrDelegateResultV2 = pick({ + items: array(CompressedTokenAccountResultV2), + cursor: nullable(string()), +}); + /** * @internal */ @@ -394,7 +534,7 @@ export const LatestNonVotingSignaturesResultPaginated = pick({ /** * @internal */ -export const MerkeProofResult = pick({ +export const MerkleProofResult = pick({ hash: BN254FromString, leafIndex: number(), merkleTree: PublicKeyFromString, @@ -403,6 +543,19 @@ export const MerkeProofResult = pick({ root: BN254FromString, }); +/** + * @internal + */ +export const MerkleProofResultV2 = pick({ + hash: BN254FromString, + leafIndex: number(), + proof: array(BN254FromString), + root: BN254FromString, + rootSeq: number(), + proveByIndex: boolean(), + treeContext: TreeInfoResultV2, +}); + /** * @internal */ @@ -427,6 +580,14 @@ const CompressedProofResult = pick({ c: array(number()), }); +/** + * @internal + */ +export const RootIndexResultV2 = pick({ + rootIndex: number(), + proveByIndex: boolean(), +}); + /** * @internal */ @@ -441,10 +602,35 @@ export const ValidityProofResult = pick({ // nullifierQueues: array(PublicKeyFromString), }); +const AccountProofInputsResult = pick({ + hash: BN254FromString, + root: BN254FromString, + rootIndex: RootIndexResultV2, + merkleContext: TreeInfoResultV2, + leafIndex: number(), +}); +const AddressProofInputsResult = pick({ + address: BN254FromString, + root: BN254FromString, + rootIndex: number(), + merkleContext: TreeInfoResultV2, +}); + +export const ValidityProofResultV2 = pick({ + compressedProof: nullable(CompressedProofResult), + accounts: array(AccountProofInputsResult), + addresses: array(AddressProofInputsResult), +}); + /** * @internal */ -export const MultipleMerkleProofsResult = array(MerkeProofResult); +export const MultipleMerkleProofsResult = array(MerkleProofResult); + +/** + * @internal + */ +export const MultipleMerkleProofsResultV2 = array(MerkleProofResultV2); /** * @internal @@ -511,6 +697,19 @@ export const SignatureListWithCursorResult = pick({ cursor: nullable(string()), }); +/** + * @internal + */ +const ClosedAccountResultV2 = pick({ + account: CompressedAccountResultV2, + txHash: BN254FromString, + nullifier: BN254FromString, +}); + +/** + * @internal + */ + export const CompressedTransactionResult = pick({ compressionInfo: pick({ closedAccounts: array( @@ -531,6 +730,29 @@ export const CompressedTransactionResult = pick({ transaction: any(), }); +/** + * @internal + */ +export const CompressedTransactionResultV2 = pick({ + compressionInfo: pick({ + closedAccounts: array( + pick({ + account: ClosedAccountResultV2, + optionalTokenData: nullable(TokenDataResult), + }), + ), + openedAccounts: array( + pick({ + account: CompressedAccountResultV2, + optionalTokenData: nullable(TokenDataResult), + }), + ), + }), + /// TODO: add transaction struct + /// https://github.com/solana-labs/solana/blob/27eff8408b7223bb3c4ab70523f8a8dca3ca6645/transaction-status/src/lib.rs#L1061 + transaction: any(), +}); + export interface CompressionApiInterface { getCompressedAccount( address?: BN254, @@ -556,17 +778,17 @@ export interface CompressionApiInterface { getValidityProof( hashes: BN254[], newAddresses: BN254[], - ): Promise; + ): Promise; getValidityProofV0( hashes: HashWithTree[], newAddresses: AddressWithTree[], - ): Promise; + ): Promise; getValidityProofAndRpcContext( hashes: HashWithTree[], newAddresses: AddressWithTree[], - ): Promise>; + ): Promise>; getCompressedAccountsByOwner( owner: PublicKey, diff --git a/js/stateless.js/src/rpc.ts b/js/stateless.js/src/rpc.ts index 8a9d473de7..2424a76c40 100644 --- a/js/stateless.js/src/rpc.ts +++ b/js/stateless.js/src/rpc.ts @@ -9,16 +9,20 @@ import { BalanceResult, CompressedAccountResult, CompressedAccountsByOwnerResult, - CompressedProofWithContext, + CompressedAccountsByOwnerResultV2, + ValidityProofWithContext, CompressedTokenAccountsByOwnerOrDelegateResult, CompressedTransaction, CompressedTransactionResult, + CompressedTransactionResultV2, CompressionApiInterface, GetCompressedTokenAccountsByOwnerOrDelegateOptions, HealthResult, HexInputsForProver, - MerkeProofResult, + MerkleProofResult, + MerkleProofResultV2, MultipleCompressedAccountsResult, + MultipleCompressedAccountsResultV2, NativeBalanceResult, ParsedTokenAccount, SignatureListResult, @@ -29,6 +33,7 @@ import { jsonRpcResult, jsonRpcResultAndContext, ValidityProofResult, + ValidityProofResultV2, NewAddressProofResult, LatestNonVotingSignaturesResult, LatestNonVotingSignatures, @@ -44,6 +49,8 @@ import { TokenBalance, TokenBalanceListResultV2, PaginatedOptions, + CompressedAccountResultV2, + CompressedTokenAccountsByOwnerOrDelegateResultV2, } from './rpc-interface'; import { MerkleContextWithMerkleProof, @@ -54,7 +61,10 @@ import { createCompressedAccountWithMerkleContext, createMerkleContext, TokenData, - CompressedProof, + ValidityProof, + TreeType, + AddressTreeInfo, + CompressedAccount, } from './state'; import { array, create, nullable } from 'superstruct'; import { @@ -62,6 +72,8 @@ import { localTestActiveStateTreeInfo, isLocalTest, defaultStateTreeLookupTables, + versionedEndpoint, + featureFlags, } from './constants'; import BN from 'bn.js'; import { toCamelCase, toHex } from './utils/conversion'; @@ -71,8 +83,11 @@ 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 { + getAllStateTreeInfos, + getStateTreeInfoByPubkey, +} from './utils/get-state-tree-infos'; +import { TreeInfo } from './state/types'; import { validateNumbersForProof } from './utils'; /** @internal */ @@ -100,8 +115,8 @@ async function getCompressedTokenAccountsByOwnerOrDelegate( filterByDelegate: boolean = false, ): Promise> { const endpoint = filterByDelegate - ? 'getCompressedTokenAccountsByDelegate' - : 'getCompressedTokenAccountsByOwner'; + ? versionedEndpoint('getCompressedTokenAccountsByDelegate') + : versionedEndpoint('getCompressedTokenAccountsByOwner'); const propertyToCheck = filterByDelegate ? 'delegate' : 'owner'; const unsafeRes = await rpcRequest(rpc.compressionApiEndpoint, endpoint, { @@ -110,11 +125,22 @@ async function getCompressedTokenAccountsByOwnerOrDelegate( limit: options.limit?.toNumber(), cursor: options.cursor, }); - - const res = create( - unsafeRes, - jsonRpcResultAndContext(CompressedTokenAccountsByOwnerOrDelegateResult), - ); + let res; + if (featureFlags.isV2()) { + res = create( + unsafeRes, + jsonRpcResultAndContext( + CompressedTokenAccountsByOwnerOrDelegateResultV2, + ), + ); + } else { + res = create( + unsafeRes, + jsonRpcResultAndContext( + CompressedTokenAccountsByOwnerOrDelegateResult, + ), + ); + } if ('error' in res) { throw new SolanaJSONRPCError( res.error, @@ -126,24 +152,31 @@ async function getCompressedTokenAccountsByOwnerOrDelegate( } const accounts: ParsedTokenAccount[] = []; - const activeStateTreeInfo = await rpc.getCachedActiveStateTreeInfo(); + const activeStateTreeInfos = await rpc.getStateTreeInfos(); res.result.value.items.map(item => { const _account = item.account; const _tokenData = item.tokenData; - const associatedQueue = getQueueForTree( - activeStateTreeInfo, - _account.tree!, + const tree = featureFlags.isV2() + ? (_account as any).merkleContext.tree + : (_account as any).tree; + + const proveByIndex = featureFlags.isV2() + ? (_account as any).proveByIndex + : false; + const stateTreeInfo = getStateTreeInfoByPubkey( + activeStateTreeInfos, + tree, ); const compressedAccount: CompressedAccountWithMerkleContext = createCompressedAccountWithMerkleContext( createMerkleContext( - _account.tree!, - associatedQueue, - _account.hash.toArray('be', 32), + stateTreeInfo, + _account.hash, _account.leafIndex, + proveByIndex, ), _account.owner, bn(_account.lamports), @@ -185,56 +218,6 @@ async function getCompressedTokenAccountsByOwnerOrDelegate( }; } -/** @internal */ -function buildCompressedAccountWithMaybeTokenData( - accountStructWithOptionalTokenData: any, - activeStateTreeInfo: ActiveTreeBundle[], -): { - account: CompressedAccountWithMerkleContext; - maybeTokenData: TokenData | null; -} { - const compressedAccountResult = accountStructWithOptionalTokenData.account; - const tokenDataResult = - accountStructWithOptionalTokenData.optionalTokenData; - - const associatedQueue = getQueueForTree( - activeStateTreeInfo, - compressedAccountResult.tree!, - ); - const compressedAccount: CompressedAccountWithMerkleContext = - createCompressedAccountWithMerkleContext( - createMerkleContext( - compressedAccountResult.merkleTree, - associatedQueue, - compressedAccountResult.hash.toArray('be', 32), - compressedAccountResult.leafIndex, - ), - compressedAccountResult.owner, - bn(compressedAccountResult.lamports), - compressedAccountResult.data - ? parseAccountData(compressedAccountResult.data) - : undefined, - compressedAccountResult.address || undefined, - ); - - if (tokenDataResult === null) { - return { account: compressedAccount, maybeTokenData: null }; - } - - const parsed: TokenData = { - mint: tokenDataResult.mint, - owner: tokenDataResult.owner, - amount: tokenDataResult.amount, - delegate: tokenDataResult.delegate, - state: ['uninitialized', 'initialized', 'frozen'].indexOf( - tokenDataResult.state, - ), - tlv: null, - }; - - return { account: compressedAccount, maybeTokenData: parsed }; -} - /** * Establish a Compression-compatible JSON RPC connection * @@ -356,7 +339,7 @@ export const proverRequest = async ( params: any = [], log = false, publicInputHash: BN | undefined = undefined, -): Promise => { +): Promise => { let logMsg: string = ''; if (log) { @@ -370,19 +353,17 @@ export const proverRequest = async ( circuitType: 'inclusion', stateTreeHeight: 26, inputCompressedAccounts: params, - // publicInputHash: publicInputHash.toString('hex'), }); } else if (method === 'new-address') { body = JSON.stringify({ circuitType: 'non-inclusion', addressTreeHeight: 26, - // publicInputHash: publicInputHash.toString('hex'), newAddresses: params, }); } else if (method === 'combined') { body = JSON.stringify({ circuitType: 'combined', - // publicInputHash: publicInputHash.toString('hex'), + stateTreeHeight: 26, addressTreeHeight: 26, inputCompressedAccounts: params[0], @@ -427,8 +408,7 @@ export type MerkleContextWithNewAddressProof = { nextIndex: BN; merkleProofHashedIndexedElementLeaf: BN[]; indexHashedIndexedElementLeaf: BN; - merkleTree: PublicKey; - nullifierQueue: PublicKey; + treeInfo: AddressTreeInfo; }; export type NonInclusionJsonStruct = { @@ -498,7 +478,7 @@ function calculateTwoInputsHashChain( throw new Error('Input lengths must match.'); } if (hashesFirst.length === 0) { - return new BN(0); + return bn(0); } let hashChain = lightWasm.poseidonHashBN([ @@ -551,83 +531,92 @@ 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; +export interface NullifierMetadata { + nullifier: BN254; + txHash: BN254; } -/** - * Get the tree for a given queue - * - * @param info - The active state tree addresses - * @param queue - The queue to get the tree for - * @returns The tree for the given queue, or undefined if not found - */ -export function getTreeForQueue( - info: ActiveTreeBundle[], - queue: PublicKey, -): PublicKey { - const index = info.findIndex(q => q.queue?.equals(queue)); - if (index === -1) { - throw new Error( - 'No associated tree found for queue. Please set activeStateTreeInfo with latest Tree accounts. If you use custom state trees, set manually.', - ); - } - if (!info[index].tree) { - throw new Error('Tree must not be null for state tree'); - } - return info[index].tree; +/** @internal */ +function buildCompressedAccountWithMaybeTokenDataFromClosedAccountResultV2( + closedAccountResultV2: any, +): { + account: CompressedAccountWithMerkleContext; + maybeTokenData: TokenData | null; + maybeNullifierMetadata: NullifierMetadata | null; +} { + const v1type = { + account: closedAccountResultV2.account.account, + optionalTokenData: closedAccountResultV2.optionalTokenData, + }; + + const v2NullifierMetadata = { + nullifier: closedAccountResultV2.account.nullifier, + txHash: closedAccountResultV2.account.txHash, + }; + + const x = buildCompressedAccountWithMaybeTokenData(v1type); + const y = { + account: x.account, + maybeTokenData: x.maybeTokenData, + maybeNullifierMetadata: v2NullifierMetadata, + }; + return y; } -/** - * 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; +/** @internal */ +function buildCompressedAccountWithMaybeTokenData( + accountStructWithOptionalTokenData: any, +): { + account: CompressedAccountWithMerkleContext; + maybeTokenData: TokenData | null; } { - const length = info.length; - const index = Math.floor(Math.random() * length); + const compressedAccountResult = accountStructWithOptionalTokenData.account; + const tokenDataResult = + accountStructWithOptionalTokenData.optionalTokenData; + + const compressedAccount: CompressedAccountWithMerkleContext = + createCompressedAccountWithMerkleContext( + createMerkleContext( + compressedAccountResult.treeInfo, + compressedAccountResult.hash.toArray('be', 32), + compressedAccountResult.leafIndex, + compressedAccountResult.proveByIndex, + ), + compressedAccountResult.owner, + bn(compressedAccountResult.lamports), + compressedAccountResult.data + ? parseAccountData(compressedAccountResult.data) + : undefined, + compressedAccountResult.address || undefined, + ); - if (!info[index].queue) { - throw new Error('Queue must not be null for state tree'); + if (tokenDataResult === null) { + return { account: compressedAccount, maybeTokenData: null }; } - return { - tree: info[index].tree, - queue: info[index].queue, + + const parsed: TokenData = { + mint: tokenDataResult.mint, + owner: tokenDataResult.owner, + amount: tokenDataResult.amount, + delegate: tokenDataResult.delegate, + state: ['uninitialized', 'initialized', 'frozen'].indexOf( + tokenDataResult.state, + ), + tlv: null, }; -} + return { account: compressedAccount, maybeTokenData: parsed }; +} /** * */ export class Rpc extends Connection implements CompressionApiInterface { compressionApiEndpoint: string; proverEndpoint: string; - activeStateTreeInfo: ActiveTreeBundle[] | null = null; + allStateTreeInfos: TreeInfo[] | null = null; + lastStateTreeFetchTime: number | null = null; + CACHE_TTL = 1000 * 60 * 60; // 1 hour in ms + fetchPromise: Promise | null = null; constructor( endpoint: string, @@ -641,58 +630,72 @@ export class Rpc extends Connection implements CompressionApiInterface { } /** - * Manually set state tree addresses + * @deprecated Use {@link getStateTreeInfos} instead */ - setStateTreeInfo(info: ActiveTreeBundle[]): void { - this.activeStateTreeInfo = info; - } + async getCachedActiveStateTreeInfos() {} /** - * Get the active state tree addresses from the cluster. - * If not already cached, fetches from the cluster. + * Get a list of all state tree infos. If not already cached, fetches from + * the cluster. */ - async getCachedActiveStateTreeInfo(): Promise { + async getStateTreeInfos(): Promise { if (isLocalTest(this.rpcEndpoint)) { return localTestActiveStateTreeInfo(); } - let info: ActiveTreeBundle[] | null = null; - if (!this.activeStateTreeInfo) { - const { mainnet, devnet } = defaultStateTreeLookupTables(); - try { - info = await getLightStateTreeInfo({ - connection: this, - stateTreeLookupTableAddress: - mainnet[0].stateTreeLookupTable, - nullifyTableAddress: mainnet[0].nullifyTable, - }); - this.activeStateTreeInfo = info; - } catch { - info = await getLightStateTreeInfo({ - connection: this, - stateTreeLookupTableAddress: devnet[0].stateTreeLookupTable, - nullifyTableAddress: devnet[0].nullifyTable, - }); - this.activeStateTreeInfo = info; + // return cached + if (this.allStateTreeInfos && this.lastStateTreeFetchTime) { + const now = Date.now(); + if (now - this.lastStateTreeFetchTime <= this.CACHE_TTL) { + return this.allStateTreeInfos; } } - if (!this.activeStateTreeInfo) { - throw new Error( - `activeStateTreeInfo should not be null ${JSON.stringify( - this.activeStateTreeInfo, - )}`, - ); + + if (this.fetchPromise) { + return this.fetchPromise; } - return this.activeStateTreeInfo!; + let info: TreeInfo[] | undefined; + try { + this.fetchPromise = this.doFetch(); + info = await this.fetchPromise; + this.allStateTreeInfos = info; + this.lastStateTreeFetchTime = Date.now(); + return info; + } finally { + this.fetchPromise = null; + } } /** - * Fetch the latest state tree addresses from the cluster. + * @internal */ - async getLatestActiveStateTreeInfo(): Promise { - this.activeStateTreeInfo = null; - return await this.getCachedActiveStateTreeInfo(); + async doFetch(): Promise { + const { mainnet, devnet } = defaultStateTreeLookupTables(); + + /// Mainnet keys are not available on devnet and vice versa. Chaining + /// the requests lets us get the state tree infos from the correct + /// network. + try { + const res = await getAllStateTreeInfos({ + connection: this, + stateTreeLUTPairs: [mainnet[0]], + }); + return res; + } catch (mainnetError) { + try { + const res = await getAllStateTreeInfos({ + connection: this, + stateTreeLUTPairs: [devnet[0]], + }); + return res; + } catch (devnetError) { + throw new Error( + `Failed to fetch state tree infos from both mainnet and devnet. ` + + `Mainnet error: ${mainnetError}. Devnet error: ${devnetError}`, + ); + } + } } /** @@ -708,18 +711,30 @@ export class Rpc extends Connection implements CompressionApiInterface { if (hash && address) { throw new Error('Only one of hash or address must be provided'); } + const activeStateTreeInfo = await this.getStateTreeInfos(); + const unsafeRes = await rpcRequest( this.compressionApiEndpoint, - 'getCompressedAccount', + versionedEndpoint('getCompressedAccount'), { hash: hash ? encodeBN254toBase58(hash) : undefined, address: address ? encodeBN254toBase58(address) : undefined, }, ); - const res = create( - unsafeRes, - jsonRpcResultAndContext(nullable(CompressedAccountResult)), - ); + + let res; + if (featureFlags.isV2()) { + res = create( + unsafeRes, + jsonRpcResultAndContext(nullable(CompressedAccountResultV2)), + ); + } else { + res = create( + unsafeRes, + jsonRpcResultAndContext(nullable(CompressedAccountResult)), + ); + } + if ('error' in res) { throw new SolanaJSONRPCError( res.error, @@ -730,25 +745,22 @@ export class Rpc extends Connection implements CompressionApiInterface { return null; } - const activeStateTreeInfo = await this.getCachedActiveStateTreeInfo(); - const associatedQueue = getQueueForTree( + const tree = featureFlags.isV2() + ? (res.result.value as any).merkleContext.tree + : (res.result.value as any).tree!; + const stateTreeInfo = getStateTreeInfoByPubkey( activeStateTreeInfo, - res.result.value.tree!, + tree, ); const item = res.result.value; - const account = createCompressedAccountWithMerkleContext( - createMerkleContext( - item.tree!, - associatedQueue, - item.hash.toArray('be', 32), - item.leafIndex, - ), + + return createCompressedAccountWithMerkleContext( + createMerkleContext(stateTreeInfo, item.hash, item.leafIndex), item.owner, bn(item.lamports), item.data ? parseAccountData(item.data) : undefined, item.address || undefined, ); - return account; } /** @@ -786,7 +798,6 @@ export class Rpc extends Connection implements CompressionApiInterface { return bn(res.result.value); } - /// TODO: validate that this is just for sol accounts /** * Fetch the total compressed balance for the specified owner public key */ @@ -821,13 +832,20 @@ export class Rpc extends Connection implements CompressionApiInterface { ): Promise { const unsafeRes = await rpcRequest( this.compressionApiEndpoint, - 'getCompressedAccountProof', + versionedEndpoint('getCompressedAccountProof'), { hash: encodeBN254toBase58(hash) }, ); - const res = create( - unsafeRes, - jsonRpcResultAndContext(MerkeProofResult), - ); + + let res; + if (featureFlags.isV2()) { + res = create( + unsafeRes, + jsonRpcResultAndContext(MerkleProofResultV2), + ); + } else { + res = create(unsafeRes, jsonRpcResultAndContext(MerkleProofResult)); + } + if ('error' in res) { throw new SolanaJSONRPCError( res.error, @@ -839,20 +857,22 @@ export class Rpc extends Connection implements CompressionApiInterface { `failed to get proof for compressed account ${hash.toString()}`, ); } - const activeStateTreeInfo = await this.getCachedActiveStateTreeInfo(); - const associatedQueue = getQueueForTree( - activeStateTreeInfo, - res.result.value.merkleTree, - ); + const activeStateTreeInfo = await this.getStateTreeInfos(); + const tree = featureFlags.isV2() + ? (res.result.value as any).treeContext.tree + : (res.result.value as any).tree!; + const treeInfo = getStateTreeInfoByPubkey(activeStateTreeInfo, tree); const value: MerkleContextWithMerkleProof = { - hash: res.result.value.hash.toArray('be', 32), - merkleTree: res.result.value.merkleTree, - leafIndex: res.result.value.leafIndex, - merkleProof: res.result.value.proof, - nullifierQueue: associatedQueue, // TODO(photon): support nullifierQueue in response. - rootIndex: res.result.value.rootSeq % 2400, - root: res.result.value.root, + hash: bn((res.result.value as any).hash.toArray('be', 32)), + treeInfo, + leafIndex: (res.result.value as any).leafIndex, + merkleProof: (res.result.value as any).proof, + rootIndex: (res.result.value as any).rootSeq % 2400, + root: (res.result.value as any).root, + proveByIndex: featureFlags.isV2() + ? (res.result.value as any).proveByIndex + : false, }; return value; } @@ -866,13 +886,23 @@ export class Rpc extends Connection implements CompressionApiInterface { ): Promise { const unsafeRes = await rpcRequest( this.compressionApiEndpoint, - 'getMultipleCompressedAccounts', + versionedEndpoint('getMultipleCompressedAccounts'), { hashes: hashes.map(hash => encodeBN254toBase58(hash)) }, ); - const res = create( - unsafeRes, - jsonRpcResultAndContext(MultipleCompressedAccountsResult), - ); + + let res; + if (featureFlags.isV2()) { + res = create( + unsafeRes, + jsonRpcResultAndContext(MultipleCompressedAccountsResultV2), + ); + } else { + res = create( + unsafeRes, + jsonRpcResultAndContext(MultipleCompressedAccountsResult), + ); + } + if ('error' in res) { throw new SolanaJSONRPCError( res.error, @@ -884,18 +914,21 @@ 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.getStateTreeInfos(); const accounts: CompressedAccountWithMerkleContext[] = []; - res.result.value.items.map(item => { - const associatedQueue = getQueueForTree( + // TODO: fix type + res.result.value.items.map((item: any) => { + const tree = featureFlags.isV2() + ? item.merkleContext.tree + : item.tree!; + const stateTreeInfo = getStateTreeInfoByPubkey( activeStateTreeInfo, - item.tree!, + tree, ); const account = createCompressedAccountWithMerkleContext( createMerkleContext( - item.tree!, - associatedQueue, - item.hash.toArray('be', 32), + stateTreeInfo, + bn(item.hash.toArray('be', 32)), item.leafIndex, ), item.owner, @@ -918,14 +951,23 @@ export class Rpc extends Connection implements CompressionApiInterface { ): Promise { const unsafeRes = await rpcRequest( this.compressionApiEndpoint, - 'getMultipleCompressedAccountProofs', + versionedEndpoint('getMultipleCompressedAccountProofs'), hashes.map(hash => encodeBN254toBase58(hash)), ); - const res = create( - unsafeRes, - jsonRpcResultAndContext(array(MerkeProofResult)), - ); + let res; + if (featureFlags.isV2()) { + res = create( + unsafeRes, + jsonRpcResultAndContext(array(MerkleProofResultV2)), + ); + } else { + res = create( + unsafeRes, + jsonRpcResultAndContext(array(MerkleProofResult)), + ); + } + if ('error' in res) { throw new SolanaJSONRPCError( res.error, @@ -940,20 +982,24 @@ export class Rpc extends Connection implements CompressionApiInterface { const merkleProofs: MerkleContextWithMerkleProof[] = []; - const activeStateTreeInfo = await this.getCachedActiveStateTreeInfo(); + const treeInfos = await this.getStateTreeInfos(); for (const proof of res.result.value) { - const associatedQueue = getQueueForTree( - activeStateTreeInfo, - proof.merkleTree, + const treeInfo = getStateTreeInfoByPubkey( + treeInfos, + featureFlags.isV2() + ? (proof as any).treeContext.tree + : (proof as any).tree!, ); const value: MerkleContextWithMerkleProof = { - hash: proof.hash.toArray('be', 32), - merkleTree: proof.merkleTree, + hash: bn(proof.hash.toArray('be', 32)), + treeInfo, leafIndex: proof.leafIndex, merkleProof: proof.proof, - nullifierQueue: associatedQueue, rootIndex: proof.rootSeq % 2400, root: proof.root, + proveByIndex: featureFlags.isV2() + ? (proof as any).proveByIndex + : false, }; merkleProofs.push(value); } @@ -970,7 +1016,7 @@ export class Rpc extends Connection implements CompressionApiInterface { ): Promise> { const unsafeRes = await rpcRequest( this.compressionApiEndpoint, - 'getCompressedAccountsByOwner', + versionedEndpoint('getCompressedAccountsByOwner'), { owner: owner.toBase58(), filters: config?.filters || [], @@ -980,10 +1026,18 @@ export class Rpc extends Connection implements CompressionApiInterface { }, ); - const res = create( - unsafeRes, - jsonRpcResultAndContext(CompressedAccountsByOwnerResult), - ); + let res; + if (featureFlags.isV2()) { + res = create( + unsafeRes, + jsonRpcResultAndContext(CompressedAccountsByOwnerResultV2), + ); + } else { + res = create( + unsafeRes, + jsonRpcResultAndContext(CompressedAccountsByOwnerResult), + ); + } if ('error' in res) { throw new SolanaJSONRPCError( @@ -998,19 +1052,19 @@ export class Rpc extends Connection implements CompressionApiInterface { }; } const accounts: CompressedAccountWithMerkleContext[] = []; - const activeStateTreeInfo = await this.getCachedActiveStateTreeInfo(); + const activeStateTreeInfo = await this.getStateTreeInfos(); - res.result.value.items.map(item => { - const associatedQueue = getQueueForTree( + (res.result.value as any).items.map((item: any) => { + const stateTreeInfo = getStateTreeInfoByPubkey( activeStateTreeInfo, - item.tree!, + featureFlags.isV2() ? item.merkleContext.tree : item.tree, ); const account = createCompressedAccountWithMerkleContext( createMerkleContext( - item.tree!, - associatedQueue, - item.hash.toArray('be', 32), + stateTreeInfo, + bn(item.hash.toArray('be', 32)), item.leafIndex, + true, ), item.owner, bn(item.lamports), @@ -1023,7 +1077,7 @@ export class Rpc extends Connection implements CompressionApiInterface { return { items: accounts.sort((a, b) => b.leafIndex - a.leafIndex), - cursor: res.result.value.cursor, + cursor: (res.result.value as any).cursor, }; } @@ -1232,14 +1286,19 @@ export class Rpc extends Connection implements CompressionApiInterface { ): Promise { const unsafeRes = await rpcRequest( this.compressionApiEndpoint, - 'getTransactionWithCompressionInfo', + versionedEndpoint('getTransactionWithCompressionInfo'), { signature }, ); - const res = create( - unsafeRes, - jsonRpcResult(CompressedTransactionResult), - ); + let res; + if (featureFlags.isV2()) { + res = create( + unsafeRes, + jsonRpcResult(CompressedTransactionResultV2), + ); + } else { + res = create(unsafeRes, jsonRpcResult(CompressedTransactionResult)); + } if ('error' in res) { throw new SolanaJSONRPCError(res.error, 'failed to get slot'); @@ -1257,24 +1316,70 @@ export class Rpc extends Connection implements CompressionApiInterface { maybeTokenData: TokenData | null; }[] = []; - const activeStateTreeInfo = await this.getCachedActiveStateTreeInfo(); + const activeStateTreeInfo = await this.getStateTreeInfos(); - res.result.compressionInfo.closedAccounts.map(item => { - closedAccounts.push( - buildCompressedAccountWithMaybeTokenData( - item, + if (featureFlags.isV2()) { + res.result.compressionInfo.closedAccounts.map(item => { + closedAccounts.push( + buildCompressedAccountWithMaybeTokenDataFromClosedAccountResultV2( + item, + ), + ); + }); + res.result.compressionInfo.openedAccounts.map(item => { + openedAccounts.push( + buildCompressedAccountWithMaybeTokenData(item), + ); + }); + } else { + res.result.compressionInfo.closedAccounts.map((item: any) => { + const stateTreeInfo = getStateTreeInfoByPubkey( activeStateTreeInfo, - ), - ); - }); - res.result.compressionInfo.openedAccounts.map(item => { - openedAccounts.push( - buildCompressedAccountWithMaybeTokenData( - item, + item.account.tree, + ); + const account = createCompressedAccountWithMerkleContext( + createMerkleContext( + stateTreeInfo, + bn(item.account.hash.toArray('be', 32)), + item.account.leafIndex, + ), + item.account.owner, + bn(item.account.lamports), + item.account.data + ? parseAccountData(item.account.data) + : undefined, + item.account.address || undefined, + ); + closedAccounts.push({ + account, + maybeTokenData: item.optionalTokenData, + }); + }); + + res.result.compressionInfo.openedAccounts.map((item: any) => { + const stateTreeInfo = getStateTreeInfoByPubkey( activeStateTreeInfo, - ), - ); - }); + item.account.tree, + ); + const account = createCompressedAccountWithMerkleContext( + createMerkleContext( + stateTreeInfo, + bn(item.account.hash.toArray('be', 32)), + item.account.leafIndex, + ), + item.account.owner, + bn(item.account.lamports), + item.account.data + ? parseAccountData(item.account.data) + : undefined, + item.account.address || undefined, + ); + openedAccounts.push({ + account, + maybeTokenData: item.optionalTokenData, + }); + }); + } const calculateTokenBalances = ( accounts: Array<{ @@ -1623,180 +1728,21 @@ export class Rpc extends Connection implements CompressionApiInterface { nextIndex: bn(proof.nextIndex), merkleProofHashedIndexedElementLeaf: proof.proof, indexHashedIndexedElementLeaf: bn(proof.lowElementLeafIndex), - merkleTree: proof.merkleTree, - nullifierQueue: defaultTestStateTreeAccounts().addressQueue, + treeInfo: { + tree: proof.merkleTree, + queue: defaultTestStateTreeAccounts().addressQueue, + treeType: TreeType.AddressV1, + nextTreeInfo: null, + }, }; newAddressProofs.push(_proof); } return newAddressProofs; } - /** - * Advanced usage of getValidityProof: fetches ZKP directly from a custom - * non-rpcprover. Note: This uses the proverEndpoint specified in the - * constructor. For normal usage, please use {@link getValidityProof} - * instead. - * - * Fetch the latest validity proof for (1) compressed accounts specified by - * an array of account hashes. (2) new unique addresses specified by an - * array of addresses. - * - * Validity proofs prove the presence of compressed accounts in state trees - * and the non-existence of addresses in address trees, respectively. They - * enable verification without recomputing the merkle proof path, thus - * lowering verification and data costs. - * - * @param hashes Array of BN254 hashes. - * @param newAddresses Array of BN254 new addresses. - * @returns validity proof with context - */ - async getValidityProofDirect( - hashes: BN254[] = [], - newAddresses: BN254[] = [], - ): Promise { - let validityProof: CompressedProofWithContext; - - if (hashes.length === 0 && newAddresses.length === 0) { - throw new Error( - 'Empty input. Provide hashes and/or new addresses.', - ); - } else if (hashes.length > 0 && newAddresses.length === 0) { - /// inclusion - const merkleProofsWithContext = - await this.getMultipleCompressedAccountProofs(hashes); - const inputs = convertMerkleProofsWithContextToHex( - merkleProofsWithContext, - ); - // const lightWasm = await WasmFactory.getInstance(); - // const publicInputHash = getPublicInputHash( - // merkleProofsWithContext, - // hashes, - // [], - // lightWasm, - // ); - const compressedProof = await proverRequest( - this.proverEndpoint, - 'inclusion', - inputs, - false, - // publicInputHash, - ); - validityProof = { - compressedProof, - roots: merkleProofsWithContext.map(proof => proof.root), - rootIndices: merkleProofsWithContext.map( - proof => proof.rootIndex, - ), - leafIndices: merkleProofsWithContext.map( - proof => proof.leafIndex, - ), - leaves: merkleProofsWithContext.map(proof => bn(proof.hash)), - merkleTrees: merkleProofsWithContext.map( - proof => proof.merkleTree, - ), - nullifierQueues: merkleProofsWithContext.map( - proof => proof.nullifierQueue, - ), - }; - } else if (hashes.length === 0 && newAddresses.length > 0) { - /// new-address - const newAddressProofs: MerkleContextWithNewAddressProof[] = - await this.getMultipleNewAddressProofs(newAddresses); - - const inputs = - convertNonInclusionMerkleProofInputsToHex(newAddressProofs); - // const lightWasm = await WasmFactory.getInstance(); - // const publicInputHash = getPublicInputHash( - // [], - // [], - // newAddressProofs, - // lightWasm, - // ); - const compressedProof = await proverRequest( - this.proverEndpoint, - 'new-address', - inputs, - false, - // publicInputHash, - ); - - validityProof = { - compressedProof, - roots: newAddressProofs.map(proof => proof.root), - rootIndices: newAddressProofs.map(proof => proof.rootIndex), - leafIndices: newAddressProofs.map(proof => - proof.nextIndex.toNumber(), - ), - leaves: newAddressProofs.map(proof => bn(proof.value)), - merkleTrees: newAddressProofs.map(proof => proof.merkleTree), - nullifierQueues: newAddressProofs.map( - proof => proof.nullifierQueue, - ), - }; - } else if (hashes.length > 0 && newAddresses.length > 0) { - /// combined - const merkleProofsWithContext = - await this.getMultipleCompressedAccountProofs(hashes); - const inputs = convertMerkleProofsWithContextToHex( - merkleProofsWithContext, - ); - const newAddressProofs: MerkleContextWithNewAddressProof[] = - await this.getMultipleNewAddressProofs(newAddresses); - - const newAddressInputs = - convertNonInclusionMerkleProofInputsToHex(newAddressProofs); - // const lightWasm = await WasmFactory.getInstance(); - // const publicInputHash = getPublicInputHash( - // merkleProofsWithContext, - // hashes, - // newAddressProofs, - // lightWasm, - // ); - const compressedProof = await proverRequest( - this.proverEndpoint, - 'combined', - [inputs, newAddressInputs], - false, - // publicInputHash, - ); - - validityProof = { - compressedProof, - roots: merkleProofsWithContext - .map(proof => proof.root) - .concat(newAddressProofs.map(proof => proof.root)), - rootIndices: merkleProofsWithContext - .map(proof => proof.rootIndex) - .concat(newAddressProofs.map(proof => proof.rootIndex)), - leafIndices: merkleProofsWithContext - .map(proof => proof.leafIndex) - .concat( - newAddressProofs.map( - proof => proof.nextIndex.toNumber(), // TODO: support >32bit - ), - ), - leaves: merkleProofsWithContext - .map(proof => bn(proof.hash)) - .concat(newAddressProofs.map(proof => bn(proof.value))), - merkleTrees: merkleProofsWithContext - .map(proof => proof.merkleTree) - .concat(newAddressProofs.map(proof => proof.merkleTree)), - nullifierQueues: merkleProofsWithContext - .map(proof => proof.nullifierQueue) - .concat( - newAddressProofs.map(proof => proof.nullifierQueue), - ), - }; - } else throw new Error('Invalid input'); - - return validityProof; - } - /** * @deprecated use {@link getValidityProofV0} instead. * - * - * * Fetch the latest validity proof for (1) compressed accounts specified by * an array of account hashes. (2) new unique addresses specified by an * array of addresses. @@ -1813,12 +1759,11 @@ export class Rpc extends Connection implements CompressionApiInterface { async getValidityProof( hashes: BN254[] = [], newAddresses: BN254[] = [], - ): Promise { + ): Promise { const accs = await this.getMultipleCompressedAccounts(hashes); - const trees = accs.map(acc => acc.merkleTree); - const queues = accs.map(acc => acc.nullifierQueue); + const trees = accs.map(acc => acc.treeInfo.tree); + const queues = accs.map(acc => acc.treeInfo.queue); - // TODO: add dynamic address tree support here const defaultAddressTreePublicKey = defaultTestStateTreeAccounts().addressTree; const defaultAddressQueuePublicKey = @@ -1860,7 +1805,7 @@ export class Rpc extends Connection implements CompressionApiInterface { async getValidityProofV0( hashes: HashWithTree[] = [], newAddresses: AddressWithTree[] = [], - ): Promise { + ): Promise { const { value } = await this.getValidityProofAndRpcContext( hashes, newAddresses, @@ -1887,12 +1832,12 @@ export class Rpc extends Connection implements CompressionApiInterface { async getValidityProofAndRpcContext( hashes: HashWithTree[] = [], newAddresses: AddressWithTree[] = [], - ): Promise> { + ): Promise> { validateNumbersForProof(hashes.length, newAddresses.length); const unsafeRes = await rpcRequest( this.compressionApiEndpoint, - 'getValidityProof', + versionedEndpoint('getValidityProof'), { hashes: hashes.map(({ hash }) => encodeBN254toBase58(hash)), newAddressesWithTrees: newAddresses.map( @@ -1904,37 +1849,71 @@ export class Rpc extends Connection implements CompressionApiInterface { }, ); - const res = create( - unsafeRes, - jsonRpcResultAndContext(ValidityProofResult), - ); + let res; + if (featureFlags.isV2()) { + res = create( + unsafeRes, + jsonRpcResultAndContext(ValidityProofResultV2), + ); + } else { + throw new Error('V1 is not supported'); + res = create( + unsafeRes, + jsonRpcResultAndContext(ValidityProofResult), + ); + } + if ('error' in res) { throw new SolanaJSONRPCError( res.error, - `failed to get ValidityProof for compressed accounts ${hashes.map(hash => hash.toString())}`, + `failed to get validity proof for hashes ${hashes.map(h => h.hash.toString()).join(', ')}`, ); } - - const result = res.result.value; - - if (result === null) { + if (res.result.value === null) { throw new Error( - `failed to get ValidityProof for compressed accounts ${hashes.map(hash => hash.toString())}`, + `failed to get validity proof for hashes ${hashes.map(h => h.hash.toString()).join(', ')}`, ); } - const value: CompressedProofWithContext = { - compressedProof: result.compressedProof, - merkleTrees: result.merkleTrees, - leafIndices: result.leafIndices, - nullifierQueues: [ - ...hashes.map(({ queue }) => queue), - ...newAddresses.map(({ queue }) => queue), - ], - rootIndices: result.rootIndices, - roots: result.roots, - leaves: result.leaves, + const value = res.result.value as any; + return { + value: { + compressedProof: value.compressedProof, + leaves: value.accounts + .map((r: any) => r.hash) + .concat(value.addresses.map((r: any) => r.address)), + roots: value.accounts + .map((r: any) => r.root) + .concat(value.addresses.map((r: any) => r.root)), + rootIndices: value.accounts + .map((r: any) => r.rootIndex.rootIndex) + .concat(value.addresses.map((r: any) => r.rootIndex)), + proveByIndices: value.accounts + .map((r: any) => r.rootIndex.proveByIndex) + .concat(value.addresses.map((r: any) => false)), + treeInfos: value.accounts + .map((r: any) => r.merkleContext) + .concat(value.addresses.map((r: any) => r.merkleContext)), + leafIndices: value.accounts + .map((r: any) => r.leafIndex) + .concat(value.addresses.map((r: any) => 0)), + }, + context: res.result.context, }; - return { value, context: res.result.context }; + // TODO: enable with v1 support. + // return { + // value: { + // compressedProof: value.compressedProof, + // roots: value.roots, + // rootIndices: value.rootIndices.map((r: any) => r.rootIndex), + // leafIndices: value.leafIndices, + // leaves: value.leaves, + // treeInfos: value.merkleContexts, + // proveByIndices: value.rootIndices.map( + // (r: any) => r.proveByIndex, + // ), + // }, + // context: res.result.context, + // }; } } diff --git a/js/stateless.js/src/state/BN254.ts b/js/stateless.js/src/state/BN254.ts index 78c33dff06..f7111ede7d 100644 --- a/js/stateless.js/src/state/BN254.ts +++ b/js/stateless.js/src/state/BN254.ts @@ -1,9 +1,4 @@ -// TODO: consider implementing BN254 as wrapper class around _BN mirroring -// PublicKey this would encapsulate our runtime checks and also enforce -// typesafety at compile time - import { FIELD_SIZE } from '../constants'; -import { PublicKey } from '@solana/web3.js'; import BN from 'bn.js'; import bs58 from 'bs58'; import { Buffer } from 'buffer'; @@ -16,12 +11,6 @@ import { Buffer } from 'buffer'; */ export type BN254 = BN; -export const bn = ( - number: string | number | BN | Buffer | Uint8Array | number[], - base?: number | 'hex' | undefined, - endian?: BN.Endianness | undefined, -): BN => new BN(number, base, endian); - /** Create a bigint instance with <254-bit max size and base58 capabilities */ export const createBN254 = ( number: string | number | BN | Buffer | Uint8Array | number[], @@ -56,5 +45,5 @@ export function encodeBN254toBase58(bigintNumber: BN): string { const bn254 = createBN254(bigintNumber); const bn254Buffer = bn254.toArrayLike(Buffer, undefined, 32); - return bs58.encode(bn254Buffer); + return bs58.encode(new Uint8Array(bn254Buffer)); } diff --git a/js/stateless.js/src/state/bn.ts b/js/stateless.js/src/state/bn.ts new file mode 100644 index 0000000000..b8b69581a9 --- /dev/null +++ b/js/stateless.js/src/state/bn.ts @@ -0,0 +1,12 @@ +import BN from 'bn.js'; +import { Buffer } from 'buffer'; +export const bn = ( + number: string | number | BN | Buffer | Uint8Array | number[], + base?: number | 'hex' | undefined, + endian?: BN.Endianness | undefined, +): BN => { + if (number instanceof Uint8Array && !(number instanceof Buffer)) { + return new BN(Buffer.from(number), base, endian); + } + return new BN(number, base, endian); +}; diff --git a/js/stateless.js/src/state/compressed-account.ts b/js/stateless.js/src/state/compressed-account.ts index 6ba72eb262..a6d7d02a22 100644 --- a/js/stateless.js/src/state/compressed-account.ts +++ b/js/stateless.js/src/state/compressed-account.ts @@ -1,33 +1,106 @@ -import BN from 'bn.js'; import { PublicKey } from '@solana/web3.js'; -import { CompressedAccount, CompressedAccountData } from './types'; -import { BN254, bn } from './BN254'; +import { + CompressedAccount, + CompressedAccountData, + CompressedAccountLegacy, + TreeInfo, +} from './types'; +import BN from 'bn.js'; +import { BN254 } from './BN254'; +import { bn } from './bn'; + +// @deprecated use {@link CompressedAccount} instead +// export type CompressedAccountWithMerkleContext = CompressedAccount & +// MerkleContext & { +// readOnly: boolean; +// }; -export type CompressedAccountWithMerkleContext = CompressedAccount & - MerkleContext & { - readOnly: boolean; - }; +export type CompressedAccountWithMerkleContext = MerkleContext & { + /** + * Public key of program or user owning the account. + */ + owner: PublicKey; + /** + * Lamports attached to the account. + */ + lamports: BN; + /** + * Optional unique account ID that is persistent across transactions. + */ + address: number[] | null; + /** + * Optional data attached to the account. + */ + data: CompressedAccountData | null; +} & { + readOnly: boolean; +}; + +// @deprecated use {@link CompressedAccount} instead +// export type CompressedAccountWithMerkleContextLegacy = CompressedAccount & +// MerkleContextLegacy; /** - * Context for compressed account inserted into a state Merkle tree - * */ -export type MerkleContext = { - /** State Merkle tree */ + * @deprecated use {@link MerkleContext} instead. + * + * Legacy MerkleContext + */ +export type MerkleContextLegacy = { + /** + * State tree + */ merkleTree: PublicKey; - /** The state nullfier queue belonging to merkleTree */ + /** + * Nullifier queue + */ nullifierQueue: PublicKey; - /** Poseidon hash of the utxo preimage. Is a leaf in state merkle tree */ - hash: number[]; // TODO: BN254; - /** 'hash' position within the Merkle tree */ + /** + * Poseidon hash of the account. Stored as leaf in state tree + */ + hash: number[]; + /** + * Position of `hash` in the State tree + */ leafIndex: number; }; +/** + * Context for compressed account stored in a state tree + */ +export type MerkleContext = { + /** + * Tree info + */ + treeInfo: TreeInfo; + /** + * Poseidon hash of the account. Stored as leaf in state tree + */ + hash: BN; + /** + * Position of `hash` in the State tree + */ + leafIndex: number; + /** + * Whether the account can be proven by index or by merkle proof + */ + proveByIndex: boolean; +}; + +/** + * MerkleContext with merkle proof + */ export type MerkleContextWithMerkleProof = MerkleContext & { - /** Recent valid 'hash' proof path, expires after n slots */ + /** + * Recent valid 'hash' proof path, expires after n slots + */ merkleProof: BN254[]; - /** Index of state root the merkleproof is valid for, expires after n slots */ + /** + * Index of state root the merkleproof is valid for, expires after n slots + */ rootIndex: number; - /** Current root */ + /** + * Current root + */ root: BN254; }; @@ -56,13 +129,13 @@ export const createCompressedAccountWithMerkleContext = ( }); export const createMerkleContext = ( - merkleTree: PublicKey, - nullifierQueue: PublicKey, - hash: number[], // TODO: BN254, + treeInfo: TreeInfo, + hash: BN254, leafIndex: number, + proveByIndex: boolean = false, ): MerkleContext => ({ - merkleTree, - nullifierQueue, + treeInfo, hash, leafIndex, + proveByIndex, }); diff --git a/js/stateless.js/src/state/index.ts b/js/stateless.js/src/state/index.ts index 975faa8435..ad869c8175 100644 --- a/js/stateless.js/src/state/index.ts +++ b/js/stateless.js/src/state/index.ts @@ -1,3 +1,4 @@ export * from './BN254'; +export * from './bn'; export * from './compressed-account'; export * from './types'; diff --git a/js/stateless.js/src/state/types.ts b/js/stateless.js/src/state/types.ts index 1717951314..f2e7a3c070 100644 --- a/js/stateless.js/src/state/types.ts +++ b/js/stateless.js/src/state/types.ts @@ -7,45 +7,154 @@ 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, } +/** + * @deprecated Use {@link TreeInfo} instead. + * + * A bundle of active trees for a given tree type. + */ export type ActiveTreeBundle = { + /** + * Tree. + */ tree: PublicKey; + /** + * Queue. + */ queue: PublicKey | null; + /** + * CPI context. + */ cpiContext: PublicKey | null; + /** + * Tree type. + */ treeType: TreeType; }; +/** + * @deprecated Use {@link TreeInfo} instead. + * + * State tree info, versioned via {@link TreeType}. The protocol + * stores compressed accounts in state trees. + */ +export type StateTreeInfo = TreeInfo; + +/** + * Tree info, versioned via {@link TreeType}. The protocol + * stores compressed accounts in state trees, and PDAs in address trees. + * + * Onchain Accounts are subject to Solana's write-lock limits. + * + * To load balance transactions, use {@link selectStateTreeInfo} to + * randomly select a tree from a range of active trees. + * + * Example: + * ```typescript + * const infos = await rpc.getStateTreeInfos(); + * const info = selectStateTreeInfo(infos); + * const ix = await CompressedTokenProgram.compress({ + * // ... + * outputStateTreeInfo: info + * }); + * ``` + */ +export type TreeInfo = { + /** + * Pubkey of the tree account. + */ + tree: PublicKey; + /** + * Pubkey of the queue account associated with the tree. + */ + queue: PublicKey; + /** + * The type of tree. One of {@link TreeType}. + */ + treeType: TreeType; + /** + * Optional compressed cpi context account. + */ + cpiContext?: PublicKey; + /** + * Next tree info. Is `some` if the next tree should be used for the next + * state transition. + */ + nextTreeInfo: TreeInfo | null; +}; + +/** + * @deprecated Use {@link TreeInfo} instead. + * + * Address tree info, versioned via {@link TreeType}. The protocol + * stores PDAs in address trees. + */ +export type AddressTreeInfo = Omit< + StateTreeInfo, + 'cpiContext' | 'nextTreeInfo' +> & { + /** + * Next tree info. + */ + nextTreeInfo: AddressTreeInfo | null; +}; + +/** + * Packed compressed account with merkle context. + */ export interface PackedCompressedAccountWithMerkleContext { + /** + * Compressed account. + */ compressedAccount: CompressedAccount; + /** + * Merkle context. + */ merkleContext: PackedMerkleContext; - rootIndex: number; // u16 + /** + * Root index. + */ + rootIndex: number; + /** + * Read only. + */ readOnly: boolean; } +/** + * Packed merkle context. + */ export interface PackedMerkleContext { - merkleTreePubkeyIndex: number; // u8 - nullifierQueuePubkeyIndex: number; // u8 - leafIndex: number; // u32 - queueIndex: null | QueueIndex; // Option -} - -export interface QueueIndex { - queueId: number; // u8 - index: number; // u16 + /** + * Merkle tree pubkey index. + */ + merkleTreePubkeyIndex: number; + /** + * Queue pubkey index in remaining accounts. + */ + queuePubkeyIndex: number; + /** + * Leaf index. + */ + leafIndex: number; + /** + * Whether to prove by index or validity proof. + */ + proveByIndex: boolean; } /** @@ -53,109 +162,331 @@ export interface QueueIndex { * compressed account. * */ export interface CompressedAccount { - /** Public key of program or user that owns the account */ + /** + * Public key of program or user owning the account. + */ owner: PublicKey; - /** Lamports attached to the account */ - lamports: BN; // u64 // FIXME: optional /** - * TODO: use PublicKey. Optional unique account ID that is persistent across - * transactions. + * Lamports attached to the account. */ - address: number[] | null; // Option - /** Optional data attached to the account */ - data: CompressedAccountData | null; // Option + lamports: BN; + /** + * Optional unique account ID that is persistent across transactions. + */ + address: number[] | null; + /** + * Optional data attached to the account. + */ + data: CompressedAccountData | null; } /** + * @deprecated Use {@link CompressedAccount} instead. + * * Describe the generic compressed account details applicable to every * compressed account. + * * */ +export interface CompressedAccountLegacy { + /** + * Public key of program or user owning the account. + */ + owner: PublicKey; + /** + * Lamports attached to the account. + */ + lamports: BN; + /** + * Optional unique account ID that is persistent across transactions. + */ + address: number[] | null; + /** + * Optional data attached to the account. + */ + data: CompressedAccountData | null; +} +/** + * Describe the generic compressed account details applicable to every + * compressed account. + */ export interface OutputCompressedAccountWithPackedContext { compressedAccount: CompressedAccount; merkleTreeIndex: number; } +/** + * Describes compressed account data. + */ export interface CompressedAccountData { - discriminator: number[]; // [u8; 8] // TODO: test with uint8Array instead - data: Buffer; // bytes - dataHash: number[]; // [u8; 32] + /** + * 8 bytes. + */ + discriminator: number[]; + /** + * Data. + */ + data: Buffer; + /** + * 32 bytes. + */ + dataHash: number[]; } + +/** + * Merkle tree sequence number. + */ export interface MerkleTreeSequenceNumber { + /** + * Public key. + */ pubkey: PublicKey; + /** + * Sequence number. + */ seq: BN; } +/** + * Public transaction event. + */ export interface PublicTransactionEvent { - inputCompressedAccountHashes: number[][]; // Vec<[u8; 32]> - outputCompressedAccountHashes: number[][]; // Vec<[u8; 32]> + /** + * Input compressed account hashes. + */ + inputCompressedAccountHashes: number[][]; + /** + * Output compressed account hashes. + */ + outputCompressedAccountHashes: number[][]; + /** + * Output compressed accounts. + */ outputCompressedAccounts: OutputCompressedAccountWithPackedContext[]; - outputLeafIndices: number[]; // Vec - sequenceNumbers: MerkleTreeSequenceNumber[]; // Vec - relayFee: BN | null; // Option - isCompress: boolean; // bool - compressOrDecompressLamports: BN | null; // Option - pubkeyArray: PublicKey[]; // Vec - message: Uint8Array | null; // Option + /** + * Output leaf indices. + */ + outputLeafIndices: number[]; + /** + * Sequence numbers. + */ + sequenceNumbers: MerkleTreeSequenceNumber[]; + /** + * Relay fee. Default is null. + */ + relayFee: BN | null; + /** + * Whether it's a compress or decompress instruction. + */ + isCompress: boolean; + /** + * If some, it's either a compress or decompress instruction. + */ + compressOrDecompressLamports: BN | null; + /** + * Public keys. + */ + pubkeyArray: PublicKey[]; + /** + * Message. Default is null. + */ + message: Uint8Array | null; } +/** + * Instruction data for invoke. + */ export interface InstructionDataInvoke { - proof: CompressedProof | null; // Option + /** + * Validity proof. + */ + proof: ValidityProof | null; + /** + * Input compressed accounts with merkle context. + */ inputCompressedAccountsWithMerkleContext: PackedCompressedAccountWithMerkleContext[]; + /** + * Output compressed accounts. + */ outputCompressedAccounts: OutputCompressedAccountWithPackedContext[]; - relayFee: BN | null; // Option - newAddressParams: NewAddressParamsPacked[]; // Vec - compressOrDecompressLamports: BN | null; // Option - isCompress: boolean; // bool + /** + * Relay fee. Default is null. + */ + relayFee: BN | null; + /** + * Params for creating new addresses. + */ + newAddressParams: NewAddressParamsPacked[]; + /** + * If some, it's either a compress or decompress instruction. + */ + compressOrDecompressLamports: BN | null; + /** + * Whether it's a compress or decompress instruction. + */ + isCompress: boolean; } +/** + * Instruction data for invoking a CPI. + */ export interface InstructionDataInvokeCpi { - proof: CompressedProof | null; // Option + /** + * Validity proof. + */ + proof: ValidityProof | null; + /** + * Input compressed accounts with merkle context. + */ inputCompressedAccountsWithMerkleContext: PackedCompressedAccountWithMerkleContext[]; + /** + * Output compressed accounts. + */ outputCompressedAccounts: OutputCompressedAccountWithPackedContext[]; - relayFee: BN | null; // Option - newAddressParams: NewAddressParamsPacked[]; // Vec - compressOrDecompressLamports: BN | null; // Option - isCompress: boolean; // bool + /** + * Relay fee. Default is null. + */ + relayFee: BN | null; + /** + * Params for creating new addresses. + */ + newAddressParams: NewAddressParamsPacked[]; + /** + * If some, it's either a compress or decompress instruction. + */ + compressOrDecompressLamports: BN | null; + /** + * If `compressOrDecompressLamports` is some, whether it's a compress or + * decompress instruction. + */ + isCompress: boolean; + /** + * Optional compressed CPI context. + */ compressedCpiContext: CompressedCpiContext | null; } +/** + * Compressed CPI context. + * + * Use if you want to use a single {@link ValidityProof} to update two + * compressed accounts owned by separate programs. + */ export interface CompressedCpiContext { - /// Is set by the program that is invoking the CPI to signal that is should - /// set the cpi context. - set_context: boolean; - /// Is set to wipe the cpi context since someone could have set it before - /// with unrelated data. - first_set_context: boolean; - /// Index of cpi context account in remaining accounts. - cpi_context_account_index: number; + /** + * Is set by the program that is invoking the CPI to signal that it should + * set the cpi context. + */ + setContext: boolean; + /** + * Is set to wipe the cpi context since someone could have set it before + * with unrelated data. + */ + firstSetContext: boolean; + /** + * Index of cpi context account in remaining accounts. + */ + cpiContextAccountIndex: number; } +/** + * @deprecated Use {@link ValidityProof} instead. + */ export interface CompressedProof { - a: number[]; // [u8; 32] - b: number[]; // [u8; 64] - c: number[]; // [u8; 32] + /** + * 32 bytes. + */ + a: number[]; + /** + * 64 bytes. + */ + b: number[]; + /** + * 32 bytes. + */ + c: number[]; } +/** + * Validity proof. + * + * You can request proofs via `rpc.getValidityProof` or + * `rpc.getValidityProofV0`. + * + * One proof can prove the existence of N compressed accounts or the uniqueness + * of N PDAs. + */ +export interface ValidityProof { + /** + * 32 bytes. + */ + a: number[]; + /** + * 64 bytes. + */ + b: number[]; + /** + * 32 bytes. + */ + c: number[]; +} + +/** + * Packed token data for input compressed accounts. + */ export interface InputTokenDataWithContext { + /** + * Amount of tokens. + */ amount: BN; - delegateIndex: number | null; // Option + /** + * Delegate index. + */ + delegateIndex: number | null; + /** + * Merkle context. + */ merkleContext: PackedMerkleContext; - rootIndex: number; // u16 + /** + * Root index. + */ + rootIndex: number; + /** + * Lamports. + */ lamports: BN | null; + /** + * Tlv. + */ tlv: Buffer | null; } + +/** + * Token data. + */ export type TokenData = { - /// The mint associated with this account + /** + * The mint associated with this account. + */ mint: PublicKey; - /// The owner of this account. + /** + * The owner of this account. + */ owner: PublicKey; - /// The amount of tokens this account holds. + /** + * The amount of tokens this account holds. + */ amount: BN; - /// If `delegate` is `Some` then `delegated_amount` represents - /// the amount authorized by the delegate + /** + * If `delegate` is `Some` then `delegated_amount` represents the amount + * authorized by the delegate. + */ delegate: PublicKey | null; - /// The account's state - state: number; // AccountState_IdlType; - /// TokenExtension tlv + /** + * The account's state. + */ + state: number; + /** + * Token extension tlv. + */ tlv: Buffer | null; }; 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..9d434863b4 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 @@ -1,15 +1,15 @@ import { PublicKey } from '@solana/web3.js'; - import BN from 'bn.js'; import { getParsedEvents } from './get-parsed-events'; -import { defaultTestStateTreeAccounts } from '../../constants'; import { Rpc } from '../../rpc'; import { CompressedAccountWithMerkleContext, bn, MerkleContext, createCompressedAccountWithMerkleContext, + TreeType, } from '../../state'; +import { getStateTreeInfoByPubkey } from '../../utils/get-state-tree-infos'; export async function getCompressedAccountsByOwnerTest( rpc: Rpc, @@ -43,6 +43,7 @@ async function getCompressedAccountsForTest(rpc: Rpc) { const events = (await getParsedEvents(rpc)).reverse(); const allOutputAccounts: CompressedAccountWithMerkleContext[] = []; const allInputAccountHashes: BN[] = []; + const infos = await rpc.getStateTreeInfos(); for (const event of events) { for ( @@ -50,12 +51,20 @@ async function getCompressedAccountsForTest(rpc: Rpc) { index < event.outputCompressedAccounts.length; index++ ) { + const maybeTree = + event.pubkeyArray[ + event.outputCompressedAccounts[index].merkleTreeIndex + ]; + + const treeInfo = getStateTreeInfoByPubkey(infos, maybeTree); + const account = event.outputCompressedAccounts[index]; const merkleContext: MerkleContext = { - merkleTree: defaultTestStateTreeAccounts().merkleTree, - nullifierQueue: defaultTestStateTreeAccounts().nullifierQueue, - hash: event.outputCompressedAccountHashes[index], + treeInfo, + hash: bn(event.outputCompressedAccountHashes[index]), leafIndex: event.outputLeafIndices[index], + // V2 trees always have proveByIndex = true in test-rpc. + proveByIndex: treeInfo.treeType === TreeType.StateV2, }; const withCtx: CompressedAccountWithMerkleContext = createCompressedAccountWithMerkleContext( 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..321eb30fec 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, featureFlags } from '../../constants'; import { Rpc } from '../../rpc'; +import { getStateTreeInfoByPubkey } from '../../utils/get-state-tree-infos'; import { ParsedTokenAccount, WithCursor } from '../../rpc-interface'; import { CompressedAccount, @@ -10,6 +11,7 @@ import { MerkleContext, createCompressedAccountWithMerkleContext, bn, + TreeType, } from '../../state'; import { struct, @@ -21,10 +23,6 @@ import { Layout, } from '@coral-xyz/borsh'; -const tokenProgramId: PublicKey = new PublicKey( - 'cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m', -); - type TokenData = { mint: PublicKey; owner: PublicKey; @@ -48,6 +46,7 @@ export type EventWithParsedTokenTlvData = { inputCompressedAccountHashes: number[][]; outputCompressedAccounts: ParsedTokenAccount[]; }; + /** * Manually parse the compressed token layout for a given compressed account. * @param compressedAccount - The compressed account @@ -55,55 +54,75 @@ 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; const { data } = compressedAccount.data; if (data.length === 0) return null; + if (compressedAccount.owner.toBase58() !== programId.toBase58()) { throw new Error( `Invalid owner ${compressedAccount.owner.toBase58()} for token layout`, ); } - return TokenDataLayout.decode(Buffer.from(data)); + try { + const decoded = TokenDataLayout.decode(Buffer.from(data)); + return decoded; + } catch (error) { + console.error('Decoding error:', error); + throw error; + } } /** * 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.getStateTreeInfos(); const outputHashes = event.outputCompressedAccountHashes; const outputCompressedAccountsWithParsedTokenData: ParsedTokenAccount[] = event.outputCompressedAccounts.map((compressedAccount, i) => { - const merkleContext: MerkleContext = { - merkleTree: + const maybeTree = + pubkeyArray[event.outputCompressedAccounts[i].merkleTreeIndex]; + + const treeInfo = getStateTreeInfoByPubkey(infos, maybeTree); + + if ( + !treeInfo.tree.equals( pubkeyArray[ event.outputCompressedAccounts[i].merkleTreeIndex ], - nullifierQueue: - // FIXME: fix make dynamic - defaultTestStateTreeAccounts().nullifierQueue, - hash: outputHashes[i], + ) && + (featureFlags.isV2() + ? !treeInfo.queue.equals( + pubkeyArray[ + event.outputCompressedAccounts[i].merkleTreeIndex + ], + ) + : true) + ) { + throw new Error('Invalid tree'); + } + const merkleContext: MerkleContext = { + treeInfo, + hash: bn(outputHashes[i]), leafIndex: event.outputLeafIndices[i], + // V2 trees are always proveByIndex in test-rpc. + proveByIndex: treeInfo.treeType === TreeType.StateV2, }; - if (!compressedAccount.compressedAccount.data) throw new Error('No data'); - const parsedData = parseTokenLayoutWithIdl( compressedAccount.compressedAccount, ); - if (!parsedData) throw new Error('Invalid token data'); - const withMerkleContext = createCompressedAccountWithMerkleContext( merkleContext, compressedAccount.compressedAccount.owner, @@ -134,12 +153,12 @@ 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 /// an input compressed account of tx n+m, it is spent const allOutCompressedAccounts = eventsWithParsedTokenTlvData.flatMap( @@ -148,14 +167,12 @@ export async function getCompressedTokenAccounts( const allInCompressedAccountHashes = eventsWithParsedTokenTlvData.flatMap( event => event.inputCompressedAccountHashes, ); + const unspentCompressedAccounts = allOutCompressedAccounts.filter( outputCompressedAccount => !allInCompressedAccountHashes.some(hash => { - return ( - JSON.stringify(hash) === - JSON.stringify( - outputCompressedAccount.compressedAccount.hash, - ) + return bn(hash).eq( + outputCompressedAccount.compressedAccount.hash, ); }), ); @@ -170,14 +187,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 +210,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 +230,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 2a7a78cc0b..34b19af7da 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 @@ -1,10 +1,7 @@ import { - GetVersionedTransactionConfig, - MessageV0, ParsedMessageAccount, ParsedTransactionWithMeta, PublicKey, - VersionedTransactionResponse, } from '@solana/web3.js'; import bs58 from 'bs58'; import { @@ -26,7 +23,7 @@ import { InstructionDataInvoke, PublicTransactionEvent } from '../../state'; import { decodeInstructionDataInvokeCpiWithReadOnly, decodePublicTransactionEvent, -} from '../../programs/layout'; +} from '../../programs/system/layout'; import { Buffer } from 'buffer'; import { convertInvokeCpiWithReadOnlyToInvoke } from '../../utils'; @@ -256,9 +253,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..651ca87a7c 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 @@ -10,7 +10,6 @@ import { getCompressedTokenAccountsByDelegateTest, getCompressedTokenAccountsByOwnerTest, } from './get-compressed-token-accounts'; - import { MerkleTree } from '../merkle-tree/merkle-tree'; import { getParsedEvents } from './get-parsed-events'; import { @@ -31,7 +30,7 @@ import { WithCursor, } from '../../rpc-interface'; import { - CompressedProofWithContext, + ValidityProofWithContext, CompressionApiInterface, GetCompressedTokenAccountsByOwnerOrDelegateOptions, ParsedTokenAccount, @@ -42,6 +41,7 @@ import { CompressedAccountWithMerkleContext, MerkleContextWithMerkleProof, PublicTransactionEvent, + TreeType, bn, } from '../../state'; import { IndexedArray } from '../merkle-tree'; @@ -51,18 +51,10 @@ import { convertNonInclusionMerkleProofInputsToHex, proverRequest, } from '../../rpc'; -import { ActiveTreeBundle } from '../../state/types'; +import { TreeInfo } from '../../state/types'; +import { getStateTreeInfoByPubkey } from '../../utils/get-state-tree-infos'; export interface TestRpcConfig { - /** - * Address of the state tree to index. Default: public default test state - * tree. - */ - merkleTreeAddress?: PublicKey; - /** - * Nullifier queue associated with merkleTreeAddress - */ - nullifierQueueAddress?: PublicKey; /** * Depth of state tree. Defaults to the public default test state tree depth */ @@ -71,15 +63,6 @@ export interface TestRpcConfig { * Log proof generation time */ log?: boolean; - /** - * Address of the address tree to index. Default: public default test - * address tree. - */ - addressTreeAddress?: PublicKey; - /** - * Address queue associated with addressTreeAddress - */ - addressQueueAddress?: PublicKey; } export type ClientSubscriptionId = number; @@ -110,13 +93,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,34 +103,30 @@ export async function getTestRpc( proverEndpoint, undefined, { - merkleTreeAddress: merkleTreeAddress || defaultAccounts.merkleTree, - nullifierQueueAddress: - nullifierQueueAddress || defaultAccounts.nullifierQueue, - depth: depth || defaultAccounts.merkleTreeHeight, + depth: depth || defaultTestStateTreeAccounts().merkleTreeHeight, log, }, ); } /** - * Simple mock rpc for unit tests that simulates the compression rpc interface. - * Fetches, parses events and builds merkletree on-demand, i.e. it does not persist state. + * Mock RPC for unit tests that simulates the ZK Compression RPC interface. + * Parses events and builds merkletree on-demand. It does not persist state. * Constraints: - * - Can only index 1 merkletree * - Can only index up to 1000 transactions * - * For advanced testing use photon: https://github.com/helius-labs/photon + * For advanced testing use `Rpc` class which uses photon: + * https://github.com/helius-labs/photon */ 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; + allStateTreeInfos: TreeInfo[] | null = null; + lastStateTreeFetchTime: number | null = null; + fetchPromise: Promise | null = null; + CACHE_TTL = 1000 * 60 * 60; // 1 hour /** * Establish a Compression-compatible JSON RPC mock-connection @@ -178,51 +153,26 @@ 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; } /** - * Manually set state tree addresses + * @deprecated Use {@link getStateTreeInfos} instead */ - setStateTreeInfo(info: ActiveTreeBundle[]): void { - this.activeStateTreeInfo = info; - } - + async getCachedActiveStateTreeInfos() {} /** * Returns local test state trees. */ - async getCachedActiveStateTreeInfo(): Promise { + async getStateTreeInfos(): Promise { return localTestActiveStateTreeInfo(); } - - /** - * Returns local test state trees. - */ - async getLatestActiveStateTreeInfo(): Promise { - return localTestActiveStateTreeInfo(); + async doFetch(): Promise { + throw new Error('doFetch not supported in test-rpc'); } /** @@ -298,6 +248,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 +256,22 @@ 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[]; + treeInfo: TreeInfo; + } + > = new Map(); + + const cachedStateTreeInfos = await this.getStateTreeInfos(); + + /// Assign leaves to their respective trees for (const event of events) { for ( let index = 0; @@ -318,52 +279,175 @@ export class TestRpc extends Connection implements CompressionApiInterface { index++ ) { const hash = event.outputCompressedAccountHashes[index]; + const treeOrQueue = + event.pubkeyArray[ + event.outputCompressedAccounts[index].merkleTreeIndex + ]; + + const stateTreeInfo = getStateTreeInfoByPubkey( + cachedStateTreeInfos, + treeOrQueue, + ); - allLeaves.push(hash); - allLeafIndices.push(event.outputLeafIndices[index]); + if (!leavesByTree.has(stateTreeInfo.tree.toBase58())) { + leavesByTree.set(stateTreeInfo.tree.toBase58(), { + leaves: [], + leafIndices: [], + treeInfo: stateTreeInfo, + }); + } + + const treeData = leavesByTree.get( + stateTreeInfo.tree.toBase58(), + ); + if (!treeData) { + throw new Error( + `Tree not found: ${stateTreeInfo.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 (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 (const [treeKey, { leaves, treeInfo }] of leavesByTree.entries()) { + const tree = new PublicKey(treeKey); - /// Validate - merkleProofs.forEach((proof, index) => { - const leafIndex = proof.leafIndex; - const computedHash = tree.elements()[leafIndex]; - const hashArr = bn(computedHash).toArray('be', 32); - if (!hashArr.every((val, index) => val === proof.hash[index])) { + let merkleTree: MerkleTree | undefined; + if (treeInfo.treeType === TreeType.StateV1) { + merkleTree = new MerkleTree( + this.depth, + this.lightWasm, + leaves.map(leaf => bn(leaf).toString()), + ); + } else if (treeInfo.treeType === TreeType.StateV2) { + /// In V2 State trees, The Merkle tree stays empty until the + /// first forester transaction. And since test-rpc is only used + /// for non-forested tests, we must return a tree with + /// zerovalues. + merkleTree = new MerkleTree(32, this.lightWasm, []); + } else { throw new Error( - `Mismatch at index ${index}: expected ${proof.hash.toString()}, got ${hashArr.toString()}`, + `Invalid tree type: ${treeInfo.treeType} in test-rpc.ts`, + ); + } + + for (let i = 0; i < hashes.length; i++) { + const leafIndex = leaves.findIndex(leaf => + bn(leaf).eq(hashes[i]), ); + // const stateTreeInfo = getStateTreeInfoByPubkey( + // cachedStateTreeInfos, + // tree, + // ); + + /// If leaf is part of current tree, return proof + if (leafIndex !== -1) { + if (treeInfo.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: bn(hashes[i].toArray('be', 32)), + treeInfo, + leafIndex, + merkleProof: bnPathElements, + proveByIndex: false, + rootIndex: leaves.length, + root, + }; + + merkleProofsMap.set(hashes[i].toString(), merkleProof); + } else if (treeInfo.treeType === TreeType.StateV2) { + const pathElements = merkleTree._zeros.slice(0, -1); + const bnPathElements = pathElements.map(value => + bn(value), + ); + const root = bn(merkleTree.root()); + + /// get leafIndex from leavesByTree for the given hash + const leafIndex = leavesByTree + .get(tree.toBase58())! + .leafIndices.findIndex(index => + hashes[i].eq( + bn( + leavesByTree.get(tree.toBase58())! + .leaves[index], + ), + ), + ); + + const merkleProof: MerkleContextWithMerkleProof = { + // Hash is 0 for proveByIndex trees in test-rpc. + hash: bn(hashes[i].toArray('be', 32)), + // hash: bn(new Array(32).fill(0)), + treeInfo, + leafIndex, + merkleProof: bnPathElements, + proveByIndex: true, + // Root index is 0 for proveByIndex trees in + // test-rpc. + rootIndex: 0, + root, + }; + + merkleProofsMap.set(hashes[i].toString(), merkleProof); + } + } + } + } + + // Validate proofs + merkleProofsMap.forEach((proof, index) => { + if (proof.treeInfo.treeType === TreeType.StateV1) { + const leafIndex = proof.leafIndex; + const computedHash = leavesByTree.get( + proof.treeInfo.tree.toBase58(), + )!.leaves[leafIndex]; + const hashArr = bn(computedHash); + if (!hashArr.eq(proof.hash)) { + throw new Error( + `Mismatch at index ${index}: expected ${proof.hash.toString()}, got ${hashArr.toString()}`, + ); + } } }); - return merkleProofs; - } + // Ensure all requested hashes belong to the same tree type + const uniqueTreeTypes = new Set( + hashes.map(hash => { + const proof = merkleProofsMap.get(hash.toString()); + if (!proof) { + throw new Error( + `Proof not found for hash: ${hash.toString()}`, + ); + } + return proof.treeInfo.treeType; + }), + ); + if (uniqueTreeTypes.size > 1) { + throw new Error( + 'Requested hashes belong to different tree types (V1/V2)', + ); + } + + // Return proofs in the order of requested hashes + return hashes.map(hash => { + 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 @@ -510,7 +594,7 @@ export class TestRpc extends Connection implements CompressionApiInterface { */ async getTransactionWithCompressionInfo( _signature: string, - ): Promise { + ): Promise { throw new Error('getCompressedTransaction not implemented in test-rpc'); } @@ -624,8 +708,12 @@ export class TestRpc extends Connection implements CompressionApiInterface { nextIndex: bn(lowElement.nextIndex), merkleProofHashedIndexedElementLeaf: bnPathElements, indexHashedIndexedElementLeaf: bn(lowElement.index), - merkleTree: this.addressTreeAddress, - nullifierQueue: this.addressQueueAddress, + treeInfo: { + tree: defaultTestStateTreeAccounts().addressTree, + queue: defaultTestStateTreeAccounts().addressQueue, + treeType: TreeType.AddressV1, + nextTreeInfo: null, + }, }; newAddressProofs.push(proof); } @@ -641,21 +729,6 @@ export class TestRpc extends Connection implements CompressionApiInterface { ); } - /** - * Advanced usage of getValidityProof: fetches ZKP directly from a custom - * non-rpcprover. Note: This uses the proverEndpoint specified in the - * constructor. For normal usage, please use {@link getValidityProof} - * instead. - * - * Note: Use RPC class for forested trees. TestRpc is only for custom - * testing purposes. - */ - async getValidityProofDirect( - hashes: BN254[] = [], - newAddresses: BN254[] = [], - ): Promise { - return this.getValidityProof(hashes, newAddresses); - } /** * @deprecated This method is not available for TestRpc. Please use * {@link getValidityProof} instead. @@ -663,7 +736,7 @@ export class TestRpc extends Connection implements CompressionApiInterface { async getValidityProofAndRpcContext( hashes: HashWithTree[] = [], newAddresses: AddressWithTree[] = [], - ): Promise> { + ): Promise> { if (newAddresses.some(address => !(address instanceof BN))) { throw new Error('AddressWithTree is not supported in test-rpc'); } @@ -689,56 +762,87 @@ export class TestRpc extends Connection implements CompressionApiInterface { async getValidityProof( hashes: BN254[] = [], newAddresses: BN254[] = [], - ): Promise { + ): Promise { if (newAddresses.some(address => !(address instanceof BN))) { throw new Error('AddressWithTree is not supported in test-rpc'); } - let validityProof: CompressedProofWithContext; + let validityProof: ValidityProofWithContext | null; + + const treeInfosUsed: TreeInfo[] = []; if (hashes.length === 0 && newAddresses.length === 0) { throw new Error( 'Empty input. Provide hashes and/or new addresses.', ); } else if (hashes.length > 0 && newAddresses.length === 0) { + for (const hash of hashes) { + const account = await this.getCompressedAccount( + undefined, + hash, + ); + + if (account) { + treeInfosUsed.push(account.treeInfo); + } else throw new Error('Account not found'); + } + const hasV1Accounts = treeInfosUsed.some( + info => info.treeType === TreeType.StateV1, + ); + /// inclusion const merkleProofsWithContext = await this.getMultipleCompressedAccountProofs(hashes); - const inputs = convertMerkleProofsWithContextToHex( - merkleProofsWithContext, - ); - - // TODO: reactivate to handle proofs of height 32 - // const publicInputHash = getPublicInputHash( - // merkleProofsWithContext, - // hashes, - // [], - // this.lightWasm, - // ); + if (hasV1Accounts) { + const inputs = convertMerkleProofsWithContextToHex( + merkleProofsWithContext, + ); - const compressedProof = await proverRequest( - this.proverEndpoint, - 'inclusion', - inputs, - this.log, - // publicInputHash, - ); - validityProof = { - compressedProof, - roots: merkleProofsWithContext.map(proof => proof.root), - rootIndices: merkleProofsWithContext.map( - proof => proof.rootIndex, - ), - leafIndices: merkleProofsWithContext.map( - proof => proof.leafIndex, - ), - leaves: merkleProofsWithContext.map(proof => bn(proof.hash)), - merkleTrees: merkleProofsWithContext.map( - proof => proof.merkleTree, - ), - nullifierQueues: merkleProofsWithContext.map( - proof => proof.nullifierQueue, - ), - }; + const compressedProof = await proverRequest( + this.proverEndpoint, + 'inclusion', + inputs, + this.log, + ); + validityProof = { + compressedProof, + roots: merkleProofsWithContext.map(proof => proof.root), + rootIndices: merkleProofsWithContext.map( + proof => proof.rootIndex, + ), + leafIndices: merkleProofsWithContext.map( + proof => proof.leafIndex, + ), + leaves: merkleProofsWithContext.map(proof => + bn(proof.hash), + ), + treeInfos: merkleProofsWithContext.map( + proof => proof.treeInfo, + ), + proveByIndices: merkleProofsWithContext.map( + proof => proof.proveByIndex, + ), + }; + } else { + validityProof = { + compressedProof: null, + roots: merkleProofsWithContext.map(_proof => bn(0)), + rootIndices: merkleProofsWithContext.map( + proof => proof.rootIndex, + ), + leafIndices: merkleProofsWithContext.map( + proof => proof.leafIndex, + ), + leaves: merkleProofsWithContext.map(proof => + bn(proof.hash), + ), + treeInfos: merkleProofsWithContext.map( + proof => proof.treeInfo, + ), + proveByIndices: merkleProofsWithContext.map( + proof => proof.proveByIndex, + ), + }; + } } else if (hashes.length === 0 && newAddresses.length > 0) { /// new-address const newAddressProofs: MerkleContextWithNewAddressProof[] = @@ -746,66 +850,68 @@ export class TestRpc extends Connection implements CompressionApiInterface { const inputs = convertNonInclusionMerkleProofInputsToHex(newAddressProofs); - // const publicInputHash = getPublicInputHash( - // [], - // [], - // newAddressProofs, - // this.lightWasm, - // ); + const compressedProof = await proverRequest( this.proverEndpoint, 'new-address', inputs, this.log, - // publicInputHash, ); validityProof = { compressedProof, roots: newAddressProofs.map(proof => proof.root), - // TODO(crank): make dynamic to enable forester support in - // test-rpc.ts. Currently this is a static root because the - // address tree doesn't advance. rootIndices: newAddressProofs.map(_ => 3), leafIndices: newAddressProofs.map(proof => proof.indexHashedIndexedElementLeaf.toNumber(), ), leaves: newAddressProofs.map(proof => bn(proof.value)), - merkleTrees: newAddressProofs.map(proof => proof.merkleTree), - nullifierQueues: newAddressProofs.map( - proof => proof.nullifierQueue, - ), + treeInfos: newAddressProofs.map(proof => proof.treeInfo), + proveByIndices: newAddressProofs.map(_ => false), }; } else if (hashes.length > 0 && newAddresses.length > 0) { /// combined const merkleProofsWithContext = await this.getMultipleCompressedAccountProofs(hashes); - const inputs = convertMerkleProofsWithContextToHex( - merkleProofsWithContext, - ); const newAddressProofs: MerkleContextWithNewAddressProof[] = await this.getMultipleNewAddressProofs(newAddresses); + const treeInfosUsed = merkleProofsWithContext.map( + proof => proof.treeInfo, + ); + const hasV1Accounts = treeInfosUsed.some( + info => info.treeType === TreeType.StateV1, + ); + const newAddressInputs = convertNonInclusionMerkleProofInputsToHex(newAddressProofs); - // const publicInputHash = getPublicInputHash( - // merkleProofsWithContext, - // hashes, - // newAddressProofs, - // this.lightWasm, - // ); - const compressedProof = await proverRequest( - this.proverEndpoint, - 'combined', - [inputs, newAddressInputs], - this.log, - // publicInputHash, - ); + + let compressedProof; + if (hasV1Accounts) { + const inputs = convertMerkleProofsWithContextToHex( + merkleProofsWithContext, + ); + + compressedProof = await proverRequest( + this.proverEndpoint, + 'combined', + [inputs, newAddressInputs], + true, + ); + } else { + // Still need to make the prover request for new addresses + compressedProof = await proverRequest( + this.proverEndpoint, + 'new-address', + newAddressInputs, + true, + ); + } validityProof = { compressedProof, roots: merkleProofsWithContext - .map(proof => proof.root) + .map(proof => (!hasV1Accounts ? bn(0) : proof.root)) // TODO: find better solution. .concat(newAddressProofs.map(proof => proof.root)), rootIndices: merkleProofsWithContext .map(proof => proof.rootIndex) @@ -816,22 +922,19 @@ export class TestRpc extends Connection implements CompressionApiInterface { leafIndices: merkleProofsWithContext .map(proof => proof.leafIndex) .concat( - newAddressProofs.map( - proof => - proof.indexHashedIndexedElementLeaf.toNumber(), // TODO: support >32bit + newAddressProofs.map(proof => + proof.indexHashedIndexedElementLeaf.toNumber(), ), ), leaves: merkleProofsWithContext .map(proof => bn(proof.hash)) .concat(newAddressProofs.map(proof => bn(proof.value))), - merkleTrees: merkleProofsWithContext - .map(proof => proof.merkleTree) - .concat(newAddressProofs.map(proof => proof.merkleTree)), - nullifierQueues: merkleProofsWithContext - .map(proof => proof.nullifierQueue) - .concat( - newAddressProofs.map(proof => proof.nullifierQueue), - ), + treeInfos: merkleProofsWithContext + .map(proof => proof.treeInfo) + .concat(newAddressProofs.map(proof => proof.treeInfo)), + proveByIndices: merkleProofsWithContext + .map(proof => proof.proveByIndex) + .concat(newAddressProofs.map(_ => false)), }; } else throw new Error('Invalid input'); @@ -841,7 +944,7 @@ export class TestRpc extends Connection implements CompressionApiInterface { async getValidityProofV0( hashes: HashWithTree[] = [], newAddresses: AddressWithTree[] = [], - ): Promise { + ): Promise { /// TODO(swen): add support for custom trees return this.getValidityProof( hashes.map(hash => hash.hash), diff --git a/js/stateless.js/src/utils/address.ts b/js/stateless.js/src/utils/address.ts index 659bfefd45..7d4a6ce074 100644 --- a/js/stateless.js/src/utils/address.ts +++ b/js/stateless.js/src/utils/address.ts @@ -1,7 +1,7 @@ -import { AccountMeta, PublicKey } from '@solana/web3.js'; +import { PublicKey } from '@solana/web3.js'; import { hashToBn254FieldSizeBe, hashvToBn254FieldSizeBe } from './conversion'; import { defaultTestStateTreeAccounts } from '../constants'; -import { getIndexOrAdd } from '../instruction'; +import { getIndexOrAdd } from '../programs/system/pack'; export function deriveAddressSeed( seeds: Uint8Array[], diff --git a/js/stateless.js/src/utils/calculate-compute-unit-price.ts b/js/stateless.js/src/utils/calculate-compute-unit-price.ts index 124067af1f..fc44ef53b4 100644 --- a/js/stateless.js/src/utils/calculate-compute-unit-price.ts +++ b/js/stateless.js/src/utils/calculate-compute-unit-price.ts @@ -2,7 +2,7 @@ * @param targetLamports - Target priority fee in lamports * @param computeUnits - Expected compute units used by the transaction * @returns microLamports per compute unit (use in - * `ComputeBudgetProgram.setComputeUnitPrice`) + * {@link https://github.com/solana-foundation/solana-web3.js/blob/maintenance/v1.x/src/programs/compute-budget.ts#L218}) */ export function calculateComputeUnitPrice( targetLamports: number, diff --git a/js/stateless.js/src/utils/conversion.ts b/js/stateless.js/src/utils/conversion.ts index 56e16d614d..36f0990c2d 100644 --- a/js/stateless.js/src/utils/conversion.ts +++ b/js/stateless.js/src/utils/conversion.ts @@ -1,5 +1,5 @@ import { Buffer } from 'buffer'; -import { bn, createBN254 } from '../state/BN254'; +import { bn, createBN254 } from '../state'; import { FIELD_SIZE } from '../constants'; import { keccak_256 } from '@noble/hashes/sha3'; import { Keypair, PublicKey } from '@solana/web3.js'; @@ -11,7 +11,6 @@ import { CompressedAccount, OutputCompressedAccountWithPackedContext, PackedMerkleContext, - QueueIndex, } from '../state/types'; import { NewAddressParamsPacked } from './address'; @@ -140,12 +139,10 @@ export function convertInvokeCpiWithReadOnlyToInvoke( const merkleContext: PackedMerkleContext = { merkleTreePubkeyIndex: account.packedMerkleContext.merkle_tree_pubkey_index, - nullifierQueuePubkeyIndex: + queuePubkeyIndex: account.packedMerkleContext.queue_pubkey_index, leafIndex: account.packedMerkleContext.leaf_index, - queueIndex: account.packedMerkleContext.prove_by_index - ? ({ queueId: 0, index: 0 } as QueueIndex) - : null, + proveByIndex: account.packedMerkleContext.prove_by_index, }; return { 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..f77794aecc --- /dev/null +++ b/js/stateless.js/src/utils/get-state-tree-infos.ts @@ -0,0 +1,213 @@ +import { Connection, PublicKey } from '@solana/web3.js'; +import { TreeInfo, TreeType } from '../state/types'; +import { featureFlags, StateTreeLUTPair } from '../constants'; + +/** + * @deprecated use {@link getTreeInfoByPubkey} instead + */ +export function getStateTreeInfoByPubkey( + treeInfos: TreeInfo[], + treeOrQueue: PublicKey, +): TreeInfo { + return getTreeInfoByPubkey(treeInfos, treeOrQueue); +} + +export function getTreeInfoByPubkey( + treeInfos: TreeInfo[], + treeOrQueue: PublicKey, +): TreeInfo { + const treeInfo = treeInfos.find( + info => info.tree.equals(treeOrQueue) || info.queue.equals(treeOrQueue), + ); + if (!treeInfo) { + throw new Error( + `No associated TreeInfo found for tree or queue. Please set activeStateTreeInfos with latest Tree accounts. If you use custom state trees, set manually. Pubkey: ${treeOrQueue.toBase58()}`, + ); + } + if (!treeInfo.queue) { + throw new Error( + 'Queue must not be null for state tree. Please set activeStateTreeInfos with latest Tree accounts. If you use custom state trees, set manually. Pubkey: ' + + treeOrQueue.toBase58(), + ); + } + + return treeInfo; +} + +/** + * @deprecated use {@link selectStateTreeInfo} instead. + * + * Get a random tree and queue from a set of provided state tree infos. + * + * @param infos Set of state tree infos + * @returns A random tree and queue + */ +export function pickRandomTreeAndQueue(infos: TreeInfo[]): { + tree: PublicKey; + queue: PublicKey; +} { + const length = infos.length; + const index = Math.floor(Math.random() * length); + + let selectedIndex: number; + if (index !== undefined) { + if (index < 0 || index >= infos.length) { + throw new Error( + `Index ${index} out of bounds for infos array of length ${infos.length}`, + ); + } + selectedIndex = index; + } else { + selectedIndex = Math.floor(Math.random() * infos.length); + } + + return infos[selectedIndex]; +} + +const MAX_HOTSPOTS = 5; + +/** + * Select a pseudo-random active state tree info from the set of provided state + * tree infos. + * + * Using this reduces write-lock contention on state trees. + * + * @param infos Set of state tree infos + * + * @param treeType Optional: Only use if you know what you are + * doing. The type of tree. + * @param useMaxConcurrency Optional: Only use if you know what you are + * doing. If true, select from all infos. + * + * @returns A pseudo-randomly selected tree info + */ +export function selectStateTreeInfo( + infos: TreeInfo[], + treeType: TreeType = featureFlags.isV2() + ? TreeType.StateV2 + : TreeType.StateV1, + useMaxConcurrency: boolean = false, +): TreeInfo { + const activeInfos = infos.filter(t => !t.nextTreeInfo); + const filteredInfos = activeInfos.filter(t => t.treeType === treeType); + + if (filteredInfos.length === 0) { + throw new Error( + 'No active state tree infos found for the specified tree type', + ); + } + + const length = useMaxConcurrency + ? filteredInfos.length + : Math.min(MAX_HOTSPOTS, filteredInfos.length); + const index = Math.floor(Math.random() * length); + + if (!filteredInfos[index].queue) { + throw new Error('Queue must not be null for state tree'); + } + + return filteredInfos[index]; +} + +/** + * Get active state tree infos from LUTs. + * + * @param connection The connection to the cluster + * @param stateTreeLUTPairs The state tree lookup table pairs + * + * @returns The active state tree infos + */ +export async function getAllStateTreeInfos({ + connection, + stateTreeLUTPairs, +}: { + connection: Connection; + stateTreeLUTPairs: StateTreeLUTPair[]; +}): Promise { + const stateTreeLookupTablesAndNullifyLookupTables = await Promise.all( + stateTreeLUTPairs.map(async lutPair => { + return { + stateTreeLookupTable: await connection.getAddressLookupTable( + lutPair.stateTreeLookupTable, + ), + nullifyLookupTable: await connection.getAddressLookupTable( + lutPair.nullifyLookupTable, + ), + }; + }), + ); + + const contexts: TreeInfo[] = []; + + for (const { + stateTreeLookupTable, + nullifyLookupTable, + } of stateTreeLookupTablesAndNullifyLookupTables) { + if (!stateTreeLookupTable.value) { + throw new Error('State tree lookup table not found'); + } + + if (!nullifyLookupTable.value) { + throw new Error('Nullify table not found'); + } + + const stateTreePubkeys = stateTreeLookupTable.value.state.addresses; + const nullifyLookupTablePubkeys = + nullifyLookupTable.value.state.addresses; + + if (stateTreePubkeys.length % 3 !== 0) { + throw new Error( + 'State tree lookup table must have a multiple of 3 addresses', + ); + } + + for (let i = 0; i < stateTreePubkeys.length; i += 3) { + const tree = stateTreePubkeys[i]; + const queue = stateTreePubkeys[i + 1]; + const cpiContext = stateTreePubkeys[i + 2]; + let nextTreeInfo: TreeInfo | null = null; + + if (!tree || !queue || !cpiContext) { + throw new Error('Invalid state tree pubkeys structure'); + } + if ( + nullifyLookupTablePubkeys + .map(addr => addr.toBase58()) + .includes(tree.toBase58()) + ) { + // we assign a valid tree later + nextTreeInfo = { + tree: PublicKey.default, + queue: PublicKey.default, + cpiContext: PublicKey.default, + treeType: TreeType.StateV1, + nextTreeInfo: null, + }; + } + contexts.push({ + tree, + queue, + cpiContext, + treeType: TreeType.StateV1, + nextTreeInfo, + }); + } + + /// for each context, check if the tree is in the nullifyLookupTable + for (const context of contexts) { + if (context.nextTreeInfo?.tree.equals(PublicKey.default)) { + const nextAvailableTreeInfo = contexts.find( + ctx => !ctx.nextTreeInfo, + ); + if (!nextAvailableTreeInfo) { + throw new Error( + 'No available tree info found to assign as next tree', + ); + } + context.nextTreeInfo = nextAvailableTreeInfo; + } + } + } + + return contexts; +} diff --git a/js/stateless.js/src/utils/index.ts b/js/stateless.js/src/utils/index.ts index 6a5101897c..4395aeda21 100644 --- a/js/stateless.js/src/utils/index.ts +++ b/js/stateless.js/src/utils/index.ts @@ -1,10 +1,13 @@ export * from './address'; export * from './airdrop'; +export * from './calculate-compute-unit-price'; export * from './conversion'; +export * from './dedupe-signer'; +export * from './get-state-tree-infos'; export * from './parse-validity-proof'; export * from './pipe'; 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 './state-tree-lookup-table'; +export * from './get-state-tree-infos'; diff --git a/js/stateless.js/src/utils/parse-validity-proof.ts b/js/stateless.js/src/utils/parse-validity-proof.ts index 3bce8ef4e2..ed25971d45 100644 --- a/js/stateless.js/src/utils/parse-validity-proof.ts +++ b/js/stateless.js/src/utils/parse-validity-proof.ts @@ -1,6 +1,6 @@ import BN from 'bn.js'; import { FIELD_SIZE } from '../constants'; -import { CompressedProof } from '../state'; +import { bn, ValidityProof } from '../state'; interface GnarkProofJson { ar: string[]; @@ -20,7 +20,7 @@ export const placeholderValidityProof = () => ({ c: Array.from({ length: 32 }, (_, i) => i + 1), }); -export const checkValidityProofShape = (proof: CompressedProof) => { +export const checkValidityProofShape = (proof: ValidityProof) => { if ( proof.a.length !== 32 || proof.b.length !== 64 || @@ -56,13 +56,13 @@ export function proofFromJsonStruct(json: GnarkProofJson): ProofABC { // TODO: add unit test for negation // TODO: test if LE BE issue. unit test -export function negateAndCompressProof(proof: ProofABC): CompressedProof { +export function negateAndCompressProof(proof: ProofABC): ValidityProof { const proofA = proof.a; const proofB = proof.b; const proofC = proof.c; const aXElement = proofA.slice(0, 32); - const aYElement = new BN(proofA.slice(32, 64), 32, 'be'); + const aYElement = bn(proofA.slice(32, 64), 32, 'be'); /// Negate const proofAIsPositive = yElementIsPositiveG1(aYElement) ? false : true; @@ -73,18 +73,18 @@ export function negateAndCompressProof(proof: ProofABC): CompressedProof { const bYElement = proofB.slice(64, 128); const proofBIsPositive = yElementIsPositiveG2( - new BN(bYElement.slice(0, 32), 32, 'be'), - new BN(bYElement.slice(32, 64), 32, 'be'), + bn(bYElement.slice(0, 32), 32, 'be'), + bn(bYElement.slice(32, 64), 32, 'be'), ); bXElement[0] = addBitmaskToByte(bXElement[0], proofBIsPositive); const cXElement = proofC.slice(0, 32); const cYElement = proofC.slice(32, 64); - const proofCIsPositive = yElementIsPositiveG1(new BN(cYElement, 32, 'be')); + const proofCIsPositive = yElementIsPositiveG1(bn(cYElement, 32, 'be')); cXElement[0] = addBitmaskToByte(cXElement[0], proofCIsPositive); - const compressedProof: CompressedProof = { + const compressedProof: ValidityProof = { a: Array.from(aXElement), b: Array.from(bXElement), c: Array.from(cXElement), @@ -95,11 +95,11 @@ export function negateAndCompressProof(proof: ProofABC): CompressedProof { function deserializeHexStringToBeBytes(hexStr: string): Uint8Array { // Using BN for simpler conversion from hex string to byte array - const bn = new BN( + const bN = bn( hexStr.startsWith('0x') ? hexStr.substring(2) : hexStr, 'hex', ); - return new Uint8Array(bn.toArray('be', 32)); + return new Uint8Array(bN.toArray('be', 32)); } function yElementIsPositiveG1(yElement: BN): boolean { @@ -107,7 +107,7 @@ function yElementIsPositiveG1(yElement: BN): boolean { } function yElementIsPositiveG2(yElement1: BN, yElement2: BN): boolean { - const fieldMidpoint = FIELD_SIZE.div(new BN(2)); + const fieldMidpoint = FIELD_SIZE.div(bn(2)); // Compare the first component of the y coordinate if (yElement1.lt(fieldMidpoint)) { 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 53% 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..3edef00eaf 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,128 +130,91 @@ 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 nullifyLookupTableAddress Address of the nullifier lookup table to + * store address in + * @param stateTreeLookupTableAddress lookup table storing all state tree + * addresses + * @param payer Keypair of the payer + * @param authority Keypair of the authority */ export async function nullifyLookupTable({ connection, fullStateTreeAddress, - nullifyTableAddress, + nullifyLookupTableAddress, stateTreeLookupTableAddress, payer, authority, }: { connection: Connection; fullStateTreeAddress: PublicKey; - nullifyTableAddress: PublicKey; + nullifyLookupTableAddress: PublicKey; stateTreeLookupTableAddress: PublicKey; payer: Keypair; authority: Keypair; }): Promise<{ txId: string }> { - // to be nullified address must be part of stateTreeLookupTable set + // to be nullified, the address must be part of stateTreeLookupTable set const stateTreeLookupTable = await connection.getAddressLookupTable( stateTreeLookupTableAddress, ); if (!stateTreeLookupTable.value) { + console.log('stateTreeLookupTable', stateTreeLookupTable); throw new Error('State tree lookup table not found'); } if ( - !stateTreeLookupTable.value.state.addresses.includes( - fullStateTreeAddress, - ) + !stateTreeLookupTable.value.state.addresses + .map(addr => addr.toBase58()) + .includes(fullStateTreeAddress.toBase58()) ) { + console.log('fullStateTreeAddress', fullStateTreeAddress); + console.log( + 'stateTreeLookupTable.value.state.addresses', + stateTreeLookupTable.value.state.addresses, + ); throw new Error( 'State tree address not found in lookup table. Pass correct address or stateTreeLookupTable', ); } - const nullifyTable = - await connection.getAddressLookupTable(nullifyTableAddress); + const nullifyLookupTable = await connection.getAddressLookupTable( + nullifyLookupTableAddress, + ); - if (!nullifyTable.value) { + if (!nullifyLookupTable.value) { throw new Error('Nullify table not found'); } - if (nullifyTable.value.state.addresses.includes(fullStateTreeAddress)) { + if ( + nullifyLookupTable.value.state.addresses + .map(addr => addr.toBase58()) + .includes(fullStateTreeAddress.toBase58()) + ) { throw new Error('Address already exists in nullify lookup table'); } const instructions = AddressLookupTableProgram.extendLookupTable({ payer: payer.publicKey, authority: authority.publicKey, - lookupTable: nullifyTableAddress, + lookupTable: nullifyLookupTableAddress, addresses: [fullStateTreeAddress], }); const blockhash = await connection.getLatestBlockhash(); - const tx = buildAndSignTx([instructions], payer, blockhash.blockhash); - // we pass a Connection type so we don't have to depend on the Rpc module. - // @ts-expect-error - const txId = await sendAndConfirmTx(connection, tx); + const tx = buildAndSignTx( + [instructions], + payer, + blockhash.blockhash, + dedupeSigner(payer as Signer, [authority]), + ); + + const txId = await sendAndConfirmTx(connection as Rpc, tx); return { 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..843db5492c 100644 --- a/js/stateless.js/tests/e2e/compress.test.ts +++ b/js/stateless.js/tests/e2e/compress.test.ts @@ -4,18 +4,19 @@ import { STATE_MERKLE_TREE_NETWORK_FEE, ADDRESS_QUEUE_ROLLOVER_FEE, STATE_MERKLE_TREE_ROLLOVER_FEE, - defaultTestStateTreeAccounts, ADDRESS_TREE_NETWORK_FEE, } from '../../src/constants'; import { newAccountWithLamports } from '../../src/test-helpers/test-utils'; import { Rpc } from '../../src/rpc'; import { LightSystemProgram, + TreeInfo, 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 +66,13 @@ function txFees( describe('compress', () => { let rpc: Rpc; let payer: Signer; + let stateTreeInfo: TreeInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); rpc = await getTestRpc(lightWasm); payer = await newAccountWithLamports(rpc, 1e9, 256); + stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); }); it('should create account with address', async () => { @@ -86,10 +89,28 @@ describe('compress', () => { ], LightSystemProgram.programId, undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); + await expect( + createAccountWithLamports( + rpc as TestRpc, + payer, + [ + new Uint8Array([ + 1, 2, 255, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, + ]), + ], + 0, + LightSystemProgram.programId, + ), + ).rejects.toThrowError( + 'Neither input accounts nor outputStateTreeInfo are available', + ); + + // 0 lamports => 0 input accounts selected, so outputStateTreeInfo is required await createAccountWithLamports( rpc as TestRpc, payer, @@ -102,8 +123,7 @@ describe('compress', () => { 0, LightSystemProgram.programId, undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await createAccount( @@ -117,8 +137,7 @@ describe('compress', () => { ], LightSystemProgram.programId, undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await createAccount( @@ -132,8 +151,7 @@ describe('compress', () => { ], LightSystemProgram.programId, undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); await expect( createAccount( @@ -148,8 +166,7 @@ describe('compress', () => { ], LightSystemProgram.programId, undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ), ).rejects.toThrow(); const postCreateAccountsBalance = await rpc.getBalance(payer.publicKey); @@ -177,7 +194,7 @@ describe('compress', () => { payer, compressLamportsAmount, payer.publicKey, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); const compressedAccounts = await rpc.getCompressedAccountsByOwner( @@ -210,8 +227,6 @@ describe('compress', () => { 100, LightSystemProgram.programId, undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, ); const postCreateAccountBalance = await rpc.getBalance(payer.publicKey); @@ -228,13 +243,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 +282,7 @@ describe('compress', () => { Number(compressedAccounts2.items[0].lamports), compressLamportsAmount - decompressLamportsAmount, ); - await decompress( - rpc, - payer, - 1, - decompressRecipient, - defaultTestStateTreeAccounts().merkleTree, - ); + await decompress(rpc, payer, 1, decompressRecipient); 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..a0a5ef4bad 100644 --- a/js/stateless.js/tests/e2e/layout.test.ts +++ b/js/stateless.js/tests/e2e/layout.test.ts @@ -13,10 +13,11 @@ import { encodePublicTransactionEvent, decodePublicTransactionEvent, invokeAccountsLayout, -} from '../../src/programs/layout'; +} from '../../src/programs/system/layout'; 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) { @@ -144,9 +141,9 @@ describe('layout', () => { }, merkleContext: { merkleTreePubkeyIndex: 0, - nullifierQueuePubkeyIndex: 1, + queuePubkeyIndex: 1, leafIndex: 10, - queueIndex: null, + proveByIndex: false, }, rootIndex: 0, readOnly: false, diff --git a/js/stateless.js/tests/e2e/rpc-interop.test.ts b/js/stateless.js/tests/e2e/rpc-interop.test.ts index ab9a096def..b175435633 100644 --- a/js/stateless.js/tests/e2e/rpc-interop.test.ts +++ b/js/stateless.js/tests/e2e/rpc-interop.test.ts @@ -4,13 +4,15 @@ import { newAccountWithLamports } from '../../src/test-helpers/test-utils'; import { Rpc, createRpc } from '../../src/rpc'; import { LightSystemProgram, + TreeInfo, bn, compress, createAccount, createAccountWithLamports, - defaultTestStateTreeAccounts, deriveAddress, deriveAddressSeed, + getDefaultAddressTreeInfo, + selectStateTreeInfo, sleep, } from '../../src'; import { getTestRpc, TestRpc } from '../../src/test-helpers/test-rpc'; @@ -18,12 +20,34 @@ 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', () => { + LightSystemProgram.deriveCompressedSolPda(); let payer: Signer; let bob: Signer; let rpc: Rpc; let testRpc: TestRpc; let executedTxs = 0; + let stateTreeInfo: TreeInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); rpc = createRpc(); @@ -34,13 +58,11 @@ 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.getStateTreeInfos(); + stateTreeInfo = selectStateTreeInfo(stateTreeInfos); + + await compress(rpc, payer, 1e9, payer.publicKey, stateTreeInfo); + executedTxs++; }); @@ -93,15 +115,20 @@ describe('rpc-interop', () => { validityProof.roots.forEach((elem, index) => { assert.isTrue(elem.eq(validityProofTest.roots[index])); }); + validityProof.rootIndices.forEach((elem, index) => { assert.equal(elem, validityProofTest.rootIndices[index]); }); - validityProof.merkleTrees.forEach((elem, index) => { - assert.isTrue(elem.equals(validityProofTest.merkleTrees[index])); + + validityProof.treeInfos.forEach((elem, index) => { + assert.isTrue( + elem.tree.equals(validityProofTest.treeInfos[index].tree), + ); }); - validityProof.nullifierQueues.forEach((elem, index) => { + + validityProof.treeInfos.forEach((elem, index) => { assert.isTrue( - elem.equals(validityProofTest.nullifierQueues[index]), + elem.queue.equals(validityProofTest.treeInfos[index].queue), ); }); @@ -115,12 +142,7 @@ describe('rpc-interop', () => { }); it('getValidityProof [noforester] (new-addresses) should match', async () => { - const newAddressSeeds = [ - new Uint8Array([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 42, 42, 42, 14, 15, 16, 11, 18, - 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, - ]), - ]; + const newAddressSeeds = [new Uint8Array(randomBytes(32))]; const newAddressSeed = deriveAddressSeed( newAddressSeeds, LightSystemProgram.programId, @@ -147,22 +169,19 @@ describe('rpc-interop', () => { validityProof.rootIndices.forEach((elem, index) => { assert.equal(elem, validityProofTest.rootIndices[index]); }); - validityProof.merkleTrees.forEach((elem, index) => { - assert.isTrue(elem.equals(validityProofTest.merkleTrees[index])); + validityProof.treeInfos.forEach((elem, index) => { + assert.isTrue( + elem.tree.equals(validityProofTest.treeInfos[index].tree), + ); }); - validityProof.nullifierQueues.forEach((elem, index) => { + validityProof.treeInfos.forEach((elem, index) => { assert.isTrue( - elem.equals(validityProofTest.nullifierQueues[index]), + elem.queue.equals(validityProofTest.treeInfos[index].queue), ); }); /// Need a new unique address because the previous one has been created. - const newAddressSeedsTest = [ - new Uint8Array([ - 2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 42, 42, 42, 14, 15, 16, 17, 18, - 19, 20, 21, 22, 23, 24, 25, 26, 27, 32, 29, 30, 31, 32, - ]), - ]; + const newAddressSeedsTest = [new Uint8Array(randomBytes(32))]; /// Creates a compressed account with address using a (non-inclusion) /// 'validityProof' from Photon await createAccount( @@ -171,8 +190,7 @@ describe('rpc-interop', () => { newAddressSeedsTest, LightSystemProgram.programId, undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); executedTxs++; @@ -184,8 +202,7 @@ describe('rpc-interop', () => { newAddressSeeds, LightSystemProgram.programId, undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); executedTxs++; }); @@ -205,12 +222,7 @@ describe('rpc-interop', () => { // accounts are the same assert.isTrue(hash.eq(hashTest)); - const newAddressSeeds = [ - new Uint8Array([ - 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 42, 32, 42, 14, 15, 16, 17, 18, - 19, 20, 21, 22, 23, 24, 32, 32, 27, 28, 29, 30, 31, 32, - ]), - ]; + const newAddressSeeds = [new Uint8Array(randomBytes(32))]; const newAddressSeed = deriveAddressSeed( newAddressSeeds, LightSystemProgram.programId, @@ -264,11 +276,13 @@ describe('rpc-interop', () => { ), ); assert.isTrue( - newAddressProof.merkleTree.equals(newAddressProofTest.merkleTree), + newAddressProof.treeInfo.tree.equals( + newAddressProofTest.treeInfo.tree, + ), ); assert.isTrue( - newAddressProof.nullifierQueue.equals( - newAddressProofTest.nullifierQueue, + newAddressProof.treeInfo.queue.equals( + newAddressProofTest.treeInfo.queue, ), ); assert.isTrue(newAddressProof.root.eq(newAddressProofTest.root)); @@ -287,16 +301,18 @@ describe('rpc-interop', () => { validityProof.rootIndices.forEach((elem, index) => { assert.equal(elem, validityProofTest.rootIndices[index]); }); - validityProof.merkleTrees.forEach((elem, index) => { - assert.isTrue(elem.equals(validityProofTest.merkleTrees[index])); + validityProof.treeInfos.forEach((elem, index) => { + assert.isTrue( + elem.tree.equals(validityProofTest.treeInfos[index].tree), + ); }); - validityProof.nullifierQueues.forEach((elem, index) => { + validityProof.treeInfos.forEach((elem, index) => { assert.isTrue( - elem.equals(validityProofTest.nullifierQueues[index]), + elem.queue.equals(validityProofTest.treeInfos[index].queue), 'Mismatch in nullifierQueues expected: ' + elem + ' got: ' + - validityProofTest.nullifierQueues[index], + validityProofTest.treeInfos[index].queue, ); }); @@ -305,18 +321,11 @@ describe('rpc-interop', () => { await createAccountWithLamports( rpc, payer, - [ - new Uint8Array([ - 1, 2, 255, 4, 5, 6, 7, 8, 9, 10, 11, 111, 13, 14, 15, 16, - 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 32, 29, 30, 31, - 32, - ]), - ], + [new Uint8Array(randomBytes(32))], 0, LightSystemProgram.programId, undefined, - undefined, - defaultTestStateTreeAccounts().merkleTree, + stateTreeInfo, ); executedTxs++; }); @@ -324,10 +333,12 @@ describe('rpc-interop', () => { /// This assumes support for getMultipleNewAddressProofs in Photon. it('getMultipleNewAddressProofs [noforester] should match', async () => { const newAddress = bn( - new Uint8Array([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 42, 42, 42, 14, 15, 16, 17, 18, - 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, - ]), + deriveAddress( + deriveAddressSeed( + [new Uint8Array(randomBytes(32))], + LightSystemProgram.programId, + ), + ).toBytes(), ); const newAddressProof = ( await rpc.getMultipleNewAddressProofs([newAddress]) @@ -358,13 +369,15 @@ describe('rpc-interop', () => { ); assert.isTrue( - newAddressProof.merkleTree.equals(newAddressProofTest.merkleTree), + newAddressProof.treeInfo.tree.equals( + newAddressProofTest.treeInfo.tree, + ), ); assert.isTrue( - newAddressProof.nullifierQueue.equals( - newAddressProofTest.nullifierQueue, + newAddressProof.treeInfo.queue.equals( + newAddressProofTest.treeInfo.queue, ), - `Mismatch in nullifierQueue expected: ${newAddressProofTest.nullifierQueue} got: ${newAddressProof.nullifierQueue}`, + `Mismatch in nullifierQueue expected: ${newAddressProofTest.treeInfo.queue} got: ${newAddressProof.treeInfo.queue}`, ); assert.isTrue(newAddressProof.root.eq(newAddressProofTest.root)); @@ -535,13 +548,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( @@ -656,23 +663,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 +699,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..f5ade6426a 100644 --- a/js/stateless.js/tests/e2e/rpc-multi-trees.test.ts +++ b/js/stateless.js/tests/e2e/rpc-multi-trees.test.ts @@ -1,16 +1,18 @@ 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, + TreeInfo, bn, compress, createAccount, createAccountWithLamports, - defaultTestStateTreeAccounts2, deriveAddress, deriveAddressSeed, + featureFlags, + selectStateTreeInfo, } from '../../src'; import { getTestRpc, TestRpc } from '../../src/test-helpers/test-rpc'; import { transfer } from '../../src/actions/transfer'; @@ -26,26 +28,29 @@ describe('rpc-multi-trees', () => { const randTrees: PublicKey[] = []; const randQueues: PublicKey[] = []; - + let stateTreeInfo2: TreeInfo; beforeAll(async () => { const lightWasm = await WasmFactory.getInstance(); rpc = createRpc(); testRpc = await getTestRpc(lightWasm); + const stateTreeInfo = selectStateTreeInfo( + await rpc.getStateTreeInfos(), + ); + if (featureFlags.isV2()) { + // TODO: add test specifically for multiple v2 trees. + stateTreeInfo2 = stateTreeInfo; + } else + stateTreeInfo2 = selectStateTreeInfo(await rpc.getStateTreeInfos()); + /// 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++; }); @@ -69,17 +74,15 @@ describe('rpc-multi-trees', () => { dataSlice: { offset: 1, length: 2 }, }); - expect(accs.items[0].merkleTree).toEqual(randTrees[0]); - expect(accs.items[0].nullifierQueue).toEqual(randQueues[0]); + expect(accs.items[0].treeInfo.tree).toEqual(randTrees[0]); + expect(accs.items[0].treeInfo.queue).toEqual(randQueues[0]); assert.equal(accs.items.length, 1); }); let address: PublicKey; - it('must create account with random output tree (pickRandomTreeAndQueue)', async () => { - const tree = pickRandomTreeAndQueue( - await rpc.getCachedActiveStateTreeInfo(), - ); + it('must create account with random output tree (selectStateTreeInfo)', async () => { + const tree = selectStateTreeInfo(await rpc.getStateTreeInfos()); const seed = randomBytes(32); const addressSeed = deriveAddressSeed( @@ -94,16 +97,15 @@ describe('rpc-multi-trees', () => { [seed], LightSystemProgram.programId, undefined, - undefined, - tree.tree, // output state tree + tree, // output state tree ); randTrees.push(tree.tree); randQueues.push(tree.queue); const acc = await rpc.getCompressedAccount(bn(address.toBuffer())); - expect(acc!.merkleTree).toEqual(tree.tree); - expect(acc!.nullifierQueue).toEqual(tree.queue); + expect(acc!.treeInfo.tree).toEqual(tree.tree); + expect(acc!.treeInfo.queue).toEqual(tree.queue); }); it('getValidityProof [noforester] (inclusion) should return correct trees and queues', async () => { @@ -111,35 +113,31 @@ describe('rpc-multi-trees', () => { const hash = bn(acc!.hash); const pos = randTrees.length - 1; - expect(acc?.merkleTree).toEqual(randTrees[pos]); - expect(acc?.nullifierQueue).toEqual(randQueues[pos]); + expect(acc?.treeInfo.tree).toEqual(randTrees[pos]); + expect(acc?.treeInfo.queue).toEqual(randQueues[pos]); const validityProof = await rpc.getValidityProof([hash]); - expect(validityProof.merkleTrees[0]).toEqual(randTrees[pos]); - expect(validityProof.nullifierQueues[0]).toEqual(randQueues[pos]); + expect(validityProof.treeInfos[0].tree).toEqual(randTrees[pos]); + expect(validityProof.treeInfos[0].queue).toEqual(randQueues[pos]); /// Executes transfers using random output trees - const tree1 = pickRandomTreeAndQueue( - await rpc.getCachedActiveStateTreeInfo(), - ); - await transfer(rpc, payer, 1e5, payer, bob.publicKey, tree1.tree); + const tree1 = selectStateTreeInfo(await rpc.getStateTreeInfos()); + await transfer(rpc, payer, 1e5, payer, bob.publicKey); executedTxs++; randTrees.push(tree1.tree); randQueues.push(tree1.queue); - const tree2 = pickRandomTreeAndQueue( - await rpc.getCachedActiveStateTreeInfo(), - ); - await transfer(rpc, payer, 1e5, payer, bob.publicKey, tree2.tree); + const tree2 = selectStateTreeInfo(await rpc.getStateTreeInfos()); + await transfer(rpc, payer, 1e5, payer, bob.publicKey); executedTxs++; randTrees.push(tree2.tree); randQueues.push(tree2.queue); const validityProof2 = await rpc.getValidityProof([hash]); - expect(validityProof2.merkleTrees[0]).toEqual(randTrees[pos]); - expect(validityProof2.nullifierQueues[0]).toEqual(randQueues[pos]); + expect(validityProof2.treeInfos[0].tree).toEqual(randTrees[pos]); + expect(validityProof2.treeInfos[0].queue).toEqual(randQueues[pos]); }); it('getValidityProof [noforester] (combined) should return correct trees and queues', async () => { @@ -148,12 +146,7 @@ describe('rpc-multi-trees', () => { ); const hash = bn(senderAccounts.items[0].hash); - const newAddressSeeds = [ - new Uint8Array([ - 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 42, 30, 40, 10, 13, 16, 17, 18, - 19, 20, 21, 22, 23, 24, 32, 32, 27, 28, 29, 30, 31, 32, - ]), - ]; + const newAddressSeeds = [new Uint8Array(randomBytes(32))]; const newAddressSeed = deriveAddressSeed( newAddressSeeds, LightSystemProgram.programId, @@ -178,26 +171,24 @@ describe('rpc-multi-trees', () => { // only compare state tree assert.isTrue( - validityProof.merkleTrees[0].equals( - senderAccounts.items[0].merkleTree, + validityProof.treeInfos[0].tree.equals( + senderAccounts.items[0].treeInfo.tree, ), 'Mismatch in merkleTrees expected: ' + - senderAccounts.items[0].merkleTree + + senderAccounts.items[0].treeInfo.tree + ' got: ' + - validityProof.merkleTrees[0], + validityProof.treeInfos[0].tree, ); assert.isTrue( - validityProof.nullifierQueues[0].equals( - senderAccounts.items[0].nullifierQueue, + validityProof.treeInfos[0].queue.equals( + senderAccounts.items[0].treeInfo.queue, ), - `Mismatch in nullifierQueues expected: ${senderAccounts.items[0].nullifierQueue} got: ${validityProof.nullifierQueues[0]}`, + `Mismatch in nullifierQueues expected: ${senderAccounts.items[0].treeInfo.queue} got: ${validityProof.treeInfos[0].queue}`, ); /// 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.getStateTreeInfos()); await createAccountWithLamports( rpc, payer, @@ -205,8 +196,7 @@ describe('rpc-multi-trees', () => { 0, LightSystemProgram.programId, undefined, - undefined, - tree.tree, + tree, ); executedTxs++; randTrees.push(tree.tree); @@ -224,43 +214,31 @@ describe('rpc-multi-trees', () => { ); proofs.forEach((proof, index) => { + const expectedTree = + prePayerAccounts.items[index].treeInfo.tree; + const actualTree = proof.treeInfo.tree; + const expectedQueue = + prePayerAccounts.items[index].treeInfo.queue; + const actualQueue = proof.treeInfo.queue; + assert.isTrue( - proof.merkleTree.equals( - prePayerAccounts.items[index].merkleTree, - ), + actualTree.equals(expectedTree), `Iteration ${round + 1}: Mismatch in merkleTree for account index ${index}`, ); assert.isTrue( - proof.nullifierQueue.equals( - prePayerAccounts.items[index].nullifierQueue, - ), + actualQueue.equals(expectedQueue), `Iteration ${round + 1}: Mismatch in nullifierQueue for account index ${index}`, ); }); - const tree = pickRandomTreeAndQueue( - await rpc.getCachedActiveStateTreeInfo(), - ); - await transfer( - rpc, - payer, - transferAmount, - payer, - bob.publicKey, - tree.tree, - ); + const tree = selectStateTreeInfo(await rpc.getStateTreeInfos()); + await transfer(rpc, payer, transferAmount, payer, bob.publicKey); 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( @@ -273,14 +251,14 @@ describe('rpc-multi-trees', () => { compressedAccounts.forEach((account, index) => { assert.isTrue( - account.merkleTree.equals( - senderAccounts.items[index].merkleTree, + account.treeInfo.tree.equals( + senderAccounts.items[index].treeInfo.tree, ), `Mismatch in merkleTree for account index ${index}`, ); assert.isTrue( - account.nullifierQueue.equals( - senderAccounts.items[index].nullifierQueue, + account.treeInfo.queue.equals( + senderAccounts.items[index].treeInfo.queue, ), `Mismatch in nullifierQueue for account index ${index}`, ); diff --git a/js/stateless.js/tests/e2e/test-rpc.test.ts b/js/stateless.js/tests/e2e/test-rpc.test.ts index 9d4ab0254e..09dfdd58c3 100644 --- a/js/stateless.js/tests/e2e/test-rpc.test.ts +++ b/js/stateless.js/tests/e2e/test-rpc.test.ts @@ -4,13 +4,13 @@ import { STATE_MERKLE_TREE_NETWORK_FEE, STATE_MERKLE_TREE_ROLLOVER_FEE, defaultTestStateTreeAccounts, + featureFlags, } from '../../src/constants'; import { newAccountWithLamports } from '../../src/test-helpers/test-utils'; 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', () => { @@ -29,29 +29,17 @@ describe('test-rpc', () => { const lightWasm = await WasmFactory.getInstance(); rpc = await getTestRpc(lightWasm); - refPayer = await newAccountWithLamports(rpc, 1e9, 200); - payer = await newAccountWithLamports(rpc, 1e9, 148); + refPayer = await newAccountWithLamports(rpc, 1e9, 256); + payer = await newAccountWithLamports(rpc, 1e9, 256); /// 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,14 +79,15 @@ describe('test-rpc', () => { const compressedAccountProof = await rpc.getCompressedAccountProof( bn(refHash), ); + const proof = compressedAccountProof.merkleProof.map(x => x.toString()); - expect(proof.length).toStrictEqual(26); + expect(proof.length).toStrictEqual(featureFlags.isV2() ? 32 : 26); expect(compressedAccountProof.hash).toStrictEqual(refHash); expect(compressedAccountProof.leafIndex).toStrictEqual( compressedAccounts.items[0].leafIndex, ); - expect(compressedAccountProof.rootIndex).toStrictEqual(2); + preCompressBalance = await rpc.getBalance(payer.publicKey); await transfer( @@ -107,7 +96,6 @@ describe('test-rpc', () => { compressLamportsAmount, payer, payer.publicKey, - merkleTree, ); const compressedAccounts1 = await rpc.getCompressedAccountsByOwner( payer.publicKey, @@ -122,13 +110,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..4b1eb73292 100644 --- a/js/stateless.js/tests/e2e/testnet.test.ts +++ b/js/stateless.js/tests/e2e/testnet.test.ts @@ -2,10 +2,8 @@ import { describe, it, assert, beforeAll } from 'vitest'; import { Signer } from '@solana/web3.js'; import { newAccountWithLamports } from '../../src/test-helpers/test-utils'; import { createRpc, Rpc } from '../../src/rpc'; -import { bn, compress, defaultTestStateTreeAccounts } from '../../src'; +import { bn, compress } from '../../src'; import { transfer } from '../../src/actions/transfer'; -import { getTestRpc } from '../../src/test-helpers/test-rpc'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; describe('testnet transfer', () => { let rpc: Rpc; @@ -20,13 +18,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..b2fbcb6997 100644 --- a/js/stateless.js/tests/e2e/transfer.test.ts +++ b/js/stateless.js/tests/e2e/transfer.test.ts @@ -2,7 +2,7 @@ import { describe, it, assert, beforeAll } from 'vitest'; import { Signer } from '@solana/web3.js'; import { newAccountWithLamports } from '../../src/test-helpers/test-utils'; import { Rpc } from '../../src/rpc'; -import { bn, compress, defaultTestStateTreeAccounts } from '../../src'; +import { bn, compress } from '../../src'; import { transfer } from '../../src/actions/transfer'; import { getTestRpc } from '../../src/test-helpers/test-rpc'; import { WasmFactory } from '@lightprotocol/hasher.rs'; @@ -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/tests/unit/instruction/pack-compressed-accounts.test.ts b/js/stateless.js/tests/unit/instruction/pack-compressed-accounts.test.ts index e9db65ddb4..7689f4d33d 100644 --- a/js/stateless.js/tests/unit/instruction/pack-compressed-accounts.test.ts +++ b/js/stateless.js/tests/unit/instruction/pack-compressed-accounts.test.ts @@ -1,33 +1,33 @@ import { describe, expect, it } from 'vitest'; import { PublicKey } from '@solana/web3.js'; -import { padOutputStateMerkleTrees } from '../../../src/instruction/pack-compressed-accounts'; +import { padOutputStateMerkleTrees } from '../../../src/programs/system/pack'; describe('padOutputStateMerkleTrees', () => { const treeA: any = PublicKey.unique(); const treeB: any = PublicKey.unique(); const treeC: any = PublicKey.unique(); - const accA: any = { merkleTree: treeA }; - const accB: any = { merkleTree: treeB }; - const accC: any = { merkleTree: treeC }; + const accA: any = { treeInfo: { tree: treeA } }; + const accB: any = { treeInfo: { tree: treeB } }; + const accC: any = { treeInfo: { tree: treeC } }; it('should use the 0th state tree of input state if no output state trees are provided', () => { - const result = padOutputStateMerkleTrees(undefined, 3, [accA, accB]); + const result = padOutputStateMerkleTrees(treeA, 3); expect(result).toEqual([treeA, treeA, treeA]); }); it('should fill up with the first state tree if provided trees are less than required', () => { - const result = padOutputStateMerkleTrees([treeA, treeB], 5, []); - expect(result).toEqual([treeA, treeB, treeA, treeA, treeA]); + const result = padOutputStateMerkleTrees(treeA, 5); + expect(result).toEqual([treeA, treeA, treeA, treeA, treeA]); }); it('should remove extra trees if the number of output state trees is greater than the number of output accounts', () => { - const result = padOutputStateMerkleTrees([treeA, treeB, treeC], 2, []); - expect(result).toEqual([treeA, treeB]); + const result = padOutputStateMerkleTrees(treeA, 2); + expect(result).toEqual([treeA, treeA]); }); it('should return the same outputStateMerkleTrees if its length equals the number of output compressed accounts', () => { - const result = padOutputStateMerkleTrees([treeA, treeB, treeC], 3, []); - expect(result).toEqual([treeA, treeB, treeC]); + const result = padOutputStateMerkleTrees(treeA, 3); + expect(result).toEqual([treeA, treeA, treeA]); }); }); diff --git a/js/stateless.js/tests/unit/state/bn254.test.ts b/js/stateless.js/tests/unit/state/bn254.test.ts index eafe971238..e032eff129 100644 --- a/js/stateless.js/tests/unit/state/bn254.test.ts +++ b/js/stateless.js/tests/unit/state/bn254.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { createBN254, encodeBN254toBase58 } from '../../../src/state/BN254'; +import { createBN254, encodeBN254toBase58 } from '../../../src/state'; import { bn } from '../../../src/state'; import { PublicKey } from '@solana/web3.js'; import { FIELD_SIZE } from '../../../src/constants'; diff --git a/js/stateless.js/tests/unit/state/compressed-account.test.ts b/js/stateless.js/tests/unit/state/compressed-account.test.ts index 5d6b16667c..3a6a33b7a6 100644 --- a/js/stateless.js/tests/unit/state/compressed-account.test.ts +++ b/js/stateless.js/tests/unit/state/compressed-account.test.ts @@ -5,7 +5,8 @@ import { createMerkleContext, } from '../../../src/state/compressed-account'; import { PublicKey } from '@solana/web3.js'; -import { bn } from '../../../src/state/BN254'; +import { bn } from '../../../src/state'; +import { TreeType } from '../../../src/state'; describe('createCompressedAccount function', () => { it('should create a compressed account with default values', () => { @@ -46,9 +47,13 @@ describe('createCompressedAccountWithMerkleContext function', () => { const hash = new Array(32).fill(1); const leafIndex = 0; const merkleContext = createMerkleContext( - merkleTree, - nullifierQueue, - hash, + { + tree: merkleTree, + queue: nullifierQueue, + treeType: TreeType.AddressV1, + nextTreeInfo: null, + }, + bn(hash), leafIndex, ); const accountWithMerkleContext = @@ -58,11 +63,16 @@ describe('createCompressedAccountWithMerkleContext function', () => { lamports: bn(0), address: null, data: null, - merkleTree, - nullifierQueue, - hash, + treeInfo: { + tree: merkleTree, + queue: nullifierQueue, + treeType: TreeType.AddressV1, + nextTreeInfo: null, + }, + hash: bn(hash), leafIndex, readOnly: false, + proveByIndex: false, }); }); }); @@ -75,16 +85,26 @@ describe('createMerkleContext function', () => { const leafIndex = 0; const merkleContext = createMerkleContext( - merkleTree, - nullifierQueue, - hash, + { + tree: merkleTree, + queue: nullifierQueue, + treeType: TreeType.AddressV1, + nextTreeInfo: null, + }, + bn(hash), leafIndex, + false, ); expect(merkleContext).toEqual({ - merkleTree, - nullifierQueue, - hash, + treeInfo: { + tree: merkleTree, + queue: nullifierQueue, + treeType: TreeType.AddressV1, + nextTreeInfo: null, + }, + hash: bn(hash), leafIndex, + proveByIndex: false, }); }); }); diff --git a/js/stateless.js/tests/unit/utils/conversion.test.ts b/js/stateless.js/tests/unit/utils/conversion.test.ts index 117d0502fa..ac6b5a2a85 100644 --- a/js/stateless.js/tests/unit/utils/conversion.test.ts +++ b/js/stateless.js/tests/unit/utils/conversion.test.ts @@ -10,17 +10,7 @@ import { } from '../../../src/utils/conversion'; import { calculateComputeUnitPrice } from '../../../src/utils'; import { deserializeAppendNullifyCreateAddressInputsIndexer } from '../../../src/programs'; -import { - struct, - u8, - bool, - publicKey, - option, - vec, - u64, -} from '@coral-xyz/borsh'; -import { decodeInstructionDataInvokeCpiWithReadOnly } from '../../../src/programs/layout'; -import { InstructionDataInvoke } from '../../../src/state/types'; +import { decodeInstructionDataInvokeCpiWithReadOnly } from '../../../src/programs/system/layout'; import BN from 'bn.js'; describe('toArray', () => { @@ -65,7 +55,7 @@ describe('deserialize apc cpi', () => { expect(result.meta.is_invoked_by_program).toEqual(1); expect(result.addresses.length).toBeGreaterThan(0); - console.log('address ', result.addresses[0]); + expect(result.addresses[0]).toEqual({ address: new Array(32).fill(1), tree_index: 1, @@ -675,9 +665,9 @@ describe('convertInvokeCpiWithReadOnlyToInvoke', () => { expect(firstAccount.readOnly).toBe(false); expect(firstAccount.compressedAccount.lamports).toEqual(new BN(2000)); expect(firstAccount.merkleContext.merkleTreePubkeyIndex).toBe(8); - expect(firstAccount.merkleContext.nullifierQueuePubkeyIndex).toBe(9); + expect(firstAccount.merkleContext.queuePubkeyIndex).toBe(9); expect(firstAccount.merkleContext.leafIndex).toBe(456); - expect(firstAccount.merkleContext.queueIndex).toBeNull(); + expect(firstAccount.merkleContext.proveByIndex).toBe(false); // Check output accounts conversion expect(result.outputCompressedAccounts).toHaveLength(1); expect(result.outputCompressedAccounts[0].merkleTreeIndex).toBe(10); diff --git a/js/stateless.js/tests/unit/utils/tree-info.test.ts b/js/stateless.js/tests/unit/utils/tree-info.test.ts new file mode 100644 index 0000000000..672f0d54a1 --- /dev/null +++ b/js/stateless.js/tests/unit/utils/tree-info.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, it } from 'vitest'; +import { TreeType, TreeInfo } from '../../../src/state'; +import { selectStateTreeInfo } from '../../../src/utils'; +import { PublicKey } from '@solana/web3.js'; +import { + cpiContext2Pubkey, + cpiContextPubkey, + merkleTree2Pubkey, + merkletreePubkey, + nullifierQueue2Pubkey, + nullifierQueuePubkey, +} from '../../../src'; + +describe('selectStateTreeInfo', () => { + const info1: TreeInfo = { + tree: new PublicKey(merkletreePubkey), + queue: new PublicKey(nullifierQueuePubkey), + cpiContext: new PublicKey(cpiContextPubkey), + treeType: TreeType.StateV1, + nextTreeInfo: null, + }; + const info2: TreeInfo = { + tree: new PublicKey(merkleTree2Pubkey), + queue: new PublicKey(nullifierQueue2Pubkey), + cpiContext: new PublicKey(cpiContext2Pubkey), + treeType: TreeType.StateV1, + nextTreeInfo: null, + }; + const infoV2: TreeInfo = { + tree: new PublicKey(merkleTree2Pubkey), + queue: new PublicKey(nullifierQueue2Pubkey), + cpiContext: new PublicKey(cpiContext2Pubkey), + treeType: TreeType.StateV2, + nextTreeInfo: null, + }; + const info3: TreeInfo = { + tree: PublicKey.unique(), + queue: PublicKey.unique(), + cpiContext: PublicKey.unique(), + treeType: TreeType.StateV1, + nextTreeInfo: null, + }; + const info4: TreeInfo = { + tree: PublicKey.unique(), + queue: PublicKey.unique(), + cpiContext: PublicKey.unique(), + treeType: TreeType.StateV1, + nextTreeInfo: null, + }; + const info5: TreeInfo = { + tree: PublicKey.unique(), + queue: PublicKey.unique(), + cpiContext: PublicKey.unique(), + treeType: TreeType.StateV1, + nextTreeInfo: null, + }; + const info6: TreeInfo = { + tree: PublicKey.unique(), + queue: PublicKey.unique(), + cpiContext: PublicKey.unique(), + treeType: TreeType.StateV1, + nextTreeInfo: null, + }; + const infoInactive: TreeInfo = { + tree: PublicKey.unique(), + queue: PublicKey.unique(), + cpiContext: PublicKey.unique(), + treeType: TreeType.StateV1, + nextTreeInfo: info1, + }; + + const info1V2: TreeInfo = { + tree: new PublicKey(merkletreePubkey), + queue: new PublicKey(nullifierQueuePubkey), + cpiContext: new PublicKey(cpiContextPubkey), + treeType: TreeType.StateV2, + nextTreeInfo: null, + }; + + it('returns a filtered tree info', () => { + const infos = [info1, info2, infoV2]; + for (let i = 0; i < 10_000; i++) { + const result = selectStateTreeInfo(infos, TreeType.StateV1); + expect(result.treeType).toBe(TreeType.StateV1); + } + }); + + it('should default to MAX_HOTSPOTS (5) if there are more than 5 infos', () => { + const infos = [info1, info2, info3, info4, info5, info6]; + for (let i = 0; i < 10_000; i++) { + const result = selectStateTreeInfo(infos, TreeType.StateV1); + const expectedRange = [info1, info2, info3, info4, info5]; + + expect(expectedRange.length).toBe(5); + expect(expectedRange.includes(result)).toBe(true); + } + }); + + it('should return all infos if useMaxConcurrency is true', () => { + const infos = [info1, info2, info3, info4, info5, info6]; + for (let i = 0; i < 10_000; i++) { + const result = selectStateTreeInfo(infos, TreeType.StateV1, true); + const expectedRange = [info1, info2, info3, info4, info5, info6]; + + expect(expectedRange.includes(result)).toBe(true); + } + }); + + it('should never return inactive infos if useMaxConcurrency is true', () => { + const infos = [info1, info2, info3, info4, info5, info6, infoInactive]; + for (let i = 0; i < 100_000; i++) { + const result = selectStateTreeInfo(infos, TreeType.StateV1, true); + const expectedRange = [info1, info2, info3, info4, info5, info6]; + + expect(expectedRange.includes(result)).toBe(true); + expect(result !== infoInactive).toBe(true); + } + }); + + it('throws if queue is missing', () => { + const infos = [ + { ...info1, queue: null }, + { ...info1V2, queue: null }, + ]; + expect(() => selectStateTreeInfo(infos as any)).toThrow( + 'Queue must not be null for state tree', + ); + }); +}); diff --git a/js/stateless.js/vitest.config.ts b/js/stateless.js/vitest.config.ts index 00148c4f1b..2d24a436c1 100644 --- a/js/stateless.js/vitest.config.ts +++ b/js/stateless.js/vitest.config.ts @@ -4,7 +4,8 @@ export default defineConfig({ test: { include: ['tests/**/*.test.ts'], exclude: process.env.EXCLUDE_E2E ? ['tests/e2e/**'] : [], - testTimeout: 35000, - reporters: ['default', 'verbose'], + testTimeout: 30000, + hookTimeout: 20000, + reporters: ['verbose'], }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a386f6b9e2..332aa03b98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3074,6 +3074,9 @@ packages: '@types/node@22.15.30': resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==} + '@types/node@22.5.5': + resolution: {integrity: sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==} + '@types/normalize-package-data@2.4.2': resolution: {integrity: sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==} @@ -11968,6 +11971,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@22.5.5': + dependencies: + undici-types: 6.19.8 + '@types/normalize-package-data@2.4.2': {} '@types/react-dom@19.1.4(@types/react@19.1.6)': @@ -12375,6 +12382,14 @@ snapshots: optionalDependencies: vite: 5.0.4(@types/node@22.15.30)(terser@5.42.0) + '@vitest/mocker@2.1.1(@vitest/spy@2.1.1)(vite@5.0.4(@types/node@22.5.5)(terser@5.39.0))': + dependencies: + '@vitest/spy': 2.1.1 + estree-walker: 3.0.3 + magic-string: 0.30.11 + optionalDependencies: + vite: 5.0.4(@types/node@22.5.5)(terser@5.39.0) + '@vitest/pretty-format@2.1.1': dependencies: tinyrainbow: 1.2.0 @@ -14335,6 +14350,17 @@ snapshots: - supports-color - typescript + eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.13.1(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(terser@5.39.0)): + dependencies: + '@typescript-eslint/utils': 7.13.1(eslint@8.57.0)(typescript@5.6.2) + eslint: 8.57.0 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.13.1(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2) + vitest: 2.1.1(@types/node@22.5.5)(terser@5.39.0) + transitivePeerDependencies: + - supports-color + - typescript + eslint-scope@7.2.2: dependencies: esrecurse: 4.3.0 @@ -17942,6 +17968,24 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + ts-node@10.9.2(@types/node@22.5.5)(typescript@5.6.2): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.5.5 + acorn: 8.11.3 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.6.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + ts-node@7.0.1: dependencies: arrify: 1.0.1 @@ -18277,6 +18321,39 @@ snapshots: - supports-color - terser + vitest@2.1.1(@types/node@22.5.5)(terser@5.39.0): + dependencies: + '@vitest/expect': 2.1.1 + '@vitest/mocker': 2.1.1(@vitest/spy@2.1.1)(vite@5.0.4(@types/node@22.5.5)(terser@5.39.0)) + '@vitest/pretty-format': 2.1.1 + '@vitest/runner': 2.1.1 + '@vitest/snapshot': 2.1.1 + '@vitest/spy': 2.1.1 + '@vitest/utils': 2.1.1 + chai: 5.2.0 + debug: 4.3.7(supports-color@8.1.1) + magic-string: 0.30.11 + pathe: 1.1.2 + std-env: 3.7.0 + tinybench: 2.9.0 + tinyexec: 0.3.0 + tinypool: 1.0.1 + tinyrainbow: 1.2.0 + vite: 5.0.4(@types/node@22.5.5)(terser@5.39.0) + vite-node: 2.1.1(@types/node@22.5.5)(terser@5.39.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.5.5 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - stylus + - sugarss + - supports-color + - terser + vlq@1.0.1: {} wait-on@7.2.0: diff --git a/programs/system/src/context.rs b/programs/system/src/context.rs index 7c7bc531ae..0b4a1a5e2a 100644 --- a/programs/system/src/context.rs +++ b/programs/system/src/context.rs @@ -150,7 +150,10 @@ pub struct WrappedInstructionData<'a, T: InstructionData<'a>> { cpi_context_outputs_end_offset: usize, } -impl<'a, 'b, T: InstructionData<'a>> WrappedInstructionData<'a, T> { +impl<'a, 'b, T> WrappedInstructionData<'a, T> +where + T: InstructionData<'a>, +{ pub fn new(instruction_data: T) -> std::result::Result { let outputs_len = instruction_data .output_accounts() @@ -262,10 +265,10 @@ impl<'a, 'b, T: InstructionData<'a>> WrappedInstructionData<'a, T> { None } } else { - self.instruction_data - .output_accounts() + let accounts = self.instruction_data.output_accounts(); + accounts .get(index) - .map(|account| account as &dyn OutputAccount<'a>) + .map(|account| account as &(dyn OutputAccount<'a> + 'b)) } } } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index e7f22fb8ba..8cde6346be 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,3 @@ [toolchain] channel = "1.86" +components = ["rustfmt", "clippy"]