diff --git a/.github/workflows/ci-pr-main-program.yml b/.github/workflows/ci-pr-main-program.yml index 87ed092..6799c0d 100644 --- a/.github/workflows/ci-pr-main-program.yml +++ b/.github/workflows/ci-pr-main-program.yml @@ -5,9 +5,9 @@ on: branches: - main env: - SOLANA_CLI_VERSION: 1.16.25 + SOLANA_CLI_VERSION: 1.18.21 NODE_VERSION: 18.14.2 - ANCHOR_CLI_VERSION: 0.28.0 + ANCHOR_CLI_VERSION: 0.29.0 jobs: program_changed_files: @@ -32,12 +32,11 @@ jobs: steps: - uses: actions/checkout@v2 # Install rust + toolchain - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - components: clippy + - run: rustup toolchain install stable --component clippy # Cache rust, cargo - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: "v1" - run: cargo test --package locker shell: bash @@ -51,12 +50,11 @@ jobs: - uses: ./.github/actions/setup-dep - uses: ./.github/actions/setup-anchor # Install rust + toolchain - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - components: clippy + - run: rustup toolchain install stable --component clippy # Cache rust, cargo - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: "v1" # Cache node_modules - uses: actions/cache@v2 id: cache-node-modules diff --git a/.gitignore b/.gitignore index 9cba99f..aa56c4b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ target node_modules test-ledger .yarn -proptest-regressions \ No newline at end of file +proptest-regressions + +.idea \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 78147a3..0026280 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,7 +56,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", - "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -73,64 +72,58 @@ dependencies = [ [[package]] name = "anchor-attribute-access-control" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa5be5b72abea167f87c868379ba3c2be356bfca9e6f474fd055fa0f7eeb4f2" +checksum = "e5f619f1d04f53621925ba8a2e633ba5a6081f2ae14758cbb67f38fd823e0a3e" dependencies = [ "anchor-syn", - "anyhow", "proc-macro2", "quote", - "regex", "syn 1.0.109", ] [[package]] name = "anchor-attribute-account" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f468970344c7c9f9d03b4da854fd7c54f21305059f53789d0045c1dd803f0018" +checksum = "e7f2a3e1df4685f18d12a943a9f2a7456305401af21a07c9fe076ef9ecd6e400" dependencies = [ "anchor-syn", - "anyhow", "bs58 0.5.1", "proc-macro2", "quote", - "rustversion", "syn 1.0.109", ] [[package]] name = "anchor-attribute-constant" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59948e7f9ef8144c2aefb3f32a40c5fce2798baeec765ba038389e82301017ef" +checksum = "9423945cb55627f0b30903288e78baf6f62c6c8ab28fb344b6b25f1ffee3dca7" dependencies = [ "anchor-syn", - "proc-macro2", + "quote", "syn 1.0.109", ] [[package]] name = "anchor-attribute-error" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc753c9d1c7981cb8948cf7e162fb0f64558999c0413058e2d43df1df5448086" +checksum = "93ed12720033cc3c3bf3cfa293349c2275cd5ab99936e33dd4bf283aaad3e241" dependencies = [ "anchor-syn", - "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "anchor-attribute-event" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38b4e172ba1b52078f53fdc9f11e3dc0668ad27997838a0aad2d148afac8c97" +checksum = "eef4dc0371eba2d8c8b54794b0b0eb786a234a559b77593d6f80825b6d2c77a2" dependencies = [ "anchor-syn", - "anyhow", "proc-macro2", "quote", "syn 1.0.109", @@ -138,25 +131,34 @@ dependencies = [ [[package]] name = "anchor-attribute-program" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eebd21543606ab61e2d83d9da37d24d3886a49f390f9c43a1964735e8c0f0d5" +checksum = "b18c4f191331e078d4a6a080954d1576241c29c56638783322a18d308ab27e4f" dependencies = [ "anchor-syn", - "anyhow", - "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "anchor-derive-accounts" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec4720d899b3686396cced9508f23dab420f1308344456ec78ef76f98fda42af" +checksum = "5de10d6e9620d3bcea56c56151cad83c5992f50d5960b3a9bebc4a50390ddc3c" dependencies = [ "anchor-syn", - "anyhow", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-serde" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e2e5be518ec6053d90a2a7f26843dbee607583c779e6c8395951b9739bdfbe" +dependencies = [ + "anchor-syn", + "borsh-derive-internal 0.10.3", "proc-macro2", "quote", "syn 1.0.109", @@ -164,9 +166,9 @@ dependencies = [ [[package]] name = "anchor-derive-space" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f495e85480bd96ddeb77b71d499247c7d4e8b501e75ecb234e9ef7ae7bd6552a" +checksum = "1ecc31d19fa54840e74b7a979d44bcea49d70459de846088a1d71e87ba53c419" dependencies = [ "proc-macro2", "quote", @@ -175,9 +177,9 @@ dependencies = [ [[package]] name = "anchor-lang" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d2d4b20100f1310a774aba3471ef268e5c4ba4d5c28c0bbe663c2658acbc414" +checksum = "35da4785497388af0553586d55ebdc08054a8b1724720ef2749d313494f2b8ad" dependencies = [ "anchor-attribute-access-control", "anchor-attribute-account", @@ -186,6 +188,7 @@ dependencies = [ "anchor-attribute-event", "anchor-attribute-program", "anchor-derive-accounts", + "anchor-derive-serde", "anchor-derive-space", "arrayref", "base64 0.13.1", @@ -199,22 +202,24 @@ dependencies = [ [[package]] name = "anchor-spl" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f860599da1c2354e7234c768783049eb42e2f54509ecfc942d2e0076a2da7b" +checksum = "6c4fd6e43b2ca6220d2ef1641539e678bfc31b6cc393cf892b373b5997b6a39a" dependencies = [ "anchor-lang", + "mpl-token-metadata", "solana-program", "spl-associated-token-account", + "spl-memo", "spl-token", - "spl-token-2022", + "spl-token-2022 0.9.0", ] [[package]] name = "anchor-syn" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a125e4b0cc046cfec58f5aa25038e34cf440151d58f0db3afc55308251fe936d" +checksum = "d9101b84702fed2ea57bd22992f75065da5648017135b844283a2f6d74f27825" dependencies = [ "anyhow", "bs58 0.5.1", @@ -351,12 +356,6 @@ dependencies = [ "rand 0.8.5", ] -[[package]] -name = "array-bytes" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ad284aeb45c13f2fb4f084de4a420ebf447423bdf9386c0540ce33cb3ef4b8c" - [[package]] name = "arrayref" version = "0.3.7" @@ -434,17 +433,14 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +dependencies = [ + "serde", +] [[package]] name = "bitmaps" @@ -514,6 +510,16 @@ dependencies = [ "hashbrown 0.13.2", ] +[[package]] +name = "borsh" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" +dependencies = [ + "borsh-derive 1.5.1", + "cfg_aliases", +] + [[package]] name = "borsh-derive" version = "0.9.3" @@ -540,6 +546,20 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "borsh-derive" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" +dependencies = [ + "once_cell", + "proc-macro-crate 3.1.0", + "proc-macro2", + "quote", + "syn 2.0.63", + "syn_derive", +] + [[package]] name = "borsh-derive-internal" version = "0.9.3" @@ -617,9 +637,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.15.0" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83" dependencies = [ "bytemuck_derive", ] @@ -658,6 +678,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -1200,6 +1226,18 @@ dependencies = [ "libsecp256k1-core", ] +[[package]] +name = "light-poseidon" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9a85a9752c549ceb7578064b4ed891179d20acd85f27318573b64d2d7ee7ee" +dependencies = [ + "ark-bn254", + "ark-ff", + "num-bigint", + "thiserror", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1223,8 +1261,11 @@ dependencies = [ "anchor-lang", "anchor-spl", "bytemuck", - "num_enum 0.7.2", + "num_enum", "proptest", + "solana-program", + "spl-pod", + "spl-transfer-hook-interface 0.5.0", "static_assertions", ] @@ -1270,6 +1311,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "mpl-token-metadata" +version = "3.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8ee05284d79b367ae8966d558e1a305a781fc80c9df51f37775169117ba64f" +dependencies = [ + "borsh 0.10.3", + "num-derive 0.3.3", + "num-traits", + "solana-program", + "thiserror", +] + [[package]] name = "num-bigint" version = "0.4.5" @@ -1291,6 +1345,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -1310,55 +1375,13 @@ dependencies = [ "libm", ] -[[package]] -name = "num_enum" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" -dependencies = [ - "num_enum_derive 0.5.11", -] - -[[package]] -name = "num_enum" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" -dependencies = [ - "num_enum_derive 0.6.1", -] - [[package]] name = "num_enum" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" dependencies = [ - "num_enum_derive 0.7.2", -] - -[[package]] -name = "num_enum_derive" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "num_enum_derive" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn 2.0.63", + "num_enum_derive", ] [[package]] @@ -1367,7 +1390,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", "syn 2.0.63", @@ -1467,14 +1490,36 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.3.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ - "once_cell", "toml_edit", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.82" @@ -1492,7 +1537,7 @@ checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.5.0", + "bitflags", "lazy_static", "num-traits", "rand 0.8.5", @@ -1513,6 +1558,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "qualifier_attr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e2e25ee72f5b24d773cae88422baddefff7714f97aab68d96fe2b6fc4a28fb2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -1643,7 +1699,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "bitflags 2.5.0", + "bitflags", ] [[package]] @@ -1696,7 +1752,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -1853,6 +1909,12 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "sized-chunks" version = "0.6.5" @@ -1871,31 +1933,23 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "solana-frozen-abi" -version = "1.16.25" +version = "1.18.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7077f6495ccc313dff49c3e3f3ed03e49058258bae7fee77ac29ba0a474ba82" +checksum = "9aa49cd7eef8103e5f06e5ef3da844fef2e01290897814b1495dda2be63bfcde" dependencies = [ - "ahash 0.8.6", - "blake3", "block-buffer 0.10.4", "bs58 0.4.0", "bv", - "byteorder", - "cc", "either", "generic-array", - "getrandom 0.1.16", "im", "lazy_static", "log", "memmap2", - "once_cell", - "rand_core 0.6.4", "rustc_version", "serde", "serde_bytes", "serde_derive", - "serde_json", "sha2 0.10.8", "solana-frozen-abi-macro", "subtle", @@ -1904,9 +1958,9 @@ dependencies = [ [[package]] name = "solana-frozen-abi-macro" -version = "1.16.25" +version = "1.18.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f516f992211a2ab70de5c367190575c97e02d156f9f1d8b76886d673f30e88a2" +checksum = "3aeb9e6679ac24dc9087f8bcc31c4912cb729a41897278a1fefe5b0a14636f6c" dependencies = [ "proc-macro2", "quote", @@ -1916,9 +1970,9 @@ dependencies = [ [[package]] name = "solana-logger" -version = "1.16.25" +version = "1.18.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b64def674bfaa4a3f8be7ba19c03c9caec4ec028ba62b9a427ec1bf608a2486" +checksum = "141e96bc474ec11533eeddcd597097308e42965d05601ea1f3c321b879f91534" dependencies = [ "env_logger", "lazy_static", @@ -1927,21 +1981,21 @@ dependencies = [ [[package]] name = "solana-program" -version = "1.16.25" +version = "1.18.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e92350aa5b42564681655331e7e0b9d5c99a442de317ceeb4741efbbe9a6c05" +checksum = "11e245fdb69973415d6d1f08c4ea2c82cce16c1100c800fc4dae396eac9c3305" dependencies = [ "ark-bn254", "ark-ec", "ark-ff", "ark-serialize", - "array-bytes", "base64 0.21.7", "bincode", - "bitflags 1.3.2", + "bitflags", "blake3", "borsh 0.10.3", "borsh 0.9.3", + "borsh 1.5.1", "bs58 0.4.0", "bv", "bytemuck", @@ -1955,14 +2009,14 @@ dependencies = [ "lazy_static", "libc", "libsecp256k1", + "light-poseidon", "log", "memoffset", "num-bigint", - "num-derive", + "num-derive 0.4.2", "num-traits", "parking_lot", - "rand 0.7.3", - "rand_chacha 0.2.2", + "rand 0.8.5", "rustc_version", "rustversion", "serde", @@ -1982,15 +2036,15 @@ dependencies = [ [[package]] name = "solana-sdk" -version = "1.16.25" +version = "1.18.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2087e15c92d4d6b3f085dc12fbe9614141c811f90a54cc418240ac30b608133f" +checksum = "92947ea7072e0dfadc53d79e570c1d13e830b74047ca8b938c3fa799707ebd34" dependencies = [ "assert_matches", "base64 0.21.7", "bincode", - "bitflags 1.3.2", - "borsh 0.10.3", + "bitflags", + "borsh 1.5.1", "bs58 0.4.0", "bytemuck", "byteorder", @@ -2007,13 +2061,14 @@ dependencies = [ "libsecp256k1", "log", "memmap2", - "num-derive", + "num-derive 0.4.2", "num-traits", - "num_enum 0.6.1", + "num_enum", "pbkdf2 0.11.0", "qstring", + "qualifier_attr", "rand 0.7.3", - "rand_chacha 0.2.2", + "rand 0.8.5", "rustc_version", "rustversion", "serde", @@ -2023,6 +2078,7 @@ dependencies = [ "serde_with", "sha2 0.10.8", "sha3 0.10.8", + "siphasher", "solana-frozen-abi", "solana-frozen-abi-macro", "solana-logger", @@ -2035,9 +2091,9 @@ dependencies = [ [[package]] name = "solana-sdk-macro" -version = "1.16.25" +version = "1.18.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e0e0e7ee984b0f9179a1d4f4e9e67ce675de2324b5a98b61d2bdb61be3c19bb" +checksum = "6548235a0d4babb0f410f0a41b05425013f1168a739bfc447860d5eb2d82f849" dependencies = [ "bs58 0.4.0", "proc-macro2", @@ -2046,11 +2102,17 @@ dependencies = [ "syn 2.0.63", ] +[[package]] +name = "solana-security-txt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "468aa43b7edb1f9b7b7b686d5c3aeb6630dc1708e86e31343499dd5c4d775183" + [[package]] name = "solana-zk-token-sdk" -version = "1.16.25" +version = "1.18.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1457c85ab70a518438b9ac2b0c56037b9f6693060dfb617bbb93c7116e4f0c22" +checksum = "43318fb5bd649bc57711188908b561f70ee3e506d26300018af7fd36585fdf90" dependencies = [ "aes-gcm-siv", "base64 0.21.7", @@ -2062,7 +2124,7 @@ dependencies = [ "itertools", "lazy_static", "merlin", - "num-derive", + "num-derive 0.4.2", "num-traits", "rand 0.7.3", "serde", @@ -2077,62 +2139,279 @@ dependencies = [ [[package]] name = "spl-associated-token-account" -version = "1.1.3" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978dba3bcbe88d0c2c58366c254d9ea41c5f73357e72fc0bdee4d6b5fc99c8f4" +checksum = "992d9c64c2564cc8f63a4b508bf3ebcdf2254b0429b13cd1d31adb6162432a5f" dependencies = [ "assert_matches", - "borsh 0.9.3", - "num-derive", + "borsh 0.10.3", + "num-derive 0.4.2", "num-traits", "solana-program", "spl-token", - "spl-token-2022", + "spl-token-2022 1.0.0", + "thiserror", +] + +[[package]] +name = "spl-discriminator" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce5d563b58ef1bb2cdbbfe0dfb9ffdc24903b10ae6a4df2d8f425ece375033f" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator-derive", +] + +[[package]] +name = "spl-discriminator-derive" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07fd7858fc4ff8fb0e34090e41d7eb06a823e1057945c26d480bfc21d2338a93" +dependencies = [ + "quote", + "spl-discriminator-syn", + "syn 2.0.63", +] + +[[package]] +name = "spl-discriminator-syn" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fea7be851bd98d10721782ea958097c03a0c2a07d8d4997041d0ece6319a63" +dependencies = [ + "proc-macro2", + "quote", + "sha2 0.10.8", + "syn 2.0.63", "thiserror", ] [[package]] name = "spl-memo" -version = "3.0.1" +version = "4.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0dc6f70db6bacea7ff25870b016a65ba1d1b6013536f08e4fd79a8f9005325" +checksum = "a49f49f95f2d02111ded31696ab38a081fab623d4c76bd4cb074286db4560836" dependencies = [ "solana-program", ] +[[package]] +name = "spl-pod" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2881dddfca792737c0706fa0175345ab282b1b0879c7d877bad129645737c079" +dependencies = [ + "borsh 0.10.3", + "bytemuck", + "solana-program", + "solana-zk-token-sdk", + "spl-program-error", +] + +[[package]] +name = "spl-program-error" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "249e0318493b6bcf27ae9902600566c689b7dfba9f1bdff5893e92253374e78c" +dependencies = [ + "num-derive 0.4.2", + "num-traits", + "solana-program", + "spl-program-error-derive", + "thiserror", +] + +[[package]] +name = "spl-program-error-derive" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1845dfe71fd68f70382232742e758557afe973ae19e6c06807b2c30f5d5cb474" +dependencies = [ + "proc-macro2", + "quote", + "sha2 0.10.8", + "syn 2.0.63", +] + +[[package]] +name = "spl-tlv-account-resolution" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "062e148d3eab7b165582757453632ffeef490c02c86a48bfdb4988f63eefb3b9" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-type-length-value", +] + +[[package]] +name = "spl-tlv-account-resolution" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "615d381f48ddd2bb3c57c7f7fb207591a2a05054639b18a62e785117dd7a8683" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-type-length-value", +] + [[package]] name = "spl-token" -version = "3.5.0" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e85e168a785e82564160dcb87b2a8e04cee9bfd1f4d488c729d53d6a4bd300d" +checksum = "b9eb465e4bf5ce1d498f05204c8089378c1ba34ef2777ea95852fc53a1fd4fb2" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", - "num_enum 0.5.11", + "num_enum", "solana-program", "thiserror", ] [[package]] name = "spl-token-2022" -version = "0.6.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0043b590232c400bad5ee9eb983ced003d15163c4c5d56b090ac6d9a57457b47" +checksum = "e4abf34a65ba420584a0c35f3903f8d727d1f13ababbdc3f714c6b065a686e86" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", - "num_enum 0.5.11", + "num_enum", "solana-program", "solana-zk-token-sdk", "spl-memo", + "spl-pod", "spl-token", + "spl-token-metadata-interface", + "spl-transfer-hook-interface 0.3.0", + "spl-type-length-value", "thiserror", ] +[[package]] +name = "spl-token-2022" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d697fac19fd74ff472dfcc13f0b442dd71403178ce1de7b5d16f83a33561c059" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive 0.4.2", + "num-traits", + "num_enum", + "solana-program", + "solana-security-txt", + "solana-zk-token-sdk", + "spl-memo", + "spl-pod", + "spl-token", + "spl-token-group-interface", + "spl-token-metadata-interface", + "spl-transfer-hook-interface 0.4.1", + "spl-type-length-value", + "thiserror", +] + +[[package]] +name = "spl-token-group-interface" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b889509d49fa74a4a033ca5dae6c2307e9e918122d97e58562f5c4ffa795c75d" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", +] + +[[package]] +name = "spl-token-metadata-interface" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c16ce3ba6979645fb7627aa1e435576172dd63088dc7848cb09aa331fa1fe4f" +dependencies = [ + "borsh 0.10.3", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-type-length-value", +] + +[[package]] +name = "spl-transfer-hook-interface" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051d31803f873cabe71aec3c1b849f35248beae5d19a347d93a5c9cccc5d5a9b" +dependencies = [ + "arrayref", + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-tlv-account-resolution 0.4.0", + "spl-type-length-value", +] + +[[package]] +name = "spl-transfer-hook-interface" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aabdb7c471566f6ddcee724beb8618449ea24b399e58d464d6b5bc7db550259" +dependencies = [ + "arrayref", + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-tlv-account-resolution 0.5.1", + "spl-type-length-value", +] + +[[package]] +name = "spl-transfer-hook-interface" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825a69d531baa1ff261a29b98fcf08e134249e624052a6b60183bb2eab2fa8ae" +dependencies = [ + "arrayref", + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-tlv-account-resolution 0.5.1", + "spl-type-length-value", +] + +[[package]] +name = "spl-type-length-value" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a468e6f6371f9c69aae760186ea9f1a01c2908351b06a5e0026d21cfc4d7ecac" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -2173,6 +2452,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.63", +] + [[package]] name = "tempfile" version = "3.10.1" @@ -2265,9 +2556,9 @@ checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" [[package]] name = "toml_edit" -version = "0.19.15" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ "indexmap", "toml_datetime", diff --git a/package.json b/package.json index 3b0c252..3e8491d 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,18 @@ "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" }, "dependencies": { - "@coral-xyz/anchor": "^0.28.0", - "@solana/spl-token": "^0.3.8" + "@coral-xyz/anchor": "^0.29.0", + "@solana/spl-token": "^0.3.8", + "tiny-invariant": "^1.3.3" }, "devDependencies": { - "chai": "^4.3.4", - "mocha": "^9.0.3", - "ts-mocha": "^10.0.0", "@types/bn.js": "^5.1.0", "@types/chai": "^4.3.0", "@types/mocha": "^9.0.0", - "typescript": "^4.3.5", - "prettier": "^2.6.2" + "chai": "^4.3.4", + "mocha": "^9.0.3", + "prettier": "^2.6.2", + "ts-mocha": "^10.0.0", + "typescript": "^4.3.5" } -} \ No newline at end of file +} diff --git a/programs/locker/Cargo.toml b/programs/locker/Cargo.toml index 374b35e..44e09a3 100644 --- a/programs/locker/Cargo.toml +++ b/programs/locker/Cargo.toml @@ -19,11 +19,14 @@ localnet = [] staging = [] [dependencies] -anchor-lang = { version = "0.28.0", features = ["event-cpi"] } -anchor-spl = "0.28.0" +anchor-lang = { version = "0.29.0", features = ["event-cpi"] } +anchor-spl = { version = "0.29.0", features = ["metadata", "memo"] } +spl-transfer-hook-interface = "0.5.0" +solana-program = "1.18.21" bytemuck = { version = "1.13.1", features = ["derive", "min_const_generics"] } static_assertions = "1.1.0" num_enum = "0.7.1" [dev-dependencies] -proptest = "1.2.0" \ No newline at end of file +proptest = "1.2.0" +spl-pod = "0.1.0" \ No newline at end of file diff --git a/programs/locker/src/constants/mod.rs b/programs/locker/src/constants/mod.rs new file mode 100644 index 0000000..f56f257 --- /dev/null +++ b/programs/locker/src/constants/mod.rs @@ -0,0 +1 @@ +pub mod transfer_memo; diff --git a/programs/locker/src/constants/transfer_memo.rs b/programs/locker/src/constants/transfer_memo.rs new file mode 100644 index 0000000..77d52ee --- /dev/null +++ b/programs/locker/src/constants/transfer_memo.rs @@ -0,0 +1 @@ +pub const TRANSFER_MEMO_CLAIM_VESTING: &str = "Jup-Lock ClaimVesting"; diff --git a/programs/locker/src/errors.rs b/programs/locker/src/errors.rs index 3d19229..6041d45 100644 --- a/programs/locker/src/errors.rs +++ b/programs/locker/src/errors.rs @@ -9,6 +9,9 @@ pub enum LockerError { #[msg("Frequency is zero")] FrequencyIsZero, + #[msg("Unauthorized")] + Unauthorized, + #[msg("Invalid escrow token address")] InvalidEscrowTokenAddress, @@ -26,4 +29,31 @@ pub enum LockerError { #[msg("Invalid vesting start time")] InvalidVestingStartTime, + + #[msg("Invalid mint account")] + InvalidMintAccount, + + #[msg("Invalid token programId")] + IncorrectTokenProgramId, + + #[msg("Parse token extensions failure")] + ParseTokenExtensionsFailure, + + #[msg("Calculate transfer fee failure")] + TransferFeeCalculationFailure, + + #[msg("Unsupported mint")] + UnsupportedMint, + + #[msg("Invalid remaining accounts")] + InvalidRemainingAccountSlice, + + #[msg("Insufficient remaining accounts")] + InsufficientRemainingAccounts, + + #[msg("Same accounts type is provided more than once")] + DuplicatedRemainingAccountTypes, + + #[msg("Unable to call transfer hook without extra accounts")] + NoTransferHookProgram, } diff --git a/programs/locker/src/instructions/claim.rs b/programs/locker/src/instructions/claim.rs index 2f62867..d9f24d9 100644 --- a/programs/locker/src/instructions/claim.rs +++ b/programs/locker/src/instructions/claim.rs @@ -1,5 +1,7 @@ +use anchor_spl::token::{Token, TokenAccount}; + use crate::*; -use anchor_spl::token::{Token, TokenAccount, Transfer}; +use crate::util::token::transfer_to_recipient; #[event_cpi] #[derive(Accounts)] @@ -13,37 +15,17 @@ pub struct ClaimCtx<'info> { #[account(mut)] pub recipient: Signer<'info>, - #[account(mut, constraint = recipient_token.key() != escrow_token.key() @ LockerError::InvalidRecipientTokenAccount)] + #[account( + mut, constraint = recipient_token.key() != escrow_token.key() @ LockerError::InvalidRecipientTokenAccount + )] pub recipient_token: Box>, /// Token program. pub token_program: Program<'info, Token>, } -impl<'info> ClaimCtx<'info> { - fn transfer_to_recipient(&self, amount: u64) -> Result<()> { - let escrow = self.escrow.load()?; - let escrow_seeds = escrow_seeds!(escrow); - anchor_spl::token::transfer( - CpiContext::new_with_signer( - self.token_program.to_account_info(), - Transfer { - from: self.escrow_token.to_account_info(), - to: self.recipient_token.to_account_info(), - authority: self.escrow.to_account_info(), - }, - &[&escrow_seeds[..]], - ), - amount, - )?; - Ok(()) - } -} - pub fn handle_claim(ctx: Context, max_amount: u64) -> Result<()> { - let current_ts = Clock::get()?.unix_timestamp as u64; let mut escrow = ctx.accounts.escrow.load_mut()?; - let escrow_token = anchor_spl::associated_token::get_associated_token_address( &ctx.accounts.escrow.key(), &escrow.token_mint, @@ -54,24 +36,18 @@ pub fn handle_claim(ctx: Context, max_amount: u64) -> Result<()> { LockerError::InvalidEscrowTokenAddress ); - let claimable_amount = escrow.get_claimable_amount(current_ts)?; - - let amount = claimable_amount.min(max_amount); - escrow.accumulate_claimed_amount(amount)?; - - // localnet debug - #[cfg(feature = "localnet")] - msg!( - "claim amount {} {} {}", - amount, - current_ts, - escrow.cliff_time - ); - + let amount = escrow.claim(max_amount)?; drop(escrow); - ctx.accounts.transfer_to_recipient(amount)?; + transfer_to_recipient( + &ctx.accounts.escrow, + &ctx.accounts.escrow_token, + &ctx.accounts.recipient_token, + &ctx.accounts.token_program, + amount, + )?; + let current_ts = Clock::get()?.unix_timestamp as u64; emit_cpi!(EventClaim { amount, current_ts, diff --git a/programs/locker/src/instructions/create_vesting_escrow.rs b/programs/locker/src/instructions/create_vesting_escrow.rs index 45798b7..ba53f04 100644 --- a/programs/locker/src/instructions/create_vesting_escrow.rs +++ b/programs/locker/src/instructions/create_vesting_escrow.rs @@ -1,8 +1,10 @@ -use crate::safe_math::SafeMath; +use anchor_spl::token::{Token, TokenAccount}; + use crate::*; -use anchor_spl::token::{Token, TokenAccount, Transfer}; -#[derive(AnchorSerialize, AnchorDeserialize)] +use crate::safe_math::SafeMath; +use crate::util::token::transfer_to_escrow; +#[derive(AnchorSerialize, AnchorDeserialize)] /// Accounts for [locker::create_vesting_escrow]. pub struct CreateVestingEscrowParameters { pub vesting_start_time: u64, @@ -19,8 +21,50 @@ impl CreateVestingEscrowParameters { let total_amount = self .cliff_unlock_amount .safe_add(self.amount_per_period.safe_mul(self.number_of_period)?)?; + Ok(total_amount) } + + fn validate(&self) -> Result<()> { + require!( + UpdateRecipientMode::try_from(self.update_recipient_mode).is_ok(), + LockerError::InvalidUpdateRecipientMode, + ); + + require!(self.frequency != 0, LockerError::FrequencyIsZero); + + Ok(()) + } + + pub fn init_escrow( + &self, + vesting_escrow: &AccountLoader, + recipient: Pubkey, + mint: Pubkey, + sender: Pubkey, + base: Pubkey, + escrow_bump: u8, + ) -> Result<()> { + self.validate()?; + + let mut escrow = vesting_escrow.load_init()?; + escrow.init( + self.vesting_start_time, + self.cliff_time, + self.frequency, + self.cliff_unlock_amount, + self.amount_per_period, + self.number_of_period, + recipient, + mint, + sender, + base, + escrow_bump, + self.update_recipient_mode, + ); + + Ok(()) + } } #[event_cpi] @@ -32,8 +76,8 @@ pub struct CreateVestingEscrowCtx<'info> { #[account( init, seeds = [ - b"escrow".as_ref(), - base.key().as_ref(), + b"escrow".as_ref(), + base.key().as_ref(), ], bump, payer = sender, @@ -64,28 +108,6 @@ pub fn handle_create_vesting_escrow( ctx: Context, params: &CreateVestingEscrowParameters, ) -> Result<()> { - let &CreateVestingEscrowParameters { - vesting_start_time, - cliff_time, - frequency, - cliff_unlock_amount, - amount_per_period, - number_of_period, - update_recipient_mode, - } = params; - - require!( - cliff_time >= vesting_start_time, - LockerError::InvalidVestingStartTime - ); - - require!( - UpdateRecipientMode::try_from(update_recipient_mode).is_ok(), - LockerError::InvalidUpdateRecipientMode, - ); - - require!(frequency != 0, LockerError::FrequencyIsZero); - let escrow_token = anchor_spl::associated_token::get_associated_token_address( &ctx.accounts.escrow.key(), &ctx.accounts.sender_token.mint, @@ -96,34 +118,32 @@ pub fn handle_create_vesting_escrow( LockerError::InvalidEscrowTokenAddress ); - let mut escrow = ctx.accounts.escrow.load_init()?; - escrow.init( - vesting_start_time, - cliff_time, - frequency, - cliff_unlock_amount, - amount_per_period, - number_of_period, + params.init_escrow( + &ctx.accounts.escrow, ctx.accounts.recipient.key(), ctx.accounts.sender_token.mint, ctx.accounts.sender.key(), ctx.accounts.base.key(), - *ctx.bumps.get("escrow").unwrap(), - update_recipient_mode, - ); + ctx.bumps.escrow, + )?; - anchor_spl::token::transfer( - CpiContext::new( - ctx.accounts.token_program.to_account_info(), - Transfer { - from: ctx.accounts.sender_token.to_account_info(), - to: ctx.accounts.escrow_token.to_account_info(), - authority: ctx.accounts.sender.to_account_info(), - }, - ), + transfer_to_escrow( + &ctx.accounts.sender, + &ctx.accounts.sender_token, + &ctx.accounts.escrow_token, + &ctx.accounts.token_program, params.get_total_deposit_amount()?, )?; + let &CreateVestingEscrowParameters { + vesting_start_time, + cliff_time, + frequency, + cliff_unlock_amount, + amount_per_period, + number_of_period, + update_recipient_mode, + } = params; emit_cpi!(EventCreateVestingEscrow { cliff_time, frequency, diff --git a/programs/locker/src/instructions/mod.rs b/programs/locker/src/instructions/mod.rs index f0af244..3303a9d 100644 --- a/programs/locker/src/instructions/mod.rs +++ b/programs/locker/src/instructions/mod.rs @@ -9,3 +9,7 @@ pub use create_vesting_escrow_metadata::*; pub mod update_vesting_escrow_recipient; pub use update_vesting_escrow_recipient::*; + +pub mod v2; + +pub use v2::*; diff --git a/programs/locker/src/instructions/v2/claim.rs b/programs/locker/src/instructions/v2/claim.rs new file mode 100644 index 0000000..044a1c6 --- /dev/null +++ b/programs/locker/src/instructions/v2/claim.rs @@ -0,0 +1,89 @@ +use anchor_spl::memo::Memo; +use anchor_spl::token_interface::{ + Mint, TokenAccount, TokenInterface, +}; + +use crate::*; +use crate::constants::transfer_memo; +use crate::util::{AccountsType, MemoTransferContext, parse_remaining_accounts, ParsedRemainingAccounts, transfer_to_recipient_v2}; + +#[event_cpi] +#[derive(Accounts)] +pub struct ClaimV2<'info> { + #[account(mut, has_one = recipient)] + pub escrow: AccountLoader<'info, VestingEscrow>, + + pub mint: Box>, + + pub memo_program: Program<'info, Memo>, + + #[account(mut)] + pub escrow_token: Box>, + + #[account(mut)] + pub recipient: Signer<'info>, + + #[account( + mut, constraint = recipient_token.key() != escrow_token.key() @ LockerError::InvalidRecipientTokenAccount + )] + pub recipient_token: Box>, + + /// Token program. + pub token_program: Interface<'info, TokenInterface>, +} + +pub fn handle_claim_v2<'c: 'info, 'info>( + mut ctx: Context<'_, '_, 'c, 'info, ClaimV2<'info>>, + max_amount: u64, + remaining_accounts_info: Option, +) -> Result<()> { + let mut escrow = ctx.accounts.escrow.load_mut()?; + let escrow_token = anchor_spl::associated_token::get_associated_token_address_with_program_id( + &ctx.accounts.escrow.key(), + &escrow.token_mint, + &ctx.accounts.token_program.key, + ); + + require!( + escrow_token == ctx.accounts.escrow_token.key(), + LockerError::InvalidEscrowTokenAddress + ); + + let amount = escrow.claim(max_amount)?; + drop(escrow); + + // Process remaining accounts + let remaining_accounts = if remaining_accounts_info.is_none() { + ParsedRemainingAccounts::default() + } else { + parse_remaining_accounts( + &mut ctx.remaining_accounts, + &remaining_accounts_info.unwrap().slices, + &[ + AccountsType::TransferHookClaim, + ], + )? + }; + + transfer_to_recipient_v2( + &ctx.accounts.escrow, + &ctx.accounts.mint, + &ctx.accounts.escrow_token, + &ctx.accounts.recipient_token, + &ctx.accounts.token_program, + Some(MemoTransferContext { + memo_program: &ctx.accounts.memo_program, + memo: transfer_memo::TRANSFER_MEMO_CLAIM_VESTING.as_bytes(), + }), + amount, + remaining_accounts.transfer_hook_claim, + )?; + + let current_ts = Clock::get()?.unix_timestamp as u64; + emit_cpi!(EventClaim { + amount, + current_ts, + escrow: ctx.accounts.escrow.key(), + }); + Ok(()) +} diff --git a/programs/locker/src/instructions/v2/create_vesting_escrow.rs b/programs/locker/src/instructions/v2/create_vesting_escrow.rs new file mode 100644 index 0000000..aa9414d --- /dev/null +++ b/programs/locker/src/instructions/v2/create_vesting_escrow.rs @@ -0,0 +1,125 @@ +use anchor_spl::token_interface::{ + Mint, TokenAccount, TokenInterface, +}; + +use crate::*; +use crate::util::{AccountsType, calculate_transfer_fee_included_amount, parse_remaining_accounts, ParsedRemainingAccounts, transfer_to_escrow_v2, validate_mint}; + +#[event_cpi] +#[derive(Accounts)] +pub struct CreateVestingEscrowV2<'info> { + #[account(mut)] + pub base: Signer<'info>, + + #[account( + init, + seeds = [ + b"escrow".as_ref(), + base.key().as_ref(), + ], + bump, + payer = sender, + space = 8 + VestingEscrow::INIT_SPACE + )] + pub escrow: AccountLoader<'info, VestingEscrow>, + + #[account(mut)] + pub escrow_token: Box>, + + #[account(mut)] + pub sender: Signer<'info>, + + #[account(mut)] + pub sender_token: Box>, + + pub mint: Box>, + + #[account(seeds = [b"token_badge", mint.key().as_ref()], bump)] + /// CHECK: checked in the handler + pub token_badge: UncheckedAccount<'info>, + + /// CHECK: recipient account + pub recipient: UncheckedAccount<'info>, + + /// Token program. + pub token_program: Interface<'info, TokenInterface>, + + // system program + pub system_program: Program<'info, System>, +} + +pub fn handle_create_vesting_escrow_v2<'c: 'info, 'info>( + mut ctx: Context<'_, '_, 'c, 'info, CreateVestingEscrowV2<'info>>, + params: &CreateVestingEscrowParameters, + remaining_accounts_info: Option, +) -> Result<()> { + require!( + validate_mint(&ctx.accounts.mint, &ctx.accounts.token_badge).unwrap(), + LockerError::UnsupportedMint, + ); + + let escrow_token = anchor_spl::associated_token::get_associated_token_address_with_program_id( + &ctx.accounts.escrow.key(), + &ctx.accounts.sender_token.mint, + &ctx.accounts.token_program.key, + ); + + require!( + escrow_token == ctx.accounts.escrow_token.key(), + LockerError::InvalidEscrowTokenAddress + ); + + params.init_escrow( + &ctx.accounts.escrow, + ctx.accounts.recipient.key(), + ctx.accounts.sender_token.mint, + ctx.accounts.sender.key(), + ctx.accounts.base.key(), + ctx.bumps.escrow, + )?; + + // Process remaining accounts + let remaining_accounts = if remaining_accounts_info.is_none() { + ParsedRemainingAccounts::default() + } else { + parse_remaining_accounts( + &mut ctx.remaining_accounts, + &remaining_accounts_info.unwrap().slices, + &[ + AccountsType::TransferHookInput, + ], + )? + }; + + transfer_to_escrow_v2( + &ctx.accounts.sender, + &ctx.accounts.mint, + &ctx.accounts.sender_token, + &ctx.accounts.escrow_token, + &ctx.accounts.token_program, + calculate_transfer_fee_included_amount(params.get_total_deposit_amount()?, &ctx.accounts.mint)?, + remaining_accounts.transfer_hook_input, + )?; + + let &CreateVestingEscrowParameters { + vesting_start_time, + cliff_time, + frequency, + cliff_unlock_amount, + amount_per_period, + number_of_period, + update_recipient_mode, + } = params; + emit_cpi!(EventCreateVestingEscrow { + vesting_start_time, + cliff_time, + frequency, + cliff_unlock_amount, + amount_per_period, + number_of_period, + recipient: ctx.accounts.recipient.key(), + escrow: ctx.accounts.escrow.key(), + update_recipient_mode, + }); + Ok(()) +} diff --git a/programs/locker/src/instructions/v2/delete_token_badge.rs b/programs/locker/src/instructions/v2/delete_token_badge.rs new file mode 100644 index 0000000..8b7bbdf --- /dev/null +++ b/programs/locker/src/instructions/v2/delete_token_badge.rs @@ -0,0 +1,39 @@ +use anchor_lang::prelude::*; +use anchor_spl::token_interface::Mint; + +use crate::LockerError; +use crate::state::*; +use crate::util::is_authorized; + +#[derive(Accounts)] +pub struct DeleteTokenBadge<'info> { + pub token_badge_authority: Signer<'info>, + + pub token_mint: InterfaceAccount<'info, Mint>, + + #[account( + mut, + seeds = [ + b"token_badge", + token_mint.key().as_ref(), + ], + bump, + close = receiver + )] + pub token_badge: Account<'info, TokenBadge>, + + /// CHECK: safe, for receiving rent only + #[account(mut)] + pub receiver: UncheckedAccount<'info>, +} + +pub fn handle_delete_token_badge( + ctx: Context, +) -> Result<()> { + require!( + is_authorized(ctx.accounts.token_badge_authority.key), + LockerError::Unauthorized + ); + + Ok(()) +} diff --git a/programs/locker/src/instructions/v2/initialize_token_badge.rs b/programs/locker/src/instructions/v2/initialize_token_badge.rs new file mode 100644 index 0000000..655511a --- /dev/null +++ b/programs/locker/src/instructions/v2/initialize_token_badge.rs @@ -0,0 +1,40 @@ +use anchor_lang::prelude::*; +use anchor_spl::token_interface::Mint; + +use crate::LockerError; +use crate::state::*; +use crate::util::is_authorized; + +#[derive(Accounts)] +pub struct InitializeTokenBadge<'info> { + pub token_badge_authority: Signer<'info>, + + pub token_mint: InterfaceAccount<'info, Mint>, + + #[account(init, + payer = payer, + seeds = [ + b"token_badge", + token_mint.key().as_ref(), + ], + bump, + space = TokenBadge::LEN)] + pub token_badge: Account<'info, TokenBadge>, + + #[account(mut)] + pub payer: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +pub fn handle_initialize_token_badge( + ctx: Context, +) -> Result<()> { + require!( + is_authorized(ctx.accounts.token_badge_authority.key), + LockerError::Unauthorized + ); + + ctx.accounts.token_badge.initialize(ctx.accounts.token_mint.key()); + Ok(()) +} diff --git a/programs/locker/src/instructions/v2/mod.rs b/programs/locker/src/instructions/v2/mod.rs new file mode 100644 index 0000000..5b558da --- /dev/null +++ b/programs/locker/src/instructions/v2/mod.rs @@ -0,0 +1,9 @@ +pub mod claim; +pub mod create_vesting_escrow; +pub mod initialize_token_badge; +pub mod delete_token_badge; + +pub use claim::*; +pub use create_vesting_escrow::*; +pub use initialize_token_badge::*; +pub use delete_token_badge::*; diff --git a/programs/locker/src/lib.rs b/programs/locker/src/lib.rs index 6219b64..c49cb7c 100644 --- a/programs/locker/src/lib.rs +++ b/programs/locker/src/lib.rs @@ -1,21 +1,27 @@ use anchor_lang::prelude::*; +pub use errors::*; +pub use events::*; +pub use instructions::*; +pub use state::*; + +use crate::util::RemainingAccountsInfo; + #[macro_use] pub mod macros; pub mod instructions; -pub use instructions::*; pub mod state; -pub use state::*; pub mod errors; -pub use errors::*; pub mod safe_math; pub mod events; -pub use events::*; + +pub mod util; +pub mod constants; #[cfg(feature = "localnet")] declare_id!("2r5VekMNiWPzi1pWwvJczrdPaZnJG59u91unSrTunwJg"); @@ -56,5 +62,26 @@ pub mod locker { handle_update_vesting_escrow_recipient(ctx, new_recipient, new_recipient_email) } + // V2 instructions + pub fn initialize_token_badge(ctx: Context) -> Result<()> { + handle_initialize_token_badge(ctx) + } + + pub fn delete_token_badge(ctx: Context) -> Result<()> { + handle_delete_token_badge(ctx) + } + + pub fn create_vesting_escrow_v2<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, CreateVestingEscrowV2<'info>>, + params: CreateVestingEscrowParameters, + remaining_accounts_info: Option, + ) -> Result<()> { + handle_create_vesting_escrow_v2(ctx, ¶ms, remaining_accounts_info) + } + + pub fn claim_v2<'c: 'info, 'info>(ctx: Context<'_, '_, 'c, 'info, ClaimV2<'info>>, max_amount: u64, remaining_accounts_info: Option) -> Result<()> { + handle_claim_v2(ctx, max_amount, remaining_accounts_info) + } + // TODO add function to close escrow after all token has been claimed } diff --git a/programs/locker/src/state/mod.rs b/programs/locker/src/state/mod.rs index f510e5f..efca730 100644 --- a/programs/locker/src/state/mod.rs +++ b/programs/locker/src/state/mod.rs @@ -3,3 +3,6 @@ pub use vesting_escrow::*; pub mod vesting_escrow_metadata; pub use vesting_escrow_metadata::*; + +pub mod token_badge; +pub use token_badge::*; \ No newline at end of file diff --git a/programs/locker/src/state/token_badge.rs b/programs/locker/src/state/token_badge.rs new file mode 100644 index 0000000..0aef1b5 --- /dev/null +++ b/programs/locker/src/state/token_badge.rs @@ -0,0 +1,18 @@ +use anchor_lang::prelude::*; + +#[account] +#[derive(Default)] +pub struct TokenBadge { + pub token_mint: Pubkey, // 32 +} + +impl TokenBadge { + pub const LEN: usize = 8 + 32; + + pub fn initialize( + &mut self, + token_mint: Pubkey, + ) { + self.token_mint = token_mint; + } +} \ No newline at end of file diff --git a/programs/locker/src/state/vesting_escrow.rs b/programs/locker/src/state/vesting_escrow.rs index 1cd01cb..fa02fca 100644 --- a/programs/locker/src/state/vesting_escrow.rs +++ b/programs/locker/src/state/vesting_escrow.rs @@ -1,8 +1,9 @@ +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use static_assertions::const_assert_eq; + use crate::*; use self::safe_math::SafeMath; -use num_enum::{IntoPrimitive, TryFromPrimitive}; -use static_assertions::const_assert_eq; #[derive(Copy, Clone, Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] #[repr(u8)] @@ -107,6 +108,16 @@ impl VestingEscrow { Ok(()) } + pub fn claim(&mut self, max_amount: u64) -> Result { + let current_ts = Clock::get()?.unix_timestamp as u64; + let claimable_amount = self.get_claimable_amount(current_ts)?; + + let amount = claimable_amount.min(max_amount); + self.accumulate_claimed_amount(amount)?; + + Ok(amount) + } + pub fn update_recipient(&mut self, new_recipient: Pubkey) { self.recipient = new_recipient; } @@ -114,9 +125,10 @@ impl VestingEscrow { #[cfg(test)] mod escrow_test { - use super::*; use proptest::proptest; + use super::*; + proptest! { #[test] fn test_get_max_unlocked_amount( diff --git a/programs/locker/src/util/mod.rs b/programs/locker/src/util/mod.rs new file mode 100644 index 0000000..9356902 --- /dev/null +++ b/programs/locker/src/util/mod.rs @@ -0,0 +1,9 @@ +pub use remaining_accounts::*; +pub use role::*; +pub use token2022::*; + +pub mod token; +pub mod token2022; +pub mod remaining_accounts; +pub mod role; + diff --git a/programs/locker/src/util/remaining_accounts.rs b/programs/locker/src/util/remaining_accounts.rs new file mode 100644 index 0000000..78d93cb --- /dev/null +++ b/programs/locker/src/util/remaining_accounts.rs @@ -0,0 +1,73 @@ +use anchor_lang::prelude::*; + +use crate::LockerError; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)] +pub enum AccountsType { + TransferHookInput, + TransferHookClaim, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct RemainingAccountsSlice { + pub accounts_type: AccountsType, + pub length: u8, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct RemainingAccountsInfo { + pub slices: Vec, +} + +#[derive(Default)] +pub struct ParsedRemainingAccounts<'a, 'info> { + pub transfer_hook_input: Option<&'a [AccountInfo<'info>]>, + pub transfer_hook_claim: Option<&'a [AccountInfo<'info>]>, +} + +pub fn parse_remaining_accounts<'a, 'info>( + remaining_accounts: &mut &'a [AccountInfo<'info>], + remaining_accounts_slice: &[RemainingAccountsSlice], + valid_accounts_type_list: &[AccountsType], +) -> Result> { + let mut parsed_remaining_accounts = ParsedRemainingAccounts::default(); + + if remaining_accounts_slice.is_empty() { + return Ok(ParsedRemainingAccounts::default()); + } + + for slice in remaining_accounts_slice.iter() { + if !valid_accounts_type_list.contains(&slice.accounts_type) { + return Err(LockerError::InvalidRemainingAccountSlice.into()); + } + + if slice.length == 0 { + continue; + } + + if remaining_accounts.len() < slice.length as usize { + return Err(LockerError::InsufficientRemainingAccounts.into()); + } + + let end_idx = slice.length as usize; + let accounts = &remaining_accounts[0..end_idx]; + *remaining_accounts = &remaining_accounts[end_idx..]; + + match slice.accounts_type { + AccountsType::TransferHookInput => { + if parsed_remaining_accounts.transfer_hook_input.is_some() { + return Err(LockerError::DuplicatedRemainingAccountTypes.into()); + } + parsed_remaining_accounts.transfer_hook_input = Some(accounts); + } + AccountsType::TransferHookClaim => { + if parsed_remaining_accounts.transfer_hook_claim.is_some() { + return Err(LockerError::DuplicatedRemainingAccountTypes.into()); + } + parsed_remaining_accounts.transfer_hook_claim = Some(accounts); + } + } + } + + Ok(parsed_remaining_accounts) +} diff --git a/programs/locker/src/util/role.rs b/programs/locker/src/util/role.rs new file mode 100644 index 0000000..ce4cfa7 --- /dev/null +++ b/programs/locker/src/util/role.rs @@ -0,0 +1,20 @@ +use solana_program::pubkey; +use solana_program::pubkey::Pubkey; + +pub fn is_authorized(pubkey: &Pubkey) -> bool { + let guardians: Vec = vec![ + pubkey!("4U8keyQCV8NFMCevhRJffLawYiUZMyeUrwBjaMcZkGeh"), // soju + pubkey!("4zvTjdpyr3SAgLeSpCnq4KaHvX2j5SbkwxYydzbfqhRQ"), // zhen + pubkey!("5unTfT2kssBuNvHPY6LbJfJpLqEcdMxGYLWHwShaeTLi"), // tian + pubkey!("ChSAh3XXTxpp5n2EmgSCm6vVvVPoD1L9VrK3mcQkYz7m"), // ben + pubkey!("DHLXnJdACTY83yKwnUkeoDjqi4QBbsYGa1v8tJL76ViX"), // andrew + #[cfg(feature = "localnet")] + pubkey!("5k2hyrHp5haXrwFCu7Zw1fsyg8r1eBkHy7iF9AMDD324"), // test + ]; + + if !guardians.contains(pubkey) { + return false; + } + + true +} \ No newline at end of file diff --git a/programs/locker/src/util/token.rs b/programs/locker/src/util/token.rs new file mode 100644 index 0000000..afe92b6 --- /dev/null +++ b/programs/locker/src/util/token.rs @@ -0,0 +1,51 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::{Token, TokenAccount, Transfer}; + +use crate::VestingEscrow; + +pub fn transfer_to_escrow<'info>( + sender: &Signer<'info>, + sender_token: &Account<'info, TokenAccount>, + escrow_token: &Account<'info, TokenAccount>, + token_program: &Program<'info, Token>, + amount: u64, +) -> Result<()> { + anchor_spl::token::transfer( + CpiContext::new( + token_program.to_account_info(), + Transfer { + from: sender_token.to_account_info(), + to: escrow_token.to_account_info(), + authority: sender.to_account_info(), + }, + ), + amount, + )?; + + Ok(()) +} + +pub fn transfer_to_recipient<'info>( + escrow: &AccountLoader<'info, VestingEscrow>, + escrow_token: &Account<'info, TokenAccount>, + recipient_token: &Account<'info, TokenAccount>, + token_program: &Program<'info, Token>, + amount: u64, +) -> Result<()> { + let escrow_state = escrow.load()?; + let escrow_seeds = escrow_seeds!(escrow_state); + + anchor_spl::token::transfer( + CpiContext::new_with_signer( + token_program.to_account_info(), + Transfer { + from: escrow_token.to_account_info(), + to: recipient_token.to_account_info(), + authority: escrow.to_account_info(), + }, + &[&escrow_seeds[..]], + ), + amount, + )?; + Ok(()) +} \ No newline at end of file diff --git a/programs/locker/src/util/token2022.rs b/programs/locker/src/util/token2022.rs new file mode 100644 index 0000000..ee33dbd --- /dev/null +++ b/programs/locker/src/util/token2022.rs @@ -0,0 +1,344 @@ +use anchor_lang::prelude::*; +use anchor_spl::memo; +use anchor_spl::memo::{BuildMemo, Memo}; +use anchor_spl::token::Token; +use anchor_spl::token_2022::spl_token_2022::{self, extension::{self, StateWithExtensions}}; +use anchor_spl::token_2022::spl_token_2022::extension::transfer_fee::{MAX_FEE_BASIS_POINTS, TransferFee, TransferFeeConfig}; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use anchor_spl::token_interface::spl_token_2022::extension::BaseStateWithExtensions; + +use crate::{LockerError, TokenBadge, VestingEscrow}; + +const ONE_IN_BASIS_POINTS: u128 = MAX_FEE_BASIS_POINTS as u128; + +#[derive(Clone, Copy)] +pub struct MemoTransferContext<'a, 'info> { + pub memo_program: &'a Program<'info, Memo>, + pub memo: &'static [u8], +} + +pub fn transfer_to_escrow_v2<'a, 'c: 'info, 'info>( + sender: &'a Signer<'info>, + token_mint: &InterfaceAccount<'info, Mint>, + sender_token: &InterfaceAccount<'info, TokenAccount>, + escrow_token: &InterfaceAccount<'info, TokenAccount>, + token_program: &Interface<'info, TokenInterface>, + amount: u64, + transfer_hook_accounts: Option<&'c [AccountInfo<'info>]>, +) -> Result<()> { + let mut instruction = spl_token_2022::instruction::transfer_checked( + token_program.key, + &sender_token.key(), + &token_mint.key(), // mint + &escrow_token.key(), // to + sender.key, // authority + &[], + // The transfer amount should include fee + amount, + token_mint.decimals, + )?; + + let mut account_infos = vec![ + token_program.to_account_info(), + sender_token.to_account_info(), + token_mint.to_account_info(), + escrow_token.to_account_info(), + sender.to_account_info(), + ]; + + // TransferHook extension + if let Some(hook_program_id) = get_transfer_hook_program_id(token_mint)? { + let Some(transfer_hook_accounts) = transfer_hook_accounts else { + return Err(LockerError::NoTransferHookProgram.into()); + }; + + spl_transfer_hook_interface::onchain::add_extra_accounts_for_execute_cpi( + &mut instruction, + &mut account_infos, + &hook_program_id, + sender_token.to_account_info(), + token_mint.to_account_info(), + escrow_token.to_account_info(), + sender.to_account_info(), + amount, + transfer_hook_accounts, + )?; + } else { + require!( + transfer_hook_accounts.is_none(), + LockerError::NoTransferHookProgram + ); + } + + solana_program::program::invoke(&instruction, &account_infos)?; + + Ok(()) +} + +pub fn transfer_to_recipient_v2<'c: 'info, 'info>( + escrow: &AccountLoader<'info, VestingEscrow>, + token_mint: &InterfaceAccount<'info, Mint>, + escrow_token: &InterfaceAccount<'info, TokenAccount>, + recipient_account: &InterfaceAccount<'info, TokenAccount>, + token_program: &Interface<'info, TokenInterface>, + memo_transfer_context: Option>, + amount: u64, + transfer_hook_accounts: Option<&'c [AccountInfo<'info>]>, +) -> Result<()> { + let escrow_state = escrow.load()?; + let escrow_seeds = escrow_seeds!(escrow_state); + + if let Some(memo_ctx) = memo_transfer_context { + if is_transfer_memo_required(&recipient_account)? { + memo::build_memo( + CpiContext::new(memo_ctx.memo_program.to_account_info(), BuildMemo {}), + memo_ctx.memo, + )?; + } + } + + let mut instruction = spl_token_2022::instruction::transfer_checked( + token_program.key, + &escrow_token.key(), + &token_mint.key(), // mint + &recipient_account.key(), // to + &escrow.key(), // authority + &[], + amount, + token_mint.decimals, + )?; + + let mut account_infos = vec![ + escrow_token.to_account_info(), + token_mint.to_account_info(), + recipient_account.to_account_info(), + escrow.to_account_info(), + ]; + + // TransferHook extension + if let Some(hook_program_id) = get_transfer_hook_program_id(token_mint)? { + let Some(transfer_hook_accounts) = transfer_hook_accounts else { + return Err(LockerError::NoTransferHookProgram.into()); + }; + + spl_transfer_hook_interface::onchain::add_extra_accounts_for_execute_cpi( + &mut instruction, + &mut account_infos, + &hook_program_id, + escrow_token.to_account_info(), + token_mint.to_account_info(), + recipient_account.to_account_info(), + escrow.to_account_info(), + amount, + transfer_hook_accounts, + )?; + } else { + require!( + transfer_hook_accounts.is_none(), + LockerError::NoTransferHookProgram + ); + } + + solana_program::program::invoke_signed( + &instruction, + &account_infos, + &[&escrow_seeds[..]], + )?; + + Ok(()) +} + +pub fn validate_mint(token_mint: &InterfaceAccount, token_badge: &UncheckedAccount) -> Result { + let is_token_badge_initialized = is_token_badge_initialized( + token_mint.key(), + token_badge, + )?; + + let token_mint_info = token_mint.to_account_info(); + + // mint owned by Token Program is supported by default + if *token_mint_info.owner == Token::id() { + return Ok(true); + } + + // seems like other programs don't like to support token-2022 native_mint :) + if spl_token_2022::native_mint::check_id(&token_mint.key()) { + return Ok(false); + } + + // reject if mint has freeze_authority, unless its token badge is initialized + if token_mint.freeze_authority.is_some() && !is_token_badge_initialized { + return Ok(false); + } + + let token_mint_data = token_mint_info.try_borrow_data()?; + let token_mint_unpacked = StateWithExtensions::::unpack(&token_mint_data)?; + + let extensions = token_mint_unpacked.get_extension_types()?; + for extension in extensions { + match extension { + // supported + extension::ExtensionType::TransferFeeConfig => {} + extension::ExtensionType::TokenMetadata => {} + extension::ExtensionType::MetadataPointer => {} + // partially supported + extension::ExtensionType::ConfidentialTransferMint => { + // Supported, but non-confidential transfer only + // + // According to the document (https://solana.com/developers/guides/token-extensions/getting-started#what-extensions-are-compatible-with-each-other) + // We prioritize TransferHook + } + extension::ExtensionType::ConfidentialTransferFeeConfig => { + // Supported, but non-confidential transfer only + } + // supported if token badge is initialized + extension::ExtensionType::PermanentDelegate => { + if !is_token_badge_initialized { return Ok(false); } + } + extension::ExtensionType::TransferHook => { + if !is_token_badge_initialized { return Ok(false); } + } + extension::ExtensionType::MintCloseAuthority => { + if !is_token_badge_initialized { return Ok(false); } + } + // mint has unknown or unsupported extensions + _ => { return Ok(false); } + } + } + + return Ok(true); +} + +// This function calculate the pre amount (with fee) require to transfer `amount` of token +pub fn calculate_transfer_fee_included_amount(amount: u64, mint: &InterfaceAccount) -> Result { + let mint_info = mint.to_account_info(); + if *mint_info.owner == Token::id() { + return Ok(0); + } + + let token_mint_data = mint_info.try_borrow_data()?; + let token_mint_unpacked = StateWithExtensions::::unpack(&token_mint_data)?; + let actual_amount: u64 = if let Ok(transfer_fee_config) = token_mint_unpacked.get_extension::() { + let transfer_fee = transfer_fee_config.get_epoch_fee(Clock::get()?.epoch); + calculate_pre_fee_amount(transfer_fee, amount).ok_or(LockerError::TransferFeeCalculationFailure)? + } else { + 0 + }; + + Ok(actual_amount) +} + +// Memo Extension support +pub fn is_transfer_memo_required(token_account: &InterfaceAccount) -> Result { + let token_account_info = token_account.to_account_info(); + if *token_account_info.owner == Token::id() { + return Ok(false); + } + + let token_account_data = token_account_info.try_borrow_data()?; + let token_account_unpacked = StateWithExtensions::::unpack(&token_account_data)?; + let extension = token_account_unpacked.get_extension::(); + + return if let Ok(memo_transfer) = extension { + Ok(memo_transfer.require_incoming_transfer_memos.into()) + } else { + Ok(false) + }; +} + +fn get_transfer_hook_program_id<'info>( + token_mint: &InterfaceAccount<'info, Mint>, +) -> Result> { + let token_mint_info = token_mint.to_account_info(); + if *token_mint_info.owner == Token::id() { + return Ok(None); + } + + let token_mint_data = token_mint_info.try_borrow_data()?; + let token_mint_unpacked = + StateWithExtensions::::unpack(&token_mint_data)?; + Ok(extension::transfer_hook::get_program_id( + &token_mint_unpacked, + )) +} + +fn is_token_badge_initialized( + token_mint_key: Pubkey, + token_badge: &UncheckedAccount, +) -> Result { + if *token_badge.owner != crate::id() { + return Ok(false); + } + + let token_badge = TokenBadge::try_deserialize( + &mut token_badge.data.borrow().as_ref() + )?; + + Ok(token_badge.token_mint == token_mint_key) +} + +pub fn calculate_pre_fee_amount(transfer_fee: &TransferFee, post_fee_amount: u64) -> Option { + if post_fee_amount == 0 { + return Some(0); + } + let maximum_fee = u64::from(transfer_fee.maximum_fee); + let transfer_fee_basis_points = u16::from(transfer_fee.transfer_fee_basis_points) as u128; + if transfer_fee_basis_points == 0 { + Some(post_fee_amount) + } else if transfer_fee_basis_points == ONE_IN_BASIS_POINTS { + Some(maximum_fee.checked_add(post_fee_amount)?) + } else { + let numerator = (post_fee_amount as u128).checked_mul(ONE_IN_BASIS_POINTS)?; + let denominator = ONE_IN_BASIS_POINTS.checked_sub(transfer_fee_basis_points)?; + // let raw_pre_fee_amount = ceil_div(numerator, denominator)?; + let raw_pre_fee_amount = numerator + .checked_add(ONE_IN_BASIS_POINTS)? + .checked_sub(1)? + .checked_div(denominator)?; + + if raw_pre_fee_amount.checked_sub(post_fee_amount as u128)? >= maximum_fee as u128 { + post_fee_amount.checked_add(maximum_fee) + } else { + // should return `None` if `pre_fee_amount` overflows + u64::try_from(raw_pre_fee_amount).ok() + } + } +} + +/// Calculate the fee that would produce the given output +pub fn calculate_inverse_fee(transfer_fee: &TransferFee, post_fee_amount: u64) -> Option { + let pre_fee_amount = calculate_pre_fee_amount(&transfer_fee, post_fee_amount)?; + transfer_fee.calculate_fee(pre_fee_amount) +} + +#[cfg(test)] +mod token2022_tests { + use proptest::prelude::*; + use spl_pod::primitives::{PodU16, PodU64}; + + use super::*; + + const MAX_FEE_BASIS_POINTS: u16 = 100; + proptest! { + #[test] + fn inverse_fee_relationship( + transfer_fee_basis_points in 0u16..MAX_FEE_BASIS_POINTS, + maximum_fee in u64::MIN..=u64::MAX, + amount_in in 0..=u64::MAX + ) { + let transfer_fee = TransferFee { + epoch: PodU64::from(0), + maximum_fee: PodU64::from(maximum_fee), + transfer_fee_basis_points: PodU16::from(transfer_fee_basis_points), + }; + let fee = transfer_fee.calculate_fee(amount_in).unwrap(); + let amount_out = amount_in.checked_sub(fee).unwrap(); + let fee_exact_out = calculate_inverse_fee(&transfer_fee, amount_out).unwrap(); + assert!(fee_exact_out >= fee); + if fee_exact_out - fee > 0 { + println!("dif {} {} {} {} {}",fee_exact_out - fee, fee, fee_exact_out, maximum_fee, amount_in); + } + + } + } +} diff --git a/sdk/artifacts/locker.json b/sdk/artifacts/locker.json new file mode 100644 index 0000000..d390181 --- /dev/null +++ b/sdk/artifacts/locker.json @@ -0,0 +1,777 @@ +{ + "version": "0.2.2", + "name": "locker", + "instructions": [ + { + "name": "createVestingEscrow", + "accounts": [ + { + "name": "base", + "isMut": true, + "isSigner": true + }, + { + "name": "escrow", + "isMut": true, + "isSigner": false + }, + { + "name": "escrowToken", + "isMut": true, + "isSigner": false + }, + { + "name": "sender", + "isMut": true, + "isSigner": true + }, + { + "name": "senderToken", + "isMut": true, + "isSigner": false + }, + { + "name": "recipient", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "Token program." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "CreateVestingEscrowParameters" + } + } + ] + }, + { + "name": "claim", + "accounts": [ + { + "name": "escrow", + "isMut": true, + "isSigner": false + }, + { + "name": "escrowToken", + "isMut": true, + "isSigner": false + }, + { + "name": "recipient", + "isMut": true, + "isSigner": true + }, + { + "name": "recipientToken", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "Token program." + ] + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "maxAmount", + "type": "u64" + } + ] + }, + { + "name": "createVestingEscrowMetadata", + "accounts": [ + { + "name": "escrow", + "isMut": true, + "isSigner": false, + "docs": [ + "The [Escrow]." + ] + }, + { + "name": "creator", + "isMut": false, + "isSigner": true, + "docs": [ + "Creator of the escrow." + ] + }, + { + "name": "escrowMetadata", + "isMut": true, + "isSigner": false, + "docs": [ + "The [ProposalMeta]." + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true, + "docs": [ + "Payer of the [ProposalMeta]." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "System program." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "CreateVestingEscrowMetadataParameters" + } + } + ] + }, + { + "name": "updateVestingEscrowRecipient", + "accounts": [ + { + "name": "escrow", + "isMut": true, + "isSigner": false, + "docs": [ + "Escrow." + ] + }, + { + "name": "escrowMetadata", + "isMut": true, + "isSigner": false, + "isOptional": true, + "docs": [ + "Escrow metadata." + ] + }, + { + "name": "signer", + "isMut": true, + "isSigner": true, + "docs": [ + "Signer." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "System program." + ] + } + ], + "args": [ + { + "name": "newRecipient", + "type": "publicKey" + }, + { + "name": "newRecipientEmail", + "type": { + "option": "string" + } + } + ] + }, + { + "name": "createVestingEscrowV2", + "accounts": [ + { + "name": "base", + "isMut": true, + "isSigner": true + }, + { + "name": "escrow", + "isMut": true, + "isSigner": false + }, + { + "name": "escrowToken", + "isMut": true, + "isSigner": false + }, + { + "name": "sender", + "isMut": true, + "isSigner": true + }, + { + "name": "senderToken", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "recipient", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "Token program." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "CreateVestingEscrowParameters" + } + }, + { + "name": "memo", + "type": "string" + } + ] + }, + { + "name": "claimV2", + "accounts": [ + { + "name": "escrow", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "escrowToken", + "isMut": true, + "isSigner": false + }, + { + "name": "recipient", + "isMut": true, + "isSigner": true + }, + { + "name": "recipientToken", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "Token program." + ] + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "maxAmount", + "type": "u64" + } + ] + } + ], + "accounts": [ + { + "name": "VestingEscrowMetadata", + "docs": [ + "Metadata about an escrow." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "escrow", + "docs": [ + "The [Escrow]." + ], + "type": "publicKey" + }, + { + "name": "name", + "docs": [ + "Name of escrow." + ], + "type": "string" + }, + { + "name": "description", + "docs": [ + "Description of escrow." + ], + "type": "string" + }, + { + "name": "creatorEmail", + "docs": [ + "Email of creator" + ], + "type": "string" + }, + { + "name": "recipientEmail", + "docs": [ + "Email of recipient" + ], + "type": "string" + } + ] + } + }, + { + "name": "VestingEscrow", + "type": { + "kind": "struct", + "fields": [ + { + "name": "recipient", + "docs": [ + "recipient address" + ], + "type": "publicKey" + }, + { + "name": "tokenMint", + "docs": [ + "token mint" + ], + "type": "publicKey" + }, + { + "name": "creator", + "docs": [ + "creator of the escrow" + ], + "type": "publicKey" + }, + { + "name": "base", + "docs": [ + "escrow base key" + ], + "type": "publicKey" + }, + { + "name": "escrowBump", + "docs": [ + "escrow bump" + ], + "type": "u8" + }, + { + "name": "updateRecipientMode", + "docs": [ + "update_recipient_mode" + ], + "type": "u8" + }, + { + "name": "padding0", + "docs": [ + "padding" + ], + "type": { + "array": [ + "u8", + 6 + ] + } + }, + { + "name": "startTime", + "docs": [ + "start time" + ], + "type": "u64" + }, + { + "name": "frequency", + "docs": [ + "frequency" + ], + "type": "u64" + }, + { + "name": "initialUnlockAmount", + "docs": [ + "initial unlock amount" + ], + "type": "u64" + }, + { + "name": "amountPerPeriod", + "docs": [ + "amount per period" + ], + "type": "u64" + }, + { + "name": "numberOfPeriod", + "docs": [ + "number of period" + ], + "type": "u64" + }, + { + "name": "totalClaimedAmount", + "docs": [ + "total claimed amount" + ], + "type": "u64" + }, + { + "name": "padding1", + "docs": [ + "padding" + ], + "type": { + "array": [ + "u8", + 8 + ] + } + }, + { + "name": "buffer", + "docs": [ + "buffer" + ], + "type": { + "array": [ + "u128", + 6 + ] + } + } + ] + } + } + ], + "types": [ + { + "name": "CreateVestingEscrowMetadataParameters", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "description", + "type": "string" + }, + { + "name": "creatorEmail", + "type": "string" + }, + { + "name": "recipientEmail", + "type": "string" + } + ] + } + }, + { + "name": "CreateVestingEscrowParameters", + "docs": [ + "Accounts for [locker::create_vesting_escrow]." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "startTime", + "type": "u64" + }, + { + "name": "frequency", + "type": "u64" + }, + { + "name": "initialUnlockAmount", + "type": "u64" + }, + { + "name": "amountPerPeriod", + "type": "u64" + }, + { + "name": "numberOfPeriod", + "type": "u64" + }, + { + "name": "updateRecipientMode", + "type": "u8" + } + ] + } + }, + { + "name": "UpdateRecipientMode", + "type": { + "kind": "enum", + "variants": [ + { + "name": "NeitherCreatorOrRecipient" + }, + { + "name": "OnlyCreator" + }, + { + "name": "OnlyRecipient" + }, + { + "name": "EitherCreatorAndRecipient" + } + ] + } + } + ], + "events": [ + { + "name": "EventCreateVestingEscrow", + "fields": [ + { + "name": "startTime", + "type": "u64", + "index": false + }, + { + "name": "frequency", + "type": "u64", + "index": false + }, + { + "name": "initialUnlockAmount", + "type": "u64", + "index": false + }, + { + "name": "amountPerPeriod", + "type": "u64", + "index": false + }, + { + "name": "numberOfPeriod", + "type": "u64", + "index": false + }, + { + "name": "updateRecipientMode", + "type": "u8", + "index": false + }, + { + "name": "recipient", + "type": "publicKey", + "index": false + }, + { + "name": "escrow", + "type": "publicKey", + "index": false + } + ] + }, + { + "name": "EventClaim", + "fields": [ + { + "name": "amount", + "type": "u64", + "index": false + }, + { + "name": "currentTs", + "type": "u64", + "index": false + }, + { + "name": "escrow", + "type": "publicKey", + "index": false + } + ] + }, + { + "name": "EventUpdateVestingEscrowRecipient", + "fields": [ + { + "name": "escrow", + "type": "publicKey", + "index": false + }, + { + "name": "oldRecipient", + "type": "publicKey", + "index": false + }, + { + "name": "newRecipient", + "type": "publicKey", + "index": false + }, + { + "name": "signer", + "type": "publicKey", + "index": false + } + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "MathOverflow", + "msg": "Math operation overflow" + }, + { + "code": 6001, + "name": "FrequencyIsZero", + "msg": "Frequency is zero" + }, + { + "code": 6002, + "name": "InvalidEscrowTokenAddress", + "msg": "Invalid escrow token address" + }, + { + "code": 6003, + "name": "InvalidUpdateRecipientMode", + "msg": "Invalid update recipient mode" + }, + { + "code": 6004, + "name": "NotPermitToDoThisAction", + "msg": "Not permit to do this action" + }, + { + "code": 6005, + "name": "InvalidRecipientTokenAccount", + "msg": "Invalid recipient token account" + }, + { + "code": 6006, + "name": "InvalidEscrowMetadata", + "msg": "Invalid escrow metadata" + }, + { + "code": 6007, + "name": "InvalidMintAccount", + "msg": "Invalid mint account" + }, + { + "code": 6008, + "name": "IncorrectTokenProgramId", + "msg": "Invalid token programId" + }, + { + "code": 6009, + "name": "ParseTokenExtensionsFailure", + "msg": "Parse token extensions failure" + }, + { + "code": 6010, + "name": "TransferFeeCalculationFailure", + "msg": "Calculate transfer fee failure" + }, + { + "code": 6011, + "name": "UnsupportedMint", + "msg": "Unsupported mint" + } + ], + "metadata": { + "address": "2r5VekMNiWPzi1pWwvJczrdPaZnJG59u91unSrTunwJg" + } +} \ No newline at end of file diff --git a/tests/escrow_metadata.ts b/tests/escrow_metadata.ts index 5e19bbf..c5fe275 100644 --- a/tests/escrow_metadata.ts +++ b/tests/escrow_metadata.ts @@ -1,105 +1,130 @@ import * as anchor from "@coral-xyz/anchor"; import { web3 } from "@coral-xyz/anchor"; import { - ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID, - createMint, - getOrCreateAssociatedTokenAccount, - mintTo, + createAssociatedTokenAccountIdempotent, + createInitializeMint2Instruction, + mintTo, + TOKEN_PROGRAM_ID, } from "@solana/spl-token"; import { BN } from "bn.js"; +import { createAndFundWallet, getCurrentBlockTime } from "./common"; import { - createAndFundWallet, - getCurrentBlockTime, - sleep, -} from "./common"; -import { claimToken, createEscrowMetadata, createLockerProgram, createVestingPlan } from "./locker_utils"; - + createEscrowMetadata, + createLockerProgram, + createVestingPlan, +} from "./locker_utils"; +import { + sendAndConfirmTransaction, + SystemProgram, + Transaction, +} from "@solana/web3.js"; const provider = anchor.AnchorProvider.env(); describe("Escrow metadata", () => { - const tokenDecimal = 8; - let TOKEN: web3.PublicKey; - let UserKP: web3.Keypair; - let ReceipentKP: web3.Keypair; + const tokenDecimal = 8; + let mintAuthority: web3.Keypair; + let mintKeypair: web3.Keypair; + let TOKEN: web3.PublicKey; + + let UserKP: web3.Keypair; + let RecipientKP: web3.Keypair; + let RecipientToken: web3.PublicKey; + + let mintAmount: bigint; + before(async () => { + { + const result = await createAndFundWallet(provider.connection); + UserKP = result.keypair; + } + { + const result = await createAndFundWallet(provider.connection); + RecipientKP = result.keypair; + } - before(async () => { - { - const result = await createAndFundWallet(provider.connection); - UserKP = result.keypair; - } - { - const result = await createAndFundWallet(provider.connection); - ReceipentKP = result.keypair; - } + mintAuthority = new web3.Keypair(); + mintKeypair = new web3.Keypair(); + TOKEN = mintKeypair.publicKey; - TOKEN = await createMint( - provider.connection, - UserKP, - UserKP.publicKey, - null, - tokenDecimal, - web3.Keypair.generate(), - null, - TOKEN_PROGRAM_ID - ); + mintAmount = BigInt(1_000_000 * Math.pow(10, tokenDecimal)); // Mint 1,000,000 tokens - const userToken = await getOrCreateAssociatedTokenAccount( - provider.connection, - UserKP, - TOKEN, - UserKP.publicKey, - false, - "confirmed", - { - commitment: "confirmed", - }, - TOKEN_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID - ); - // userBTC = userTokenX.address; - await mintTo( - provider.connection, - UserKP, - TOKEN, - userToken.address, - UserKP.publicKey, - 100 * 10 ** tokenDecimal, - [], - { - commitment: "confirmed", - }, - TOKEN_PROGRAM_ID - ); + // Step 2 - Create a New Token + const mintLamports = + await provider.connection.getMinimumBalanceForRentExemption(82); + const mintTransaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: UserKP.publicKey, + newAccountPubkey: TOKEN, + space: 82, + lamports: mintLamports, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMint2Instruction( + TOKEN, // Mint account + tokenDecimal, // Decimals + mintAuthority.publicKey, // Mint authority + null, // Freeze authority + TOKEN_PROGRAM_ID // Token program ID + ) + ); + await sendAndConfirmTransaction( + provider.connection, + mintTransaction, + [UserKP, mintKeypair], + undefined + ); + + const userToken = await createAssociatedTokenAccountIdempotent( + provider.connection, + UserKP, + TOKEN, + UserKP.publicKey, + {}, + TOKEN_PROGRAM_ID + ); + + await mintTo( + provider.connection, + UserKP, + TOKEN, + userToken, + mintAuthority, + mintAmount, + [], + undefined, + TOKEN_PROGRAM_ID + ); + }); + it("Full flow", async () => { + console.log("Create vesting plan"); + const program = createLockerProgram(new anchor.Wallet(UserKP)); + let currentBlockTime = await getCurrentBlockTime( + program.provider.connection + ); + const cliffTime = new BN(currentBlockTime).add(new BN(5)); + let escrow = await createVestingPlan({ + ownerKeypair: UserKP, + tokenMint: TOKEN, + vestingStartTime: new BN(0), + isAssertion: true, + cliffTime, + frequency: new BN(1), + cliffUnlockAmount: new BN(100_000), + amountPerPeriod: new BN(50_000), + numberOfPeriod: new BN(2), + recipient: RecipientKP.publicKey, + updateRecipientMode: 0, }); - it("Full flow", async () => { - console.log("Create vesting plan"); - const program = createLockerProgram(new anchor.Wallet(UserKP)); - let currentBlockTime = await getCurrentBlockTime(program.provider.connection); - const cliffTime = new BN(currentBlockTime).add(new BN(5)); - let escrow = await createVestingPlan({ - ownerKeypair: UserKP, - tokenMint: TOKEN, - vestingStartTime: new BN(0), - isAssertion: true, - cliffTime, - frequency: new BN(1), - cliffUnlockAmount: new BN(100_000), - amountPerPeriod: new BN(50_000), - numberOfPeriod: new BN(2), - recipient: ReceipentKP.publicKey, - updateRecipientMode: 0, - }); - console.log("Create escrow metadata"); - await createEscrowMetadata({ - escrow, - name: "Jupiter lock", - description: "This is jupiter lock", - creatorEmail: "andrew@raccoons.dev", - recipientEmail: "max@raccoons.dev", - creator: UserKP, - isAssertion: true - }); + + console.log("Create escrow metadata"); + await createEscrowMetadata({ + escrow, + name: "Jupiter lock", + description: "This is jupiter lock", + creatorEmail: "andrew@raccoons.dev", + recipientEmail: "max@raccoons.dev", + creator: UserKP, + isAssertion: true, }); + }); }); diff --git a/tests/locker.ts b/tests/locker.ts index 1d3fe9d..a85ca97 100644 --- a/tests/locker.ts +++ b/tests/locker.ts @@ -1,29 +1,37 @@ import * as anchor from "@coral-xyz/anchor"; import { web3 } from "@coral-xyz/anchor"; import { - ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID, - createMint, - getOrCreateAssociatedTokenAccount, + createAssociatedTokenAccountIdempotent, + createInitializeMint2Instruction, mintTo, + TOKEN_PROGRAM_ID, } from "@solana/spl-token"; import { BN } from "bn.js"; +import { createAndFundWallet, getCurrentBlockTime, sleep } from "./common"; import { - createAndFundWallet, - getCurrentBlockTime, - sleep, -} from "./common"; -import { claimToken, createLockerProgram, createVestingPlan } from "./locker_utils"; - + claimToken, + createLockerProgram, + createVestingPlan, +} from "./locker_utils"; +import { + sendAndConfirmTransaction, + SystemProgram, + Transaction, +} from "@solana/web3.js"; const provider = anchor.AnchorProvider.env(); describe("Full flow", () => { const tokenDecimal = 8; + let mintAuthority: web3.Keypair; + let mintKeypair: web3.Keypair; let TOKEN: web3.PublicKey; + let UserKP: web3.Keypair; - let ReceipentKP: web3.Keypair; - let ReceipentToken: web3.PublicKey; + let RecipientKP: web3.Keypair; + let RecipientToken: web3.PublicKey; + + let mintAmount: bigint; before(async () => { { @@ -32,69 +40,78 @@ describe("Full flow", () => { } { const result = await createAndFundWallet(provider.connection); - ReceipentKP = result.keypair; + RecipientKP = result.keypair; } - TOKEN = await createMint( + mintAuthority = new web3.Keypair(); + mintKeypair = new web3.Keypair(); + TOKEN = mintKeypair.publicKey; + + mintAmount = BigInt(1_000_000 * Math.pow(10, tokenDecimal)); // Mint 1,000,000 tokens + + // Step 2 - Create a New Token + const mintLamports = + await provider.connection.getMinimumBalanceForRentExemption(82); + const mintTransaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: UserKP.publicKey, + newAccountPubkey: TOKEN, + space: 82, + lamports: mintLamports, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMint2Instruction( + TOKEN, // Mint account + tokenDecimal, // Decimals + mintAuthority.publicKey, // Mint authority + null, // Freeze authority + TOKEN_PROGRAM_ID // Token program ID + ) + ); + await sendAndConfirmTransaction( provider.connection, - UserKP, - UserKP.publicKey, - null, - tokenDecimal, - web3.Keypair.generate(), - null, - TOKEN_PROGRAM_ID + mintTransaction, + [UserKP, mintKeypair], + undefined ); - const userToken = await getOrCreateAssociatedTokenAccount( + const userToken = await createAssociatedTokenAccountIdempotent( provider.connection, UserKP, TOKEN, UserKP.publicKey, - false, - "confirmed", - { - commitment: "confirmed", - }, - TOKEN_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID + {}, + TOKEN_PROGRAM_ID ); - // userBTC = userTokenX.address; - const receipentToken = await getOrCreateAssociatedTokenAccount( + await mintTo( provider.connection, UserKP, TOKEN, - ReceipentKP.publicKey, - false, - "confirmed", - { - commitment: "confirmed", - }, - TOKEN_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID + userToken, + mintAuthority, + mintAmount, + [], + undefined, + TOKEN_PROGRAM_ID ); - ReceipentToken = receipentToken.address; - await mintTo( + + RecipientToken = await createAssociatedTokenAccountIdempotent( provider.connection, UserKP, TOKEN, - userToken.address, - UserKP.publicKey, - 100 * 10 ** tokenDecimal, - [], - { - commitment: "confirmed", - }, + RecipientKP.publicKey, + {}, TOKEN_PROGRAM_ID ); }); - it("Full flow", async () => { console.log("Create vesting plan"); const program = createLockerProgram(new anchor.Wallet(UserKP)); - let currentBlockTime = await getCurrentBlockTime(program.provider.connection); + let currentBlockTime = await getCurrentBlockTime( + program.provider.connection + ); const cliffTime = new BN(currentBlockTime).add(new BN(5)); let escrow = await createVestingPlan({ vestingStartTime: new BN(0), @@ -106,13 +123,14 @@ describe("Full flow", () => { cliffUnlockAmount: new BN(100_000), amountPerPeriod: new BN(50_000), numberOfPeriod: new BN(2), - recipient: ReceipentKP.publicKey, + recipient: RecipientKP.publicKey, updateRecipientMode: 0, }); - while (true) { - const currentBlockTime = await getCurrentBlockTime(program.provider.connection); + const currentBlockTime = await getCurrentBlockTime( + program.provider.connection + ); if (currentBlockTime > cliffTime.toNumber()) { break; } else { @@ -123,11 +141,11 @@ describe("Full flow", () => { console.log("Claim token"); await claimToken({ - recipient: ReceipentKP, - recipientToken: ReceipentToken, + recipient: RecipientKP, + recipientToken: RecipientToken, escrow, maxAmount: new BN(1_000_000), isAssertion: true, - }) + }); }); }); diff --git a/tests/locker_utils/index.ts b/tests/locker_utils/index.ts index 4823027..8bf943c 100644 --- a/tests/locker_utils/index.ts +++ b/tests/locker_utils/index.ts @@ -1,234 +1,270 @@ -import { AnchorProvider, Program, Wallet, web3, BN } from "@coral-xyz/anchor"; -import { Locker, IDL as LockerIDL } from "../../target/types/locker"; +import { AnchorProvider, BN, Program, Wallet, web3 } from "@coral-xyz/anchor"; +import { IDL as LockerIDL, Locker } from "../../target/types/locker"; import { - ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, createAssociatedTokenAccountInstruction + ASSOCIATED_TOKEN_PROGRAM_ID, + createAssociatedTokenAccountInstruction, + getAssociatedTokenAddressSync, + TOKEN_PROGRAM_ID, } from "@solana/spl-token"; import { expect } from "chai"; export const LOCKER_PROGRAM_ID = new web3.PublicKey( - "2r5VekMNiWPzi1pWwvJczrdPaZnJG59u91unSrTunwJg" + "2r5VekMNiWPzi1pWwvJczrdPaZnJG59u91unSrTunwJg" ); - -export function createLockerProgram( - wallet: Wallet, -): Program { - const provider = new AnchorProvider(AnchorProvider.env().connection, wallet, { - maxRetries: 3, - }); - const program = new Program(LockerIDL, LOCKER_PROGRAM_ID, provider); - return program; +export function createLockerProgram(wallet: Wallet): Program { + const provider = new AnchorProvider(AnchorProvider.env().connection, wallet, { + maxRetries: 3, + }); + const program = new Program(LockerIDL, LOCKER_PROGRAM_ID, provider); + return program; } -export function deriveEscrow( - base: web3.PublicKey, - programId: web3.PublicKey -) { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from("escrow"), - base.toBuffer(), - ], - programId - ); +export function deriveEscrow(base: web3.PublicKey, programId: web3.PublicKey) { + return web3.PublicKey.findProgramAddressSync( + [Buffer.from("escrow"), base.toBuffer()], + programId + ); } - export function deriveEscrowMetadata( - escrow: web3.PublicKey, - programId: web3.PublicKey + escrow: web3.PublicKey, + programId: web3.PublicKey ) { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from("escrow_metadata"), - escrow.toBuffer(), - ], - programId - ); + return web3.PublicKey.findProgramAddressSync( + [Buffer.from("escrow_metadata"), escrow.toBuffer()], + programId + ); } - export interface CreateVestingPlanParams { - ownerKeypair: web3.Keypair, - tokenMint: web3.PublicKey, - isAssertion: boolean, - vestingStartTime: BN, - cliffTime: BN, - frequency: BN, - cliffUnlockAmount: BN, - amountPerPeriod: BN, - numberOfPeriod: BN, - recipient: web3.PublicKey, - updateRecipientMode: number, + ownerKeypair: web3.Keypair; + tokenMint: web3.PublicKey; + isAssertion: boolean; + vestingStartTime: BN; + cliffTime: BN; + frequency: BN; + cliffUnlockAmount: BN; + amountPerPeriod: BN; + numberOfPeriod: BN; + recipient: web3.PublicKey; + updateRecipientMode: number; } export async function createVestingPlan(params: CreateVestingPlanParams) { - let { isAssertion, tokenMint, ownerKeypair, cliffTime, frequency, cliffUnlockAmount, amountPerPeriod, numberOfPeriod, recipient, updateRecipientMode, vestingStartTime } = params; - const program = createLockerProgram(new Wallet(ownerKeypair)); + let { + isAssertion, + tokenMint, + ownerKeypair, + vestingStartTime, + cliffTime, + frequency, + cliffUnlockAmount, + amountPerPeriod, + numberOfPeriod, + recipient, + updateRecipientMode, + } = params; + const program = createLockerProgram(new Wallet(ownerKeypair)); - const baseKP = web3.Keypair.generate(); + const baseKP = web3.Keypair.generate(); - let [escrow] = deriveEscrow(baseKP.publicKey, program.programId); + let [escrow] = deriveEscrow(baseKP.publicKey, program.programId); - const senderToken = getAssociatedTokenAddressSync( - tokenMint, - ownerKeypair.publicKey, - false, - TOKEN_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID - ); + const senderToken = getAssociatedTokenAddressSync( + tokenMint, + ownerKeypair.publicKey, + false, + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); - const escrowToken = getAssociatedTokenAddressSync( - tokenMint, + const escrowToken = getAssociatedTokenAddressSync( + tokenMint, + escrow, + true, + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + await program.methods + .createVestingEscrow({ + cliffTime, + frequency, + cliffUnlockAmount, + amountPerPeriod, + numberOfPeriod, + updateRecipientMode, + vestingStartTime, + }) + .accounts({ + base: baseKP.publicKey, + senderToken, + escrowToken, + recipient, + sender: ownerKeypair.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + systemProgram: web3.SystemProgram.programId, + escrow, + }) + .preInstructions([ + createAssociatedTokenAccountInstruction( + ownerKeypair.publicKey, + escrowToken, escrow, - true, + tokenMint, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID + ), + ]) + .signers([baseKP]) + .rpc(); + + if (isAssertion) { + const escrowState = await program.account.vestingEscrow.fetch(escrow); + expect(escrowState.cliffTime.toString()).eq(cliffTime.toString()); + expect(escrowState.frequency.toString()).eq(frequency.toString()); + expect(escrowState.cliffUnlockAmount.toString()).eq( + cliffUnlockAmount.toString() ); - await program.methods.createVestingEscrow({ - cliffTime, - frequency, - cliffUnlockAmount, - amountPerPeriod, - numberOfPeriod, - updateRecipientMode, - vestingStartTime, - }).accounts({ - base: baseKP.publicKey, - senderToken, - escrowToken, - recipient, - sender: ownerKeypair.publicKey, - tokenProgram: TOKEN_PROGRAM_ID, - systemProgram: web3.SystemProgram.programId, - escrow, - }).preInstructions( - [ - createAssociatedTokenAccountInstruction( - ownerKeypair.publicKey, - escrowToken, - escrow, - tokenMint, - TOKEN_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID - ) - ] - ).signers([baseKP]).rpc(); - - if (isAssertion) { - const escrowState = await program.account.vestingEscrow.fetch(escrow); - expect(escrowState.cliffTime.toString()).eq(cliffTime.toString()); - expect(escrowState.frequency.toString()).eq(frequency.toString()); - expect(escrowState.cliffUnlockAmount.toString()).eq(cliffUnlockAmount.toString()); - expect(escrowState.amountPerPeriod.toString()).eq(amountPerPeriod.toString()); - expect(escrowState.numberOfPeriod.toString()).eq(numberOfPeriod.toString()); - expect(escrowState.recipient.toString()).eq(recipient.toString()); - expect(escrowState.tokenMint.toString()).eq(tokenMint.toString()); - expect(escrowState.creator.toString()).eq(ownerKeypair.publicKey.toString()); - expect(escrowState.base.toString()).eq(baseKP.publicKey.toString()); - expect(escrowState.updateRecipientMode).eq(updateRecipientMode); - } + expect(escrowState.amountPerPeriod.toString()).eq( + amountPerPeriod.toString() + ); + expect(escrowState.numberOfPeriod.toString()).eq(numberOfPeriod.toString()); + expect(escrowState.recipient.toString()).eq(recipient.toString()); + expect(escrowState.tokenMint.toString()).eq(tokenMint.toString()); + expect(escrowState.creator.toString()).eq( + ownerKeypair.publicKey.toString() + ); + expect(escrowState.base.toString()).eq(baseKP.publicKey.toString()); + expect(escrowState.updateRecipientMode).eq(updateRecipientMode); + } - return escrow; + return escrow; } - export interface ClaimTokenParams { - isAssertion: boolean, - escrow: web3.PublicKey, - recipient: web3.Keypair, - maxAmount: BN, - recipientToken: web3.PublicKey, + isAssertion: boolean; + escrow: web3.PublicKey; + recipient: web3.Keypair; + maxAmount: BN; + recipientToken: web3.PublicKey; } + export async function claimToken(params: ClaimTokenParams) { - let { isAssertion, escrow, recipient, maxAmount, recipientToken } = params; - const program = createLockerProgram(new Wallet(recipient)); - const escrowState = await program.account.vestingEscrow.fetch(escrow); + let { isAssertion, escrow, recipient, maxAmount, recipientToken } = params; + const program = createLockerProgram(new Wallet(recipient)); + const escrowState = await program.account.vestingEscrow.fetch(escrow); - const escrowToken = getAssociatedTokenAddressSync( - escrowState.tokenMint, - escrow, - true, - TOKEN_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID - ); + const escrowToken = getAssociatedTokenAddressSync( + escrowState.tokenMint, + escrow, + true, + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); - await program.methods.claim(maxAmount).accounts({ - tokenProgram: TOKEN_PROGRAM_ID, - escrow, - escrowToken, - recipient: recipient.publicKey, - recipientToken, - }).rpc(); + await program.methods + .claim(maxAmount) + .accounts({ + tokenProgram: TOKEN_PROGRAM_ID, + escrow, + escrowToken, + recipient: recipient.publicKey, + recipientToken, + }) + .rpc(); } - export interface CreateEscrowMetadataParams { - isAssertion: boolean, - creator: web3.Keypair, - escrow: web3.PublicKey, - name: string, - description: string, - creatorEmail: string, - recipientEmail: string, + isAssertion: boolean; + creator: web3.Keypair; + escrow: web3.PublicKey; + name: string; + description: string; + creatorEmail: string; + recipientEmail: string; } + export async function createEscrowMetadata(params: CreateEscrowMetadataParams) { - let { isAssertion, escrow, name, description, creatorEmail, recipientEmail, creator } = params; - const program = createLockerProgram(new Wallet(creator)); - const [escrowMetadata] = deriveEscrowMetadata(escrow, program.programId); - await program.methods.createVestingEscrowMetadata({ - name, - description, - creatorEmail, - recipientEmail, - }).accounts({ - escrow, - systemProgram: web3.SystemProgram.programId, - payer: creator.publicKey, - creator: creator.publicKey, - escrowMetadata - }).rpc(); - - if (isAssertion) { - const escrowMetadataState = await program.account.vestingEscrowMetadata.fetch(escrowMetadata); - expect(escrowMetadataState.escrow.toString()).eq(escrow.toString()); - expect(escrowMetadataState.name.toString()).eq(name.toString()); - expect(escrowMetadataState.description.toString()).eq(description.toString()); - expect(escrowMetadataState.creatorEmail.toString()).eq(creatorEmail.toString()); - expect(escrowMetadataState.recipientEmail.toString()).eq(recipientEmail.toString()); - } -} + let { + isAssertion, + escrow, + name, + description, + creatorEmail, + recipientEmail, + creator, + } = params; + const program = createLockerProgram(new Wallet(creator)); + const [escrowMetadata] = deriveEscrowMetadata(escrow, program.programId); + await program.methods + .createVestingEscrowMetadata({ + name, + description, + creatorEmail, + recipientEmail, + }) + .accounts({ + escrow, + systemProgram: web3.SystemProgram.programId, + payer: creator.publicKey, + creator: creator.publicKey, + escrowMetadata, + }) + .rpc(); + if (isAssertion) { + const escrowMetadataState = + await program.account.vestingEscrowMetadata.fetch(escrowMetadata); + expect(escrowMetadataState.escrow.toString()).eq(escrow.toString()); + expect(escrowMetadataState.name.toString()).eq(name.toString()); + expect(escrowMetadataState.description.toString()).eq( + description.toString() + ); + expect(escrowMetadataState.creatorEmail.toString()).eq( + creatorEmail.toString() + ); + expect(escrowMetadataState.recipientEmail.toString()).eq( + recipientEmail.toString() + ); + } +} export interface UpdateRecipientParams { - isAssertion: boolean, - signer: web3.Keypair, - escrow: web3.PublicKey, - newRecipient: web3.PublicKey, - newRecipientEmail: null | string + isAssertion: boolean; + signer: web3.Keypair; + escrow: web3.PublicKey; + newRecipient: web3.PublicKey; + newRecipientEmail: null | string; } + export async function updateRecipient(params: UpdateRecipientParams) { - let { isAssertion, escrow, signer, newRecipient, newRecipientEmail } = params; - const program = createLockerProgram(new Wallet(signer)); - let escrowMetadata = null; - if (newRecipientEmail != null) { - [escrowMetadata] = deriveEscrowMetadata(escrow, program.programId); - } - await program.methods.updateVestingEscrowRecipient(newRecipient, newRecipientEmail).accounts({ - escrow, - escrowMetadata, - signer: signer.publicKey, - systemProgram: web3.SystemProgram.programId, - }).rpc(); - - if (isAssertion) { - const escrowState = await program.account.vestingEscrow.fetch(escrow); - expect(escrowState.recipient.toString()).eq(newRecipient.toString()); - if (newRecipientEmail != null) { - [escrowMetadata] = deriveEscrowMetadata(escrow, program.programId); - const escrowMetadataState = await program.account.vestingEscrowMetadata.fetch(escrowMetadata); - expect(escrowMetadataState.recipientEmail.toString()).eq(newRecipientEmail.toString()); - } + let { isAssertion, escrow, signer, newRecipient, newRecipientEmail } = params; + const program = createLockerProgram(new Wallet(signer)); + let escrowMetadata = null; + if (newRecipientEmail != null) { + [escrowMetadata] = deriveEscrowMetadata(escrow, program.programId); + } + await program.methods + .updateVestingEscrowRecipient(newRecipient, newRecipientEmail) + .accounts({ + escrow, + escrowMetadata, + signer: signer.publicKey, + systemProgram: web3.SystemProgram.programId, + }) + .rpc(); + if (isAssertion) { + const escrowState = await program.account.vestingEscrow.fetch(escrow); + expect(escrowState.recipient.toString()).eq(newRecipient.toString()); + if (newRecipientEmail != null) { + [escrowMetadata] = deriveEscrowMetadata(escrow, program.programId); + const escrowMetadataState = + await program.account.vestingEscrowMetadata.fetch(escrowMetadata); + expect(escrowMetadataState.recipientEmail.toString()).eq( + newRecipientEmail.toString() + ); } -} \ No newline at end of file + } +} diff --git a/tests/update_recipient.ts b/tests/update_recipient.ts index 104dde3..91b649e 100644 --- a/tests/update_recipient.ts +++ b/tests/update_recipient.ts @@ -1,273 +1,324 @@ import * as anchor from "@coral-xyz/anchor"; import { web3 } from "@coral-xyz/anchor"; import { - ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID, - createMint, - getOrCreateAssociatedTokenAccount, - mintTo, + createAssociatedTokenAccountIdempotent, + createInitializeMint2Instruction, + mintTo, + TOKEN_PROGRAM_ID, } from "@solana/spl-token"; import { BN } from "bn.js"; import { - createAndFundWallet, - getCurrentBlockTime, - invokeAndAssertError, - sleep, + createAndFundWallet, + getCurrentBlockTime, + invokeAndAssertError, } from "./common"; -import { claimToken, createEscrowMetadata, createLockerProgram, createVestingPlan, updateRecipient } from "./locker_utils"; - +import { + createEscrowMetadata, + createLockerProgram, + createVestingPlan, + updateRecipient, +} from "./locker_utils"; +import { + sendAndConfirmTransaction, + SystemProgram, + Transaction, +} from "@solana/web3.js"; const provider = anchor.AnchorProvider.env(); describe("Update recipient", () => { - const tokenDecimal = 8; - let TOKEN: web3.PublicKey; - let UserKP: web3.Keypair; - let ReceipentKP: web3.Keypair; + const tokenDecimal = 8; + let mintAuthority: web3.Keypair; + let mintKeypair: web3.Keypair; + let TOKEN: web3.PublicKey; - before(async () => { - { - const result = await createAndFundWallet(provider.connection); - UserKP = result.keypair; - } - { - const result = await createAndFundWallet(provider.connection); - ReceipentKP = result.keypair; - } + let UserKP: web3.Keypair; + let RecipientKP: web3.Keypair; - TOKEN = await createMint( - provider.connection, - UserKP, - UserKP.publicKey, - null, - tokenDecimal, - web3.Keypair.generate(), - null, - TOKEN_PROGRAM_ID - ); + let mintAmount: bigint; - const userToken = await getOrCreateAssociatedTokenAccount( - provider.connection, - UserKP, - TOKEN, - UserKP.publicKey, - false, - "confirmed", - { - commitment: "confirmed", - }, - TOKEN_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID - ); - // userBTC = userTokenX.address; - await mintTo( - provider.connection, - UserKP, - TOKEN, - userToken.address, - UserKP.publicKey, - 100 * 10 ** tokenDecimal, - [], - { - commitment: "confirmed", - }, - TOKEN_PROGRAM_ID - ); - }); - it("No one is able to update", async () => { - console.log("Create vesting plan"); - const program = createLockerProgram(new anchor.Wallet(UserKP)); - let currentBlockTime = await getCurrentBlockTime(program.provider.connection); - const cliffTime = new BN(currentBlockTime).add(new BN(5)); - let escrow = await createVestingPlan({ - ownerKeypair: UserKP, - vestingStartTime: new BN(0), - tokenMint: TOKEN, - isAssertion: true, - cliffTime, - frequency: new BN(1), - cliffUnlockAmount: new BN(100_000), - amountPerPeriod: new BN(50_000), - numberOfPeriod: new BN(2), - recipient: ReceipentKP.publicKey, - updateRecipientMode: 0, - }); - console.log("Update recipient"); - const newRecipient = web3.Keypair.generate(); - invokeAndAssertError(async () => { - await updateRecipient({ - escrow, - newRecipient: newRecipient.publicKey, - isAssertion: true, - signer: UserKP, - newRecipientEmail: null, - }); - }, "Not permit to do this action", true); + before(async () => { + { + const result = await createAndFundWallet(provider.connection); + UserKP = result.keypair; + } + { + const result = await createAndFundWallet(provider.connection); + RecipientKP = result.keypair; + } - invokeAndAssertError(async () => { - await updateRecipient({ - escrow, - newRecipient: newRecipient.publicKey, - isAssertion: true, - signer: ReceipentKP, - newRecipientEmail: null, - }); - }, "Not permit to do this action", true); - }); + mintAuthority = new web3.Keypair(); + mintKeypair = new web3.Keypair(); + TOKEN = mintKeypair.publicKey; - it("Creator is able to update recipient", async () => { - console.log("Create vesting plan"); - const program = createLockerProgram(new anchor.Wallet(UserKP)); - let currentBlockTime = await getCurrentBlockTime(program.provider.connection); - const cliffTime = new BN(currentBlockTime).add(new BN(5)); - let escrow = await createVestingPlan({ - ownerKeypair: UserKP, - vestingStartTime: new BN(0), - tokenMint: TOKEN, - isAssertion: true, - cliffTime, - frequency: new BN(1), - cliffUnlockAmount: new BN(100_000), - amountPerPeriod: new BN(50_000), - numberOfPeriod: new BN(2), - recipient: ReceipentKP.publicKey, - updateRecipientMode: 1, - }); - console.log("Update recipient"); - const newRecipient = web3.Keypair.generate(); - invokeAndAssertError(async () => { - await updateRecipient({ - escrow, - newRecipient: newRecipient.publicKey, - isAssertion: true, - signer: ReceipentKP, - newRecipientEmail: null, - }); - }, "Not permit to do this action", true); + mintAmount = BigInt(1_000_000 * Math.pow(10, tokenDecimal)); // Mint 1,000,000 tokens - await updateRecipient({ - escrow, - newRecipient: newRecipient.publicKey, - isAssertion: true, - signer: UserKP, - newRecipientEmail: null, - }); - }); + // Step 2 - Create a New Token + const mintLamports = + await provider.connection.getMinimumBalanceForRentExemption(82); + const mintTransaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: UserKP.publicKey, + newAccountPubkey: TOKEN, + space: 82, + lamports: mintLamports, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMint2Instruction( + TOKEN, // Mint account + tokenDecimal, // Decimals + mintAuthority.publicKey, // Mint authority + null, // Freeze authority + TOKEN_PROGRAM_ID // Token program ID + ) + ); + await sendAndConfirmTransaction( + provider.connection, + mintTransaction, + [UserKP, mintKeypair], + undefined + ); + const userToken = await createAssociatedTokenAccountIdempotent( + provider.connection, + UserKP, + TOKEN, + UserKP.publicKey, + {}, + TOKEN_PROGRAM_ID + ); - it("Recipient is able to update recipient", async () => { - console.log("Create vesting plan"); - const program = createLockerProgram(new anchor.Wallet(UserKP)); - let currentBlockTime = await getCurrentBlockTime(program.provider.connection); - const cliffTime = new BN(currentBlockTime).add(new BN(5)); - let escrow = await createVestingPlan({ - ownerKeypair: UserKP, - vestingStartTime: new BN(0), - tokenMint: TOKEN, - isAssertion: true, - cliffTime, - frequency: new BN(1), - cliffUnlockAmount: new BN(100_000), - amountPerPeriod: new BN(50_000), - numberOfPeriod: new BN(2), - recipient: ReceipentKP.publicKey, - updateRecipientMode: 2, - }); - console.log("Update recipient"); - const newRecipient = web3.Keypair.generate(); - invokeAndAssertError(async () => { - await updateRecipient({ - escrow, - newRecipient: newRecipient.publicKey, - isAssertion: true, - signer: UserKP, - newRecipientEmail: null, - }); - }, "Not permit to do this action", true); + await mintTo( + provider.connection, + UserKP, + TOKEN, + userToken, + mintAuthority, + mintAmount, + [], + undefined, + TOKEN_PROGRAM_ID + ); + }); + it("No one is able to update", async () => { + console.log("Create vesting plan"); + const program = createLockerProgram(new anchor.Wallet(UserKP)); + let currentBlockTime = await getCurrentBlockTime( + program.provider.connection + ); + const cliffTime = new BN(currentBlockTime).add(new BN(5)); + let escrow = await createVestingPlan({ + ownerKeypair: UserKP, + vestingStartTime: new BN(0), + tokenMint: TOKEN, + isAssertion: true, + cliffTime, + frequency: new BN(1), + cliffUnlockAmount: new BN(100_000), + amountPerPeriod: new BN(50_000), + numberOfPeriod: new BN(2), + recipient: RecipientKP.publicKey, + updateRecipientMode: 0, + }); + console.log("Update recipient"); + const newRecipient = web3.Keypair.generate(); + invokeAndAssertError( + async () => { await updateRecipient({ - escrow, - newRecipient: newRecipient.publicKey, - isAssertion: true, - signer: ReceipentKP, - newRecipientEmail: null, + escrow, + newRecipient: newRecipient.publicKey, + isAssertion: true, + signer: UserKP, + newRecipientEmail: null, }); - }); + }, + "Not permit to do this action", + true + ); - it("Creator and Recipient is able to update recipient", async () => { - console.log("Create vesting plan"); - const program = createLockerProgram(new anchor.Wallet(UserKP)); - let currentBlockTime = await getCurrentBlockTime(program.provider.connection); - const cliffTime = new BN(currentBlockTime).add(new BN(5)); - let escrow = await createVestingPlan({ - ownerKeypair: UserKP, - vestingStartTime: new BN(0), - tokenMint: TOKEN, - isAssertion: true, - cliffTime, - frequency: new BN(1), - cliffUnlockAmount: new BN(100_000), - amountPerPeriod: new BN(50_000), - numberOfPeriod: new BN(2), - recipient: ReceipentKP.publicKey, - updateRecipientMode: 3, + invokeAndAssertError( + async () => { + await updateRecipient({ + escrow, + newRecipient: newRecipient.publicKey, + isAssertion: true, + signer: RecipientKP, + newRecipientEmail: null, }); - console.log("Update recipient"); + }, + "Not permit to do this action", + true + ); + }); + + it("Creator is able to update recipient", async () => { + console.log("Create vesting plan"); + const program = createLockerProgram(new anchor.Wallet(UserKP)); + let currentBlockTime = await getCurrentBlockTime( + program.provider.connection + ); + const cliffTime = new BN(currentBlockTime).add(new BN(5)); + let escrow = await createVestingPlan({ + ownerKeypair: UserKP, + vestingStartTime: new BN(0), + tokenMint: TOKEN, + isAssertion: true, + cliffTime, + frequency: new BN(1), + cliffUnlockAmount: new BN(100_000), + amountPerPeriod: new BN(50_000), + numberOfPeriod: new BN(2), + recipient: RecipientKP.publicKey, + updateRecipientMode: 1, + }); + console.log("Update recipient"); + const newRecipient = web3.Keypair.generate(); + invokeAndAssertError( + async () => { await updateRecipient({ - escrow, - newRecipient: ReceipentKP.publicKey, - isAssertion: true, - signer: UserKP, - newRecipientEmail: null, + escrow, + newRecipient: newRecipient.publicKey, + isAssertion: true, + signer: RecipientKP, + newRecipientEmail: null, }); + }, + "Not permit to do this action", + true + ); + + await updateRecipient({ + escrow, + newRecipient: newRecipient.publicKey, + isAssertion: true, + signer: UserKP, + newRecipientEmail: null, + }); + }); + it("Recipient is able to update recipient", async () => { + console.log("Create vesting plan"); + const program = createLockerProgram(new anchor.Wallet(UserKP)); + let currentBlockTime = await getCurrentBlockTime( + program.provider.connection + ); + const cliffTime = new BN(currentBlockTime).add(new BN(5)); + let escrow = await createVestingPlan({ + ownerKeypair: UserKP, + vestingStartTime: new BN(0), + tokenMint: TOKEN, + isAssertion: true, + cliffTime, + frequency: new BN(1), + cliffUnlockAmount: new BN(100_000), + amountPerPeriod: new BN(50_000), + numberOfPeriod: new BN(2), + recipient: RecipientKP.publicKey, + updateRecipientMode: 2, + }); + console.log("Update recipient"); + const newRecipient = web3.Keypair.generate(); + invokeAndAssertError( + async () => { await updateRecipient({ - escrow, - newRecipient: ReceipentKP.publicKey, - isAssertion: true, - signer: ReceipentKP, - newRecipientEmail: null, + escrow, + newRecipient: newRecipient.publicKey, + isAssertion: true, + signer: UserKP, + newRecipientEmail: null, }); + }, + "Not permit to do this action", + true + ); + + await updateRecipient({ + escrow, + newRecipient: newRecipient.publicKey, + isAssertion: true, + signer: RecipientKP, + newRecipientEmail: null, }); + }); + it("Creator and Recipient is able to update recipient", async () => { + console.log("Create vesting plan"); + const program = createLockerProgram(new anchor.Wallet(UserKP)); + let currentBlockTime = await getCurrentBlockTime( + program.provider.connection + ); + const cliffTime = new BN(currentBlockTime).add(new BN(5)); + let escrow = await createVestingPlan({ + ownerKeypair: UserKP, + vestingStartTime: new BN(0), + tokenMint: TOKEN, + isAssertion: true, + cliffTime, + frequency: new BN(1), + cliffUnlockAmount: new BN(100_000), + amountPerPeriod: new BN(50_000), + numberOfPeriod: new BN(2), + recipient: RecipientKP.publicKey, + updateRecipientMode: 3, + }); + console.log("Update recipient"); + await updateRecipient({ + escrow, + newRecipient: RecipientKP.publicKey, + isAssertion: true, + signer: UserKP, + newRecipientEmail: null, + }); - it("Update both recipient and recipient email", async () => { - console.log("Create vesting plan"); - const program = createLockerProgram(new anchor.Wallet(UserKP)); - let currentBlockTime = await getCurrentBlockTime(program.provider.connection); - const cliffTime = new BN(currentBlockTime).add(new BN(5)); - let escrow = await createVestingPlan({ - ownerKeypair: UserKP, - tokenMint: TOKEN, - vestingStartTime: new BN(0), - isAssertion: true, - cliffTime, - frequency: new BN(1), - cliffUnlockAmount: new BN(100_000), - amountPerPeriod: new BN(50_000), - numberOfPeriod: new BN(2), - recipient: ReceipentKP.publicKey, - updateRecipientMode: 3, - }); + await updateRecipient({ + escrow, + newRecipient: RecipientKP.publicKey, + isAssertion: true, + signer: RecipientKP, + newRecipientEmail: null, + }); + }); - console.log("Create escrow metadata"); - await createEscrowMetadata({ - escrow, - name: "Jupiter lock", - description: "This is jupiter lock", - creatorEmail: "andrew@raccoons.dev", - recipientEmail: "max@raccoons.dev", - creator: UserKP, - isAssertion: true - }); + it("Update both recipient and recipient email", async () => { + console.log("Create vesting plan"); + const program = createLockerProgram(new anchor.Wallet(UserKP)); + let currentBlockTime = await getCurrentBlockTime( + program.provider.connection + ); + const cliffTime = new BN(currentBlockTime).add(new BN(5)); + let escrow = await createVestingPlan({ + ownerKeypair: UserKP, + vestingStartTime: new BN(0), + tokenMint: TOKEN, + isAssertion: true, + cliffTime, + frequency: new BN(1), + cliffUnlockAmount: new BN(100_000), + amountPerPeriod: new BN(50_000), + numberOfPeriod: new BN(2), + recipient: RecipientKP.publicKey, + updateRecipientMode: 3, + }); - console.log("Update recipient"); - await updateRecipient({ - escrow, - newRecipient: ReceipentKP.publicKey, - isAssertion: true, - signer: UserKP, - newRecipientEmail: "maximillian@raccoons.dev", - }); + console.log("Create escrow metadata"); + await createEscrowMetadata({ + escrow, + name: "Jupiter lock", + description: "This is jupiter lock", + creatorEmail: "andrew@raccoons.dev", + recipientEmail: "max@raccoons.dev", + creator: UserKP, + isAssertion: true, + }); + + console.log("Update recipient"); + await updateRecipient({ + escrow, + newRecipient: RecipientKP.publicKey, + isAssertion: true, + signer: UserKP, + newRecipientEmail: "maximillian@raccoons.dev", }); + }); }); diff --git a/tests/v2/locker.ts b/tests/v2/locker.ts new file mode 100644 index 0000000..ce4f5b3 --- /dev/null +++ b/tests/v2/locker.ts @@ -0,0 +1,156 @@ +import * as anchor from "@coral-xyz/anchor"; +import { web3 } from "@coral-xyz/anchor"; +import { + createAssociatedTokenAccountIdempotent, + createInitializeMintInstruction, + ExtensionType, + getMintLen, + TOKEN_2022_PROGRAM_ID, +} from "@solana/spl-token"; +import { BN } from "bn.js"; +import { createAndFundWallet, getCurrentBlockTime, sleep } from "../common"; +import { + claimToken, + createLockerProgram, + createVestingPlan, + initializeTokenBadge, +} from "./locker_utils"; +import { createMintTransaction } from "./locker_utils/mint"; +import { assert } from "chai"; +import { + sendAndConfirmTransaction, + SystemProgram, + Transaction, +} from "@solana/web3.js"; + +const provider = anchor.AnchorProvider.env(); + +describe("[V2] Test full flow", () => { + let TOKEN: web3.PublicKey; + let UserKP: web3.Keypair; + let RecipientKP: web3.Keypair; + let RecipientToken: web3.PublicKey; + + let extensions: ExtensionType[]; + + before(async () => { + { + const result = await createAndFundWallet(provider.connection); + UserKP = result.keypair; + } + { + const result = await createAndFundWallet(provider.connection); + RecipientKP = result.keypair; + } + + // Define the extensions to be used by the mint + extensions = [ExtensionType.TransferFeeConfig]; + + TOKEN = await createMintTransaction(provider, UserKP, extensions); + + RecipientToken = await createAssociatedTokenAccountIdempotent( + provider.connection, + UserKP, + TOKEN, + RecipientKP.publicKey, + {}, + TOKEN_2022_PROGRAM_ID + ); + }); + + it("Full flow With token 2022", async () => { + console.log("Create vesting plan"); + const program = createLockerProgram(new anchor.Wallet(UserKP)); + let currentBlockTime = await getCurrentBlockTime( + program.provider.connection + ); + + const startTime = new BN(currentBlockTime).add(new BN(5)); + let escrow = await createVestingPlan({ + ownerKeypair: UserKP, + tokenMint: TOKEN, + isAssertion: true, + startTime, + frequency: new BN(1), + initialUnlockAmount: new BN(100_000), + amountPerPeriod: new BN(50_000), + numberOfPeriod: new BN(2), + recipient: RecipientKP.publicKey, + updateRecipientMode: 0, + tokenProgram: TOKEN_2022_PROGRAM_ID, + }); + + while (true) { + const currentBlockTime = await getCurrentBlockTime( + program.provider.connection + ); + if (currentBlockTime > startTime.toNumber()) { + break; + } else { + await sleep(1000); + console.log("Wait until startTime"); + } + } + + console.log("Claim token"); + try { + await claimToken({ + recipient: RecipientKP, + recipientToken: RecipientToken, + tokenMint: TOKEN, + escrow, + maxAmount: new BN(1_000_000), + isAssertion: true, + tokenProgram: TOKEN_2022_PROGRAM_ID, + }); + } catch (error) { + console.log(error); + } + }); + + it("[FAIL] init TokenBadge with wrong Authority", async () => { + extensions = [ExtensionType.PermanentDelegate]; + let mintKeypair = new web3.Keypair(); + let TOKEN = mintKeypair.publicKey; + + let mintLen = getMintLen([]); + const mintLamports = + await provider.connection.getMinimumBalanceForRentExemption(mintLen); + const mintTransaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: UserKP.publicKey, + newAccountPubkey: TOKEN, + space: mintLen, + lamports: mintLamports, + programId: TOKEN_2022_PROGRAM_ID, + }), + createInitializeMintInstruction( + TOKEN, + 8, + UserKP.publicKey, + null, + TOKEN_2022_PROGRAM_ID + ) + ); + + await sendAndConfirmTransaction( + provider.connection, + mintTransaction, + [UserKP, mintKeypair], + undefined + ); + + try { + await initializeTokenBadge({ + isAssertion: true, + ownerKeypair: UserKP, + mint: TOKEN, + }); + } catch (error) { + const errMsg = error.error?.errorMessage + ? error.error?.errorMessage + : anchor.AnchorError.parse(error.logs).error.errorMessage; + assert.equal(errMsg, "Unauthorized"); + } + }); +}); diff --git a/tests/v2/locker_utils/index.ts b/tests/v2/locker_utils/index.ts new file mode 100644 index 0000000..9a3b9e5 --- /dev/null +++ b/tests/v2/locker_utils/index.ts @@ -0,0 +1,335 @@ +import { AnchorProvider, BN, Program, Wallet, web3 } from "@coral-xyz/anchor"; +import { IDL as LockerIDL, Locker } from "../../../target/types/locker"; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + createAssociatedTokenAccountInstruction, + getAssociatedTokenAddressSync, +} from "@solana/spl-token"; +import { expect } from "chai"; + +export const LOCKER_PROGRAM_ID = new web3.PublicKey( + "2r5VekMNiWPzi1pWwvJczrdPaZnJG59u91unSrTunwJg" +); + +const MEMO_PROGRAM = new web3.PublicKey( + "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" +); + +export function createLockerProgram(wallet: Wallet): Program { + const provider = new AnchorProvider(AnchorProvider.env().connection, wallet, { + maxRetries: 3, + }); + return new Program(LockerIDL, LOCKER_PROGRAM_ID, provider); +} + +export function deriveEscrow(base: web3.PublicKey, programId: web3.PublicKey) { + return web3.PublicKey.findProgramAddressSync( + [Buffer.from("escrow"), base.toBuffer()], + programId + ); +} + +export function deriveEscrowMetadata( + escrow: web3.PublicKey, + programId: web3.PublicKey +) { + return web3.PublicKey.findProgramAddressSync( + [Buffer.from("escrow_metadata"), escrow.toBuffer()], + programId + ); +} + +export function deriveTokenBadge( + programId: web3.PublicKey, + mint: web3.PublicKey +) { + return web3.PublicKey.findProgramAddressSync( + [Buffer.from("token_badge"), mint.toBuffer()], + programId + ); +} + +export interface CreateVestingPlanParams { + ownerKeypair: web3.Keypair; + tokenMint: web3.PublicKey; + isAssertion: boolean; + vestingStartTime: BN; + cliffTime: BN; + frequency: BN; + cliffUnlockAmount: BN; + amountPerPeriod: BN; + numberOfPeriod: BN; + recipient: web3.PublicKey; + updateRecipientMode: number; + tokenProgram: web3.PublicKey; +} + +export async function createVestingPlan(params: CreateVestingPlanParams) { + let { + isAssertion, + tokenMint, + ownerKeypair, + vestingStartTime, + cliffTime, + frequency, + cliffUnlockAmount, + amountPerPeriod, + numberOfPeriod, + recipient, + updateRecipientMode, + tokenProgram, + } = params; + const program = createLockerProgram(new Wallet(ownerKeypair)); + + const baseKP = web3.Keypair.generate(); + + let [escrow] = deriveEscrow(baseKP.publicKey, program.programId); + + let [tokenBadge] = deriveTokenBadge(program.programId, tokenMint); + + const senderToken = getAssociatedTokenAddressSync( + tokenMint, + ownerKeypair.publicKey, + false, + tokenProgram, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + const escrowToken = getAssociatedTokenAddressSync( + tokenMint, + escrow, + true, + tokenProgram, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + await program.methods + .createVestingEscrowV2( + { + vestingStartTime, + cliffTime, + frequency, + cliffUnlockAmount, + amountPerPeriod, + numberOfPeriod, + updateRecipientMode, + }, + null + ) + .accounts({ + base: baseKP.publicKey, + senderToken, + escrowToken, + recipient, + tokenBadge, + mint: tokenMint, + sender: ownerKeypair.publicKey, + tokenProgram, + systemProgram: web3.SystemProgram.programId, + escrow, + }) + .preInstructions([ + createAssociatedTokenAccountInstruction( + ownerKeypair.publicKey, + escrowToken, + escrow, + tokenMint, + tokenProgram, + ASSOCIATED_TOKEN_PROGRAM_ID + ), + ]) + .signers([baseKP]) + .rpc(); + + if (isAssertion) { + const escrowState = await program.account.vestingEscrow.fetch(escrow); + expect(escrowState.cliffTime.toString()).eq(cliffTime.toString()); + expect(escrowState.frequency.toString()).eq(frequency.toString()); + expect(escrowState.cliffUnlockAmount.toString()).eq( + cliffUnlockAmount.toString() + ); + expect(escrowState.amountPerPeriod.toString()).eq( + amountPerPeriod.toString() + ); + expect(escrowState.numberOfPeriod.toString()).eq(numberOfPeriod.toString()); + expect(escrowState.recipient.toString()).eq(recipient.toString()); + expect(escrowState.tokenMint.toString()).eq(tokenMint.toString()); + expect(escrowState.creator.toString()).eq( + ownerKeypair.publicKey.toString() + ); + expect(escrowState.base.toString()).eq(baseKP.publicKey.toString()); + expect(escrowState.updateRecipientMode).eq(updateRecipientMode); + } + + return escrow; +} + +export interface ClaimTokenParams { + isAssertion: boolean; + tokenMint: web3.PublicKey; + escrow: web3.PublicKey; + recipient: web3.Keypair; + maxAmount: BN; + recipientToken: web3.PublicKey; + tokenProgram: web3.PublicKey; +} + +export async function claimToken(params: ClaimTokenParams) { + let { + isAssertion, + escrow, + tokenMint, + recipient, + maxAmount, + recipientToken, + tokenProgram, + } = params; + const program = createLockerProgram(new Wallet(recipient)); + const escrowState = await program.account.vestingEscrow.fetch(escrow); + + const escrowToken = getAssociatedTokenAddressSync( + escrowState.tokenMint, + escrow, + true, + tokenProgram, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + const tx = await program.methods + .claimV2(maxAmount, null) + .accounts({ + tokenProgram, + mint: tokenMint, + memoProgram: MEMO_PROGRAM, + escrow, + escrowToken, + recipient: recipient.publicKey, + recipientToken, + }) + .rpc(); + + console.log(" claim token signature", tx); +} + +export interface CreateEscrowMetadataParams { + isAssertion: boolean; + creator: web3.Keypair; + escrow: web3.PublicKey; + name: string; + description: string; + creatorEmail: string; + recipientEmail: string; +} + +export async function createEscrowMetadata(params: CreateEscrowMetadataParams) { + let { + isAssertion, + escrow, + name, + description, + creatorEmail, + recipientEmail, + creator, + } = params; + const program = createLockerProgram(new Wallet(creator)); + const [escrowMetadata] = deriveEscrowMetadata(escrow, program.programId); + await program.methods + .createVestingEscrowMetadata({ + name, + description, + creatorEmail, + recipientEmail, + }) + .accounts({ + escrow, + systemProgram: web3.SystemProgram.programId, + payer: creator.publicKey, + creator: creator.publicKey, + escrowMetadata, + }) + .rpc(); + + if (isAssertion) { + const escrowMetadataState = + await program.account.vestingEscrowMetadata.fetch(escrowMetadata); + expect(escrowMetadataState.escrow.toString()).eq(escrow.toString()); + expect(escrowMetadataState.name.toString()).eq(name.toString()); + expect(escrowMetadataState.description.toString()).eq( + description.toString() + ); + expect(escrowMetadataState.creatorEmail.toString()).eq( + creatorEmail.toString() + ); + expect(escrowMetadataState.recipientEmail.toString()).eq( + recipientEmail.toString() + ); + } +} + +export interface UpdateRecipientParams { + isAssertion: boolean; + signer: web3.Keypair; + escrow: web3.PublicKey; + newRecipient: web3.PublicKey; + newRecipientEmail: null | string; +} + +export async function updateRecipient(params: UpdateRecipientParams) { + let { isAssertion, escrow, signer, newRecipient, newRecipientEmail } = params; + const program = createLockerProgram(new Wallet(signer)); + let escrowMetadata = null; + if (newRecipientEmail != null) { + [escrowMetadata] = deriveEscrowMetadata(escrow, program.programId); + } + await program.methods + .updateVestingEscrowRecipient(newRecipient, newRecipientEmail) + .accounts({ + escrow, + escrowMetadata, + signer: signer.publicKey, + systemProgram: web3.SystemProgram.programId, + }) + .rpc(); + + if (isAssertion) { + const escrowState = await program.account.vestingEscrow.fetch(escrow); + expect(escrowState.recipient.toString()).eq(newRecipient.toString()); + if (newRecipientEmail != null) { + [escrowMetadata] = deriveEscrowMetadata(escrow, program.programId); + const escrowMetadataState = + await program.account.vestingEscrowMetadata.fetch(escrowMetadata); + expect(escrowMetadataState.recipientEmail.toString()).eq( + newRecipientEmail.toString() + ); + } + } +} + +export interface InitializeTokenBadgeParams { + isAssertion: boolean; + ownerKeypair: web3.Keypair; + mint: web3.PublicKey; +} + +export async function initializeTokenBadge(params: InitializeTokenBadgeParams) { + let { isAssertion, ownerKeypair, mint } = params; + const program = createLockerProgram(new Wallet(ownerKeypair)); + let [tokenBadge] = deriveTokenBadge(program.programId, mint); + + await program.methods + .initializeTokenBadge() + .accounts({ + tokenBadgeAuthority: ownerKeypair.publicKey, + tokenMint: mint, + tokenBadge, + payer: ownerKeypair.publicKey, + systemProgram: web3.SystemProgram.programId, + }) + .rpc(); + + if (isAssertion) { + const tokenBadgeState = await program.account.tokenBadge.fetch(tokenBadge); + expect(tokenBadgeState.tokenMint.toString()).eq(mint.toString()); + } + + return tokenBadge; +} diff --git a/tests/v2/locker_utils/mint.ts b/tests/v2/locker_utils/mint.ts new file mode 100644 index 0000000..1cb30ce --- /dev/null +++ b/tests/v2/locker_utils/mint.ts @@ -0,0 +1,199 @@ +import { AnchorProvider, web3 } from "@coral-xyz/anchor"; +import { + sendAndConfirmTransaction, + SystemProgram, + Transaction, +} from "@solana/web3.js"; +import { + AccountState, + createAssociatedTokenAccountIdempotent, + createInitializeDefaultAccountStateInstruction, + createInitializeInterestBearingMintInstruction, + createInitializeMintCloseAuthorityInstruction, + createInitializeMintInstruction, + createInitializePermanentDelegateInstruction, + createInitializeTransferFeeConfigInstruction, + ExtensionType, + getMintLen, + mintTo, + TOKEN_2022_PROGRAM_ID, +} from "@solana/spl-token"; +import { initializeTokenBadge } from "./index"; + +export const ADMIN = web3.Keypair.fromSecretKey( + Uint8Array.from([ + 158, 34, 15, 43, 215, 144, 99, 15, 49, 40, 202, 189, 244, 179, 70, 200, 156, + 140, 193, 247, 230, 82, 1, 103, 248, 52, 233, 244, 82, 52, 98, 196, 70, 116, + 166, 240, 58, 250, 204, 125, 228, 56, 121, 32, 22, 54, 214, 133, 148, 40, + 149, 8, 60, 74, 23, 212, 222, 54, 125, 78, 2, 203, 157, 229, + ]) +); + +let feeBasisPoints: number; +let maxFee: bigint; + +const tokenDecimal = 8; + +export async function createMintTransaction( + provider: AnchorProvider, + UserKP: web3.Keypair, + extensions: ExtensionType[], + shouldMint: boolean = true, + shouldHaveFreezeAuthority: boolean = false, + createTokenBadge: boolean = false +) { + // Set the decimals, fee basis points, and maximum fee + feeBasisPoints = 100; // 1% + maxFee = BigInt(9 * Math.pow(10, tokenDecimal)); // 9 tokens + + // Define the amount to be minted and the amount to be transferred, accounting for decimals + let mintAmount = BigInt(1_000_000 * Math.pow(10, tokenDecimal)); // Mint 1,000,000 tokens + + let mintLen = getMintLen(extensions); + const mintLamports = + await provider.connection.getMinimumBalanceForRentExemption(mintLen); + + let mintAuthority = new web3.Keypair(); + let mintKeypair = new web3.Keypair(); + let TOKEN = mintKeypair.publicKey; + + // Generate keys for transfer fee config authority and withdrawal authority + let transferFeeConfigAuthority = new web3.Keypair(); + let withdrawWithheldAuthority = new web3.Keypair(); + + const mintTransaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: UserKP.publicKey, + newAccountPubkey: TOKEN, + space: mintLen, + lamports: mintLamports, + programId: TOKEN_2022_PROGRAM_ID, + }) + ); + + mintTransaction.add( + ...createExtensionMintIx( + extensions, + UserKP, + TOKEN, + transferFeeConfigAuthority, + withdrawWithheldAuthority + ) + ); + + mintTransaction.add( + createInitializeMintInstruction( + TOKEN, + tokenDecimal, + mintAuthority.publicKey, + shouldHaveFreezeAuthority ? mintAuthority.publicKey : null, + TOKEN_2022_PROGRAM_ID + ) + ); + + await sendAndConfirmTransaction( + provider.connection, + mintTransaction, + [UserKP, mintKeypair], + undefined + ); + + if (createTokenBadge) { + await initializeTokenBadge({ + isAssertion: true, + ownerKeypair: ADMIN, + mint: TOKEN, + }); + } + + const userToken = await createAssociatedTokenAccountIdempotent( + provider.connection, + UserKP, + TOKEN, + UserKP.publicKey, + {}, + TOKEN_2022_PROGRAM_ID + ); + + if (shouldMint) { + await mintTo( + provider.connection, + UserKP, + TOKEN, + userToken, + mintAuthority, + mintAmount, + [], + undefined, + TOKEN_2022_PROGRAM_ID + ); + } + + return TOKEN; +} + +function createExtensionMintIx( + extensions: ExtensionType[], + UserKP: web3.Keypair, + TOKEN: web3.PublicKey, + transferFeeConfigAuthority: web3.Keypair, + withdrawWithheldAuthority: web3.Keypair +): web3.TransactionInstruction[] { + const ix = []; + + if (extensions.includes(ExtensionType.TransferFeeConfig)) { + ix.push( + createInitializeTransferFeeConfigInstruction( + TOKEN, + transferFeeConfigAuthority.publicKey, + withdrawWithheldAuthority.publicKey, + feeBasisPoints, + maxFee, + TOKEN_2022_PROGRAM_ID + ) + ); + } + + if (extensions.includes(ExtensionType.InterestBearingConfig)) { + ix.push( + createInitializeInterestBearingMintInstruction( + TOKEN, + UserKP.publicKey, + 10, + TOKEN_2022_PROGRAM_ID + ) + ); + } + + if (extensions.includes(ExtensionType.DefaultAccountState)) { + ix.push( + createInitializeDefaultAccountStateInstruction( + TOKEN, // Mint Account address + AccountState.Frozen, // Default AccountState + TOKEN_2022_PROGRAM_ID // Token Extension Program ID + ) + ); + } + + if (extensions.includes(ExtensionType.PermanentDelegate)) { + ix.push( + createInitializePermanentDelegateInstruction( + TOKEN, // Mint Account address + ADMIN.publicKey, // Designated Permanent Delegate + TOKEN_2022_PROGRAM_ID // Token Extension Program ID + ) + ); + } + + if (extensions.includes(ExtensionType.MintCloseAuthority)) { + ix.push( + createInitializeMintCloseAuthorityInstruction( + TOKEN, // Mint Account address + ADMIN.publicKey, // Designated Close Authority + TOKEN_2022_PROGRAM_ID // Token Extension Program ID + ) + ); + } + + return ix; +} diff --git a/tests/v2/unsupported_mint.ts b/tests/v2/unsupported_mint.ts new file mode 100644 index 0000000..01a5526 --- /dev/null +++ b/tests/v2/unsupported_mint.ts @@ -0,0 +1,153 @@ +import * as anchor from "@coral-xyz/anchor"; +import { web3 } from "@coral-xyz/anchor"; +import { ExtensionType, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; +import { BN } from "bn.js"; +import { createAndFundWallet, getCurrentBlockTime } from "../common"; +import { createLockerProgram, createVestingPlan } from "./locker_utils"; +import { assert } from "chai"; +import { createMintTransaction } from "./locker_utils/mint"; + +const provider = anchor.AnchorProvider.env(); + +describe("[V2] Test supported/unsupported Token Mint", () => { + let TOKEN: web3.PublicKey; + let UserKP: web3.Keypair; + let RecipientKP: web3.Keypair; + + let extensions: ExtensionType[]; + + before(async () => { + { + const result = await createAndFundWallet(provider.connection); + UserKP = result.keypair; + } + { + const result = await createAndFundWallet(provider.connection); + RecipientKP = result.keypair; + } + }); + + it("[FAIL] unsupported InterestBearingConfig", async () => { + // Define the extensions to be used by the mint + extensions = [ExtensionType.InterestBearingConfig]; + + TOKEN = await createMintTransaction(provider, UserKP, extensions); + + await check(TOKEN); + }); + + it("[FAIL] unsupported DefaultAccountState", async () => { + // Define the extensions to be used by the mint + extensions = [ExtensionType.DefaultAccountState]; + + TOKEN = await createMintTransaction( + provider, + UserKP, + extensions, + false, + true + ); + + await check(TOKEN); + }); + + it("[FAIL] unsupported FreezeAuthority without TokenBadge", async () => { + TOKEN = await createMintTransaction( + provider, + UserKP, + extensions, + false, + true + ); + + await check(TOKEN); + }); + + it("supported FreezeAuthority with TokenBadge", async () => { + TOKEN = await createMintTransaction( + provider, + UserKP, + extensions, + false, + true, + true + ); + + await check(TOKEN); + }); + + it("[FAIL] unsupported PermanentDelegate without TokenBadge", async () => { + extensions = [ExtensionType.PermanentDelegate]; + + TOKEN = await createMintTransaction(provider, UserKP, extensions); + + await check(TOKEN); + }); + + it("supported PermanentDelegate with TokenBadge", async () => { + extensions = [ExtensionType.PermanentDelegate]; + + TOKEN = await createMintTransaction( + provider, + UserKP, + extensions, + true, + false, + true + ); + + await check(TOKEN); + }); + + it("[FAIL] unsupported MintCloseAuthority without TokenBadge", async () => { + extensions = [ExtensionType.MintCloseAuthority]; + + TOKEN = await createMintTransaction(provider, UserKP, extensions); + + await check(TOKEN); + }); + + it("supported MintCloseAuthority with TokenBadge", async () => { + extensions = [ExtensionType.MintCloseAuthority]; + + TOKEN = await createMintTransaction( + provider, + UserKP, + extensions, + true, + false, + true + ); + + await check(TOKEN); + }); + + async function check(TOKEN: web3.PublicKey) { + const program = createLockerProgram(new anchor.Wallet(UserKP)); + let currentBlockTime = await getCurrentBlockTime( + program.provider.connection + ); + + const startTime = new BN(currentBlockTime).add(new BN(5)); + try { + await createVestingPlan({ + ownerKeypair: UserKP, + tokenMint: TOKEN, + isAssertion: true, + startTime, + frequency: new BN(1), + initialUnlockAmount: new BN(100_000), + amountPerPeriod: new BN(50_000), + numberOfPeriod: new BN(2), + recipient: RecipientKP.publicKey, + updateRecipientMode: 0, + tokenProgram: TOKEN_2022_PROGRAM_ID, + }); + } catch (error) { + const errMsg = error.error?.errorMessage + ? error.error?.errorMessage + : anchor.AnchorError.parse(error.logs).error.errorMessage; + assert.equal(errMsg, "Unsupported mint"); + } + } +}); diff --git a/tests/v2/update_recipient.ts b/tests/v2/update_recipient.ts new file mode 100644 index 0000000..3660843 --- /dev/null +++ b/tests/v2/update_recipient.ts @@ -0,0 +1,365 @@ +import * as anchor from "@coral-xyz/anchor"; +import { web3 } from "@coral-xyz/anchor"; +import { + createAssociatedTokenAccountIdempotent, + createInitializeMintInstruction, + createInitializeTransferFeeConfigInstruction, + ExtensionType, + getMintLen, + mintTo, + TOKEN_2022_PROGRAM_ID, +} from "@solana/spl-token"; +import { BN } from "bn.js"; +import { + createAndFundWallet, + getCurrentBlockTime, + invokeAndAssertError, +} from "../common"; +import { + sendAndConfirmTransaction, + SystemProgram, + Transaction, +} from "@solana/web3.js"; +import { + createEscrowMetadata, + createLockerProgram, + createVestingPlan, + updateRecipient, +} from "./locker_utils"; + +const provider = anchor.AnchorProvider.env(); + +describe("[V2] Update recipient", () => { + const tokenDecimal = 8; + let UserKP: web3.Keypair; + let RecipientKP: web3.Keypair; + let mintAuthority: web3.Keypair; + let mintKeypair: web3.Keypair; + let TOKEN: web3.PublicKey; + + let transferFeeConfigAuthority: web3.Keypair; + let withdrawWithheldAuthority: web3.Keypair; + + let extensions: ExtensionType[]; + let mintLen: number; + + let feeBasisPoints: number; + let maxFee: bigint; + + // Define the amount to be minted and the amount to be transferred, accounting for decimals + let mintAmount: bigint; + let transferAmount: bigint; + + before(async () => { + { + const result = await createAndFundWallet(provider.connection); + UserKP = result.keypair; + } + { + const result = await createAndFundWallet(provider.connection); + RecipientKP = result.keypair; + } + + mintAuthority = new web3.Keypair(); + mintKeypair = new web3.Keypair(); + TOKEN = mintKeypair.publicKey; + + // Generate keys for transfer fee config authority and withdrawal authority + transferFeeConfigAuthority = new web3.Keypair(); + withdrawWithheldAuthority = new web3.Keypair(); + + // Define the extensions to be used by the mint + extensions = [ExtensionType.TransferFeeConfig]; + + // Calculate the length of the mint + mintLen = getMintLen(extensions); + + // Set the decimals, fee basis points, and maximum fee + feeBasisPoints = 100; // 1% + maxFee = BigInt(9 * Math.pow(10, tokenDecimal)); // 9 tokens + + // Define the amount to be minted and the amount to be transferred, accounting for decimals + mintAmount = BigInt(1_000_000 * Math.pow(10, tokenDecimal)); // Mint 1,000,000 tokens + transferAmount = BigInt(1_000 * Math.pow(10, tokenDecimal)); // Transfer 1,000 tokens + + // Step 2 - Create a New Token + const mintLamports = + await provider.connection.getMinimumBalanceForRentExemption(mintLen); + const mintTransaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: UserKP.publicKey, + newAccountPubkey: TOKEN, + space: mintLen, + lamports: mintLamports, + programId: TOKEN_2022_PROGRAM_ID, + }), + createInitializeTransferFeeConfigInstruction( + TOKEN, + transferFeeConfigAuthority.publicKey, + withdrawWithheldAuthority.publicKey, + feeBasisPoints, + maxFee, + TOKEN_2022_PROGRAM_ID + ), + createInitializeMintInstruction( + TOKEN, + tokenDecimal, + mintAuthority.publicKey, + null, + TOKEN_2022_PROGRAM_ID + ) + ); + await sendAndConfirmTransaction( + provider.connection, + mintTransaction, + [UserKP, mintKeypair], + undefined + ); + + const userToken = await createAssociatedTokenAccountIdempotent( + provider.connection, + UserKP, + TOKEN, + UserKP.publicKey, + {}, + TOKEN_2022_PROGRAM_ID + ); + const mintSig = await mintTo( + provider.connection, + UserKP, + TOKEN, + userToken, + mintAuthority, + mintAmount, + [], + undefined, + TOKEN_2022_PROGRAM_ID + ); + }); + + it("No one is able to update", async () => { + console.log("Create vesting plan"); + const program = createLockerProgram(new anchor.Wallet(UserKP)); + let currentBlockTime = await getCurrentBlockTime( + program.provider.connection + ); + + const startTime = new BN(currentBlockTime).add(new BN(5)); + let escrow = await createVestingPlan({ + ownerKeypair: UserKP, + tokenMint: TOKEN, + isAssertion: true, + startTime, + frequency: new BN(1), + initialUnlockAmount: new BN(100_000), + amountPerPeriod: new BN(50_000), + numberOfPeriod: new BN(2), + recipient: RecipientKP.publicKey, + updateRecipientMode: 0, + tokenProgram: TOKEN_2022_PROGRAM_ID, + }); + console.log("Update recipient"); + const newRecipient = web3.Keypair.generate(); + invokeAndAssertError( + async () => { + await updateRecipient({ + escrow, + newRecipient: newRecipient.publicKey, + isAssertion: true, + signer: UserKP, + newRecipientEmail: null, + }); + }, + "Not permit to do this action", + true + ); + + invokeAndAssertError( + async () => { + await updateRecipient({ + escrow, + newRecipient: newRecipient.publicKey, + isAssertion: true, + signer: RecipientKP, + newRecipientEmail: null, + }); + }, + "Not permit to do this action", + true + ); + }); + + it("Creator is able to update recipient", async () => { + console.log("Create vesting plan"); + const program = createLockerProgram(new anchor.Wallet(UserKP)); + let currentBlockTime = await getCurrentBlockTime( + program.provider.connection + ); + + const startTime = new BN(currentBlockTime).add(new BN(5)); + let escrow = await createVestingPlan({ + ownerKeypair: UserKP, + tokenMint: TOKEN, + isAssertion: true, + startTime, + frequency: new BN(1), + initialUnlockAmount: new BN(100_000), + amountPerPeriod: new BN(50_000), + numberOfPeriod: new BN(2), + recipient: RecipientKP.publicKey, + updateRecipientMode: 1, + tokenProgram: TOKEN_2022_PROGRAM_ID, + }); + console.log("Update recipient"); + const newRecipient = web3.Keypair.generate(); + invokeAndAssertError( + async () => { + await updateRecipient({ + escrow, + newRecipient: newRecipient.publicKey, + isAssertion: true, + signer: RecipientKP, + newRecipientEmail: null, + }); + }, + "Not permit to do this action", + true + ); + + await updateRecipient({ + escrow, + newRecipient: newRecipient.publicKey, + isAssertion: true, + signer: UserKP, + newRecipientEmail: null, + }); + }); + + it("Recipient is able to update recipient", async () => { + console.log("Create vesting plan"); + const program = createLockerProgram(new anchor.Wallet(UserKP)); + let currentBlockTime = await getCurrentBlockTime( + program.provider.connection + ); + + const startTime = new BN(currentBlockTime).add(new BN(5)); + let escrow = await createVestingPlan({ + ownerKeypair: UserKP, + tokenMint: TOKEN, + isAssertion: true, + startTime, + frequency: new BN(1), + initialUnlockAmount: new BN(100_000), + amountPerPeriod: new BN(50_000), + numberOfPeriod: new BN(2), + recipient: RecipientKP.publicKey, + updateRecipientMode: 2, + tokenProgram: TOKEN_2022_PROGRAM_ID, + }); + console.log("Update recipient"); + const newRecipient = web3.Keypair.generate(); + invokeAndAssertError( + async () => { + await updateRecipient({ + escrow, + newRecipient: newRecipient.publicKey, + isAssertion: true, + signer: UserKP, + newRecipientEmail: null, + }); + }, + "Not permit to do this action", + true + ); + + await updateRecipient({ + escrow, + newRecipient: newRecipient.publicKey, + isAssertion: true, + signer: RecipientKP, + newRecipientEmail: null, + }); + }); + + it("Creator and Recipient is able to update recipient", async () => { + console.log("Create vesting plan"); + const program = createLockerProgram(new anchor.Wallet(UserKP)); + let currentBlockTime = await getCurrentBlockTime( + program.provider.connection + ); + + const startTime = new BN(currentBlockTime).add(new BN(5)); + let escrow = await createVestingPlan({ + ownerKeypair: UserKP, + tokenMint: TOKEN, + isAssertion: true, + startTime, + frequency: new BN(1), + initialUnlockAmount: new BN(100_000), + amountPerPeriod: new BN(50_000), + numberOfPeriod: new BN(2), + recipient: RecipientKP.publicKey, + updateRecipientMode: 3, + tokenProgram: TOKEN_2022_PROGRAM_ID, + }); + console.log("Update recipient"); + await updateRecipient({ + escrow, + newRecipient: RecipientKP.publicKey, + isAssertion: true, + signer: UserKP, + newRecipientEmail: null, + }); + + await updateRecipient({ + escrow, + newRecipient: RecipientKP.publicKey, + isAssertion: true, + signer: RecipientKP, + newRecipientEmail: null, + }); + }); + + it("Update both recipient and recipient email", async () => { + console.log("Create vesting plan"); + const program = createLockerProgram(new anchor.Wallet(UserKP)); + let currentBlockTime = await getCurrentBlockTime( + program.provider.connection + ); + + const startTime = new BN(currentBlockTime).add(new BN(5)); + let escrow = await createVestingPlan({ + ownerKeypair: UserKP, + tokenMint: TOKEN, + isAssertion: true, + startTime, + frequency: new BN(1), + initialUnlockAmount: new BN(100_000), + amountPerPeriod: new BN(50_000), + numberOfPeriod: new BN(2), + recipient: RecipientKP.publicKey, + updateRecipientMode: 3, + tokenProgram: TOKEN_2022_PROGRAM_ID, + }); + + console.log("Create escrow metadata"); + await createEscrowMetadata({ + escrow, + name: "Jupiter lock", + description: "This is jupiter lock", + creatorEmail: "andrew@raccoons.dev", + recipientEmail: "max@raccoons.dev", + creator: UserKP, + isAssertion: true, + }); + + console.log("Update recipient"); + await updateRecipient({ + escrow, + newRecipient: RecipientKP.publicKey, + isAssertion: true, + signer: UserKP, + newRecipientEmail: "maximillian@raccoons.dev", + }); + }); +}); diff --git a/tests/v2/utils/remaining-accounts.ts b/tests/v2/utils/remaining-accounts.ts new file mode 100644 index 0000000..19713e6 --- /dev/null +++ b/tests/v2/utils/remaining-accounts.ts @@ -0,0 +1,51 @@ +import { AccountMeta } from "@solana/web3.js"; + +export enum RemainingAccountsType { + TransferHookInput = "transferHookInput", + TransferHookClaim = "transferHookClaim", +} + +type RemainingAccountsAnchorType = + | { transferHookInput: {} } + | { transferHookClaim: {} }; + +export type RemainingAccountsSliceData = { + accountsType: RemainingAccountsAnchorType; + length: number; +}; + +export type RemainingAccountsInfoData = { + slices: RemainingAccountsSliceData[]; +}; + +// Option on Rust +// null is treated as None in Rust. undefined doesn't work. +export type OptionRemainingAccountsInfoData = RemainingAccountsInfoData | null; + +export class RemainingAccountsBuilder { + private remainingAccounts: AccountMeta[] = []; + private slices: RemainingAccountsSliceData[] = []; + + constructor() {} + + addSlice( + accountsType: RemainingAccountsType, + accounts?: AccountMeta[] + ): this { + if (!accounts || accounts.length === 0) return this; + + this.slices.push({ + accountsType: { [accountsType]: {} } as RemainingAccountsAnchorType, + length: accounts.length, + }); + this.remainingAccounts.push(...accounts); + + return this; + } + + build(): [OptionRemainingAccountsInfoData, AccountMeta[] | undefined] { + return this.slices.length === 0 + ? [null, undefined] + : [{ slices: this.slices }, this.remainingAccounts]; + } +} diff --git a/yarn.lock b/yarn.lock index b451168..d5512b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,14 +9,14 @@ dependencies: regenerator-runtime "^0.14.0" -"@coral-xyz/anchor@^0.28.0": - version "0.28.0" - resolved "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.28.0.tgz" - integrity sha512-kQ02Hv2ZqxtWP30WN1d4xxT4QqlOXYDxmEd3k/bbneqhV3X5QMO4LAtoUFs7otxyivOgoqam5Il5qx81FuI4vw== +"@coral-xyz/anchor@^0.29.0": + version "0.29.0" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.29.0.tgz#bd0be95bedfb30a381c3e676e5926124c310ff12" + integrity sha512-eny6QNG0WOwqV0zQ7cs/b1tIuzZGmP7U7EcH+ogt4Gdbl8HDmIYVMh/9aTmYZPaFWjtUaI8qSn73uYEXWfATdA== dependencies: - "@coral-xyz/borsh" "^0.28.0" + "@coral-xyz/borsh" "^0.29.0" + "@noble/hashes" "^1.3.1" "@solana/web3.js" "^1.68.0" - base64-js "^1.5.1" bn.js "^5.1.2" bs58 "^4.0.1" buffer-layout "^1.2.2" @@ -24,16 +24,15 @@ cross-fetch "^3.1.5" crypto-hash "^1.3.0" eventemitter3 "^4.0.7" - js-sha256 "^0.9.0" pako "^2.0.3" snake-case "^3.0.4" superstruct "^0.15.4" toml "^3.0.0" -"@coral-xyz/borsh@^0.28.0": - version "0.28.0" - resolved "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.28.0.tgz" - integrity sha512-/u1VTzw7XooK7rqeD7JLUSwOyRSesPUk0U37BV9zK0axJc1q0nRbKFGFLYCQ16OtdOJTTwGfGp11Lx9B45bRCQ== +"@coral-xyz/borsh@^0.29.0": + version "0.29.0" + resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.29.0.tgz#79f7045df2ef66da8006d47f5399c7190363e71f" + integrity sha512-s7VFVa3a0oqpkuRloWVPdCK7hMbAMY270geZOGfCnaqexrP5dTIpbEHL33req6IYPPJ0hYa71cdvJ1h6V55/oQ== dependencies: bn.js "^5.1.2" buffer-layout "^1.2.0" @@ -45,7 +44,7 @@ dependencies: "@noble/hashes" "1.4.0" -"@noble/hashes@^1.4.0", "@noble/hashes@1.4.0": +"@noble/hashes@1.4.0", "@noble/hashes@^1.3.1", "@noble/hashes@^1.4.0": version "1.4.0" resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== @@ -152,7 +151,7 @@ dependencies: buffer "^6.0.3" -"@solana/web3.js@^1.32.0", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.88.0", "@solana/web3.js@^1.91.6": +"@solana/web3.js@^1.32.0", "@solana/web3.js@^1.68.0": version "1.95.1" resolved "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.95.1.tgz" integrity sha512-mRX/AjV6QbiOXpWcy5Rz1ZWEH2lVkwO7T0pcv9t97ACpv3/i3tPiqXwk0JIZgSR3wOSTiT26JfygnJH2ulS6dQ== @@ -245,6 +244,14 @@ resolved "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== +JSONStream@^1.3.5: + version "1.3.5" + resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz" + integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + agentkeepalive@^4.5.0: version "4.5.0" resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz" @@ -304,7 +311,7 @@ base-x@^3.0.2: dependencies: safe-buffer "^5.0.1" -base64-js@^1.3.1, base64-js@^1.5.1: +base64-js@^1.3.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -384,7 +391,7 @@ buffer-layout@^1.2.0, buffer-layout@^1.2.2: resolved "https://registry.npmjs.org/buffer-layout/-/buffer-layout-1.2.2.tgz" integrity sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA== -buffer@^6.0.3, buffer@~6.0.3, buffer@6.0.3: +buffer@6.0.3, buffer@^6.0.3, buffer@~6.0.3: version "6.0.3" resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== @@ -524,16 +531,16 @@ delay@^5.0.0: resolved "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz" integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== -diff@^3.1.0: - version "3.5.0" - resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - diff@5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== +diff@^3.1.0: + version "3.5.0" + resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + dot-case@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz" @@ -589,11 +596,6 @@ fast-stable-stringify@^1.0.0: resolved "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz" integrity sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag== -fastestsmallesttextencoderdecoder@^1.0.22: - version "1.0.22" - resolved "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz" - integrity sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw== - file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz" @@ -755,21 +757,16 @@ jayson@^4.1.1: "@types/connect" "^3.4.33" "@types/node" "^12.12.54" "@types/ws" "^7.4.4" + JSONStream "^1.3.5" commander "^2.20.3" delay "^5.0.0" es6-promisify "^5.0.0" eyes "^0.1.8" isomorphic-ws "^4.0.1" json-stringify-safe "^5.0.1" - JSONStream "^1.3.5" uuid "^8.3.2" ws "^7.5.10" -js-sha256@^0.9.0: - version "0.9.0" - resolved "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz" - integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== - js-yaml@4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" @@ -794,14 +791,6 @@ jsonparse@^1.2.0: resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== -JSONStream@^1.3.5: - version "1.3.5" - resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz" - integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - locate-path@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" @@ -836,13 +825,6 @@ make-error@^1.1.1: resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -minimatch@^3.0.4: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - minimatch@4.2.1: version "4.2.1" resolved "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz" @@ -850,6 +832,13 @@ minimatch@4.2.1: dependencies: brace-expansion "^1.1.7" +minimatch@^3.0.4: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + minimist@^1.2.0, minimist@^1.2.6: version "1.2.8" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" @@ -862,7 +851,7 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.6" -"mocha@^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X || ^9.X.X || ^10.X.X", mocha@^9.0.3: +mocha@^9.0.3: version "9.2.2" resolved "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz" integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== @@ -892,16 +881,16 @@ mkdirp@^0.5.1: yargs-parser "20.2.4" yargs-unparser "2.0.0" -ms@^2.0.0, ms@2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@2.1.3, ms@^2.0.0: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + nanoid@3.3.1: version "3.3.1" resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz" @@ -1092,13 +1081,6 @@ superstruct@^2.0.2: resolved "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz" integrity sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A== -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - supports-color@8.1.1: version "8.1.1" resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" @@ -1106,6 +1088,13 @@ supports-color@8.1.1: dependencies: has-flag "^4.0.0" +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + text-encoding-utf-8@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz" @@ -1116,6 +1105,11 @@ text-encoding-utf-8@^1.0.2: resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== +tiny-invariant@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" @@ -1186,7 +1180,7 @@ undici-types@~5.26.4: resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -utf-8-validate@^5.0.2, utf-8-validate@>=5.0.2: +utf-8-validate@^5.0.2: version "5.0.10" resolved "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz" integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== @@ -1237,7 +1231,7 @@ wrappy@1: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -ws@*, ws@^7.5.10: +ws@^7.5.10: version "7.5.10" resolved "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz" integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== @@ -1252,16 +1246,16 @@ y18n@^5.0.5: resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yargs-parser@^20.2.2: - version "20.2.9" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - yargs-parser@20.2.4: version "20.2.4" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz" integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + yargs-unparser@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz"