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"]