diff --git a/Anchor.toml b/Anchor.toml index db9814449..788e21220 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -16,5 +16,15 @@ slots_per_epoch = "33" ticks_per_slot = 7 url = "https://api.mainnet-beta.solana.com" -[[test.validator.clone]] +# In v0.29.0, this doesn't work because ProgramData account is cloned with executable = false (anchor bug ?) +# So we need to use test.genesis config. +# [[test.validator.clone]] +# address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" + +[[test.genesis]] address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" +program = "sdk/tests/external_program/mpl_token_metadata.20240214.so" + +[[test.genesis]] +address = "EBZDYx7599krFc4m2govwBdZcicr4GgepqC78m71nsHS" +program = "sdk/tests/external_program/transfer_hook_counter.so" diff --git a/Cargo.lock b/Cargo.lock index 984f03709..84738d23c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,7 +18,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" dependencies = [ "cfg-if", - "cipher 0.3.0", + "cipher", "cpufeatures", "opaque-debug", ] @@ -31,7 +31,7 @@ checksum = "589c637f0e68c877bbd59a4599bbe849cac8e5f3e4b5a3ebae8f528cd218dcdc" dependencies = [ "aead", "aes", - "cipher 0.3.0", + "cipher", "ctr", "polyval", "subtle", @@ -44,11 +44,24 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.12", "once_cell", "version_check", ] +[[package]] +name = "ahash" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7d5a2cecb58716e47d67d5703a249964b14c7be1ec3cad3affc295b2d1c35d" +dependencies = [ + "cfg-if", + "getrandom 0.2.12", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "0.7.20" @@ -60,174 +73,164 @@ dependencies = [ [[package]] name = "anchor-attribute-access-control" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf7d535e1381be3de2c0716c0a1c1e32ad9df1042cddcf7bc18d743569e53319" +checksum = "e5f619f1d04f53621925ba8a2e633ba5a6081f2ae14758cbb67f38fd823e0a3e" dependencies = [ "anchor-syn", - "anyhow", "proc-macro2", "quote", - "regex", - "syn", + "syn 1.0.107", ] [[package]] name = "anchor-attribute-account" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3bcd731f21048a032be27c7791701120e44f3f6371358fc4261a7f716283d29" +checksum = "e7f2a3e1df4685f18d12a943a9f2a7456305401af21a07c9fe076ef9ecd6e400" dependencies = [ "anchor-syn", - "anyhow", - "bs58 0.4.0", + "bs58 0.5.0", "proc-macro2", "quote", - "rustversion", - "syn", + "syn 1.0.107", ] [[package]] name = "anchor-attribute-constant" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1be64a48e395fe00b8217287f226078be2cf32dae42fdf8a885b997945c3d28" +checksum = "9423945cb55627f0b30903288e78baf6f62c6c8ab28fb344b6b25f1ffee3dca7" dependencies = [ "anchor-syn", - "proc-macro2", - "syn", + "quote", + "syn 1.0.107", ] [[package]] name = "anchor-attribute-error" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ea6713d1938c0da03656ff8a693b17dc0396da66d1ba320557f07e86eca0d4" +checksum = "93ed12720033cc3c3bf3cfa293349c2275cd5ab99936e33dd4bf283aaad3e241" dependencies = [ "anchor-syn", - "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] name = "anchor-attribute-event" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401f11efb3644285685f8339829a9786d43ed7490bb1699f33c478d04d5a582" +checksum = "eef4dc0371eba2d8c8b54794b0b0eb786a234a559b77593d6f80825b6d2c77a2" dependencies = [ "anchor-syn", - "anyhow", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] -name = "anchor-attribute-interface" -version = "0.26.0" +name = "anchor-attribute-program" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6700a6f5c888a9c33fe8afc0c64fd8575fa28d05446037306d0f96102ae4480" +checksum = "b18c4f191331e078d4a6a080954d1576241c29c56638783322a18d308ab27e4f" dependencies = [ "anchor-syn", - "anyhow", - "heck", - "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] -name = "anchor-attribute-program" -version = "0.26.0" +name = "anchor-derive-accounts" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ad769993b5266714e8939e47fbdede90e5c030333c7522d99a4d4748cf26712" +checksum = "5de10d6e9620d3bcea56c56151cad83c5992f50d5960b3a9bebc4a50390ddc3c" dependencies = [ "anchor-syn", - "anyhow", - "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] -name = "anchor-attribute-state" -version = "0.26.0" +name = "anchor-derive-serde" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e677fae4a016a554acdd0e3b7f178d3acafaa7e7ffac6b8690cf4e171f1c116" +checksum = "f4e2e5be518ec6053d90a2a7f26843dbee607583c779e6c8395951b9739bdfbe" dependencies = [ "anchor-syn", - "anyhow", + "borsh-derive-internal 0.10.3", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] -name = "anchor-derive-accounts" -version = "0.26.0" +name = "anchor-derive-space" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "340beef6809d1c3fcc7ae219153d981e95a8a277ff31985bd7050e32645dc9a8" +checksum = "1ecc31d19fa54840e74b7a979d44bcea49d70459de846088a1d71e87ba53c419" dependencies = [ - "anchor-syn", - "anyhow", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] name = "anchor-lang" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662ceafe667448ee4199a4be2ee83b6bb76da28566eee5cea05f96ab38255af8" +checksum = "35da4785497388af0553586d55ebdc08054a8b1724720ef2749d313494f2b8ad" dependencies = [ "anchor-attribute-access-control", "anchor-attribute-account", "anchor-attribute-constant", "anchor-attribute-error", "anchor-attribute-event", - "anchor-attribute-interface", "anchor-attribute-program", - "anchor-attribute-state", "anchor-derive-accounts", + "anchor-derive-serde", + "anchor-derive-space", "arrayref", "base64 0.13.1", "bincode", - "borsh", + "borsh 0.10.3", "bytemuck", + "getrandom 0.2.12", "solana-program", "thiserror", ] [[package]] name = "anchor-spl" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f32390ce8356f54c0f0245ea156f8190717e37285b8bf4f406a613dc4b954cde" +checksum = "6c4fd6e43b2ca6220d2ef1641539e678bfc31b6cc393cf892b373b5997b6a39a" dependencies = [ "anchor-lang", + "mpl-token-metadata", "solana-program", "spl-associated-token-account", + "spl-memo", "spl-token", + "spl-token-2022 0.9.0", ] [[package]] name = "anchor-syn" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0418bcb5daac3b8cb1b60d8fdb1d468ca36f5509f31fb51179326fae1028fdcc" +checksum = "d9101b84702fed2ea57bd22992f75065da5648017135b844283a2f6d74f27825" dependencies = [ "anyhow", - "bs58 0.3.1", + "bs58 0.5.0", "heck", "proc-macro2", - "proc-macro2-diagnostics", "quote", "serde", "serde_json", - "sha2 0.9.9", - "syn", + "sha2 0.10.8", + "syn 1.0.107", "thiserror", ] @@ -237,17 +240,134 @@ version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +[[package]] +name = "ark-bn254" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a22f4561524cd949590d78d7d4c5df8f592430d221f7f3c9497bbafd8972120f" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", + "itertools", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "derivative", + "digest 0.10.7", + "itertools", + "num-bigint", + "num-traits", + "paste", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.107", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.107", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.107", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "arrayref" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "arrayvec" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "assert_matches" @@ -284,6 +404,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "bincode" version = "1.3.3" @@ -314,6 +440,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +dependencies = [ + "serde", +] + [[package]] name = "bitmaps" version = "2.1.0" @@ -325,16 +460,16 @@ dependencies = [ [[package]] name = "blake3" -version = "1.3.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef" +checksum = "0231f06152bf547e9c2b5194f247cd97aacf6dcd8b15d8e5ec0663f64580da87" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -349,9 +484,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] @@ -368,21 +503,44 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" dependencies = [ - "borsh-derive", + "borsh-derive 0.9.3", "hashbrown 0.11.2", ] +[[package]] +name = "borsh" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" +dependencies = [ + "borsh-derive 0.10.3", + "hashbrown 0.13.2", +] + [[package]] name = "borsh-derive" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" dependencies = [ - "borsh-derive-internal", - "borsh-schema-derive-internal", + "borsh-derive-internal 0.9.3", + "borsh-schema-derive-internal 0.9.3", "proc-macro-crate 0.1.5", "proc-macro2", - "syn", + "syn 1.0.107", +] + +[[package]] +name = "borsh-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" +dependencies = [ + "borsh-derive-internal 0.10.3", + "borsh-schema-derive-internal 0.10.3", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.107", ] [[package]] @@ -393,7 +551,18 @@ checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.107", ] [[package]] @@ -404,14 +573,19 @@ checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] -name = "bs58" -version = "0.3.1" +name = "borsh-schema-derive-internal" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "476e9cd489f9e121e02ffa6014a8ef220ecb15c05ed23fc34cca13925dc283fb" +checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.107", +] [[package]] name = "bs58" @@ -419,6 +593,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +[[package]] +name = "bs58" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.11.1" @@ -437,22 +620,22 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.12.3" +version = "1.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" +checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.3.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe233b960f12f8007e3db2d136e3cb1c291bfd7396e384ee76025fc1a3932b4" +checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -463,11 +646,12 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cc" -version = "1.0.78" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -478,11 +662,10 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" dependencies = [ - "num-integer", "num-traits", ] @@ -495,16 +678,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "cipher" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" -dependencies = [ - "crypto-common", - "inout", -] - [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -517,9 +690,9 @@ dependencies = [ [[package]] name = "console_log" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501a375961cef1a0d44767200e66e4a559283097e91d0730b1d75dfb2f8a1494" +checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" dependencies = [ "log", "web-sys", @@ -527,9 +700,9 @@ dependencies = [ [[package]] name = "constant_time_eq" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ad85c1f65dc7b37604eb0e89748faf0b9653065f2a8ef69f96a687ec1e9279" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" [[package]] name = "cpufeatures" @@ -615,7 +788,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" dependencies = [ - "cipher 0.3.0", + "cipher", ] [[package]] @@ -638,8 +811,18 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[package]] +name = "darling" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5d6b04b3fd0ba9926f945895de7d806260a2d7431ba82e7edaecb043c4c6b8" +dependencies = [ + "darling_core 0.20.5", + "darling_macro 0.20.5", ] [[package]] @@ -653,7 +836,21 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 1.0.107", +] + +[[package]] +name = "darling_core" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e48a959bcd5c761246f5d090ebc2fbf7b9cd527a492b07a67510c108f1e7e3" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.48", ] [[package]] @@ -662,9 +859,20 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core", + "darling_core 0.13.4", + "quote", + "syn 1.0.107", +] + +[[package]] +name = "darling_macro" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1545d67a2149e1d93b7e5c7752dce5a7426eb5d1357ddcfd89336b94444f77" +dependencies = [ + "darling_core 0.20.5", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -673,6 +881,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.107", +] + [[package]] name = "digest" version = "0.9.0" @@ -684,11 +903,11 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.3", + "block-buffer 0.10.4", "crypto-common", "subtle", ] @@ -725,14 +944,14 @@ dependencies = [ "derivation-path", "ed25519-dalek", "hmac 0.12.1", - "sha2 0.10.6", + "sha2 0.10.8", ] [[package]] name = "either" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "env_logger" @@ -770,9 +989,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "serde", "typenum", @@ -794,9 +1013,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "js-sys", @@ -811,16 +1030,16 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "ahash", + "ahash 0.7.6", ] [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.5", ] [[package]] @@ -872,7 +1091,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -914,15 +1133,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array", -] - [[package]] name = "instant" version = "0.1.12" @@ -958,9 +1168,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" dependencies = [ "wasm-bindgen", ] @@ -982,9 +1192,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.139" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libsecp256k1" @@ -1034,6 +1244,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 = "lock_api" version = "0.4.9" @@ -1046,12 +1268,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" @@ -1061,27 +1280,27 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memmap2" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b182332558b18d807c4ce1ca8ca983b34c3ee32765e47b3f0f69b90355cc1dc" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" dependencies = [ "libc", ] [[package]] name = "memoffset" -version = "0.6.5" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] [[package]] name = "memoffset" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] @@ -1099,74 +1318,49 @@ dependencies = [ ] [[package]] -name = "mpl-token-auth-rules" -version = "1.2.0" +name = "mpl-token-metadata" +version = "3.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69803fbfbc4bb0327de86f49d2639692c7c60276cb87d6cced84bb8189f2000" +checksum = "ba8ee05284d79b367ae8966d558e1a305a781fc80c9df51f37775169117ba64f" dependencies = [ - "borsh", - "mpl-token-metadata-context-derive", - "num-derive", + "borsh 0.9.3", + "num-derive 0.3.3", "num-traits", - "rmp-serde", - "serde", - "shank", "solana-program", - "solana-zk-token-sdk", "thiserror", ] [[package]] -name = "mpl-token-metadata" -version = "1.8.3" +name = "num-bigint" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6678bb3110abc45bb32e81f80ede7420ce9069d0feb872e7423779bf9a20d1f0" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ - "arrayref", - "borsh", - "mpl-token-auth-rules", - "mpl-token-metadata-context-derive", - "mpl-utils", - "num-derive", + "autocfg", + "num-integer", "num-traits", - "shank", - "solana-program", - "spl-associated-token-account", - "spl-token", - "thiserror", ] [[package]] -name = "mpl-token-metadata-context-derive" -version = "0.2.1" +name = "num-derive" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12989bc45715b0ee91944855130131479f9c772e198a910c3eb0ea327d5bffc3" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ + "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "mpl-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc48e64c50dba956acb46eec86d6968ef0401ef37031426da479f1f2b592066" -dependencies = [ - "arrayref", - "borsh", - "solana-program", - "spl-token", + "syn 1.0.107", ] [[package]] name = "num-derive" -version = "0.3.3" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -1181,9 +1375,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] @@ -1200,23 +1394,44 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.5.7" +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.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" dependencies = [ - "num_enum_derive", + "proc-macro-crate 1.2.1", + "proc-macro2", + "quote", + "syn 2.0.48", ] [[package]] name = "num_enum_derive" -version = "0.5.7" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ "proc-macro-crate 1.2.1", "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -1275,7 +1490,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -1324,26 +1539,13 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.49" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] -[[package]] -name = "proc-macro2-diagnostics" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "version_check", - "yansi", -] - [[package]] name = "proptest" version = "1.0.0" @@ -1351,7 +1553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5" dependencies = [ "bit-set", - "bitflags", + "bitflags 1.3.2", "byteorder", "lazy_static", "num-traits", @@ -1373,6 +1575,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.48", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -1387,9 +1600,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.23" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1453,7 +1666,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.12", ] [[package]] @@ -1511,7 +1724,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1540,28 +1753,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "rmp" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" -dependencies = [ - "byteorder", - "num-traits", - "paste", -] - -[[package]] -name = "rmp-serde" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" -dependencies = [ - "byteorder", - "rmp", - "serde", -] - [[package]] name = "rustc-hash" version = "1.1.0" @@ -1579,9 +1770,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "rusty-fork" @@ -1615,38 +1806,38 @@ checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" [[package]] name = "serde" -version = "1.0.152" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.8" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718dc5fff5b36f99093fc49b280cfc96ce6fc824317783bff5a1fed0c7a64819" +checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.152" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.91" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa", "ryu", @@ -1661,7 +1852,17 @@ checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" dependencies = [ "serde", "serde_json", - "serde_with_macros", + "serde_with_macros 1.5.2", +] + +[[package]] +name = "serde_with" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" +dependencies = [ + "serde", + "serde_with_macros 2.3.3", ] [[package]] @@ -1670,10 +1871,22 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ - "darling", + "darling 0.13.4", "proc-macro2", "quote", - "syn", + "syn 1.0.107", +] + +[[package]] +name = "serde_with_macros" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" +dependencies = [ + "darling 0.20.5", + "proc-macro2", + "quote", + "syn 2.0.48", ] [[package]] @@ -1691,13 +1904,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -1718,44 +1931,10 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "keccak", ] -[[package]] -name = "shank" -version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63e565b5e95ad88ab38f312e89444c749360641c509ef2de0093b49f55974a5" -dependencies = [ - "shank_macro", -] - -[[package]] -name = "shank_macro" -version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63927d22a1e8b74bda98cc6e151fcdf178b7abb0dc6c4f81e0bbf5ffe2fc4ec8" -dependencies = [ - "proc-macro2", - "quote", - "shank_macro_impl", - "syn", -] - -[[package]] -name = "shank_macro_impl" -version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce03403df682f80f4dc1efafa87a4d0cb89b03726d0565e6364bdca5b9a441" -dependencies = [ - "anyhow", - "proc-macro2", - "quote", - "serde", - "syn", -] - [[package]] name = "signature" version = "1.6.4" @@ -1780,33 +1959,29 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "solana-frozen-abi" -version = "1.14.12" +version = "1.17.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c39813ee5b249cb8ccb325d3639323eb3616e7bb9a2b1502936d7ea20530097" +checksum = "9c9ac7bbff73e7f0e42f2453c2a514c1f7e1976617e655c5eea4c8d4f74d01d6" dependencies = [ - "ahash", + "ahash 0.8.5", "blake3", - "block-buffer 0.9.0", + "block-buffer 0.10.4", "bs58 0.4.0", "bv", "byteorder", "cc", "either", "generic-array", - "getrandom 0.1.16", - "hashbrown 0.12.3", "im", "lazy_static", "log", "memmap2", - "once_cell", - "rand_core 0.6.4", "rustc_version", "serde", "serde_bytes", "serde_derive", "serde_json", - "sha2 0.10.6", + "sha2 0.10.8", "solana-frozen-abi-macro", "subtle", "thiserror", @@ -1814,21 +1989,21 @@ dependencies = [ [[package]] name = "solana-frozen-abi-macro" -version = "1.14.12" +version = "1.17.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad43ac27c4b8d7a3ce0e2cb8642a7e3b8ea5e3c29ecea38045a8518519adccf" +checksum = "4d6a65acf04814029dc7d8f34bf4d3e264183d1d0f2af528a6d28ce3343a22fd" dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.48", ] [[package]] name = "solana-logger" -version = "1.14.12" +version = "1.17.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13a18f8d7490f712a4340998fca2b0d35afcdef671320a0e51f40b537363d592" +checksum = "d0ea479b76e53b869443fd1a46d3d678dfc4a3cdf1e76e777b7871276d4b71d4" dependencies = [ "env_logger", "lazy_static", @@ -1837,16 +2012,20 @@ dependencies = [ [[package]] name = "solana-program" -version = "1.14.12" +version = "1.17.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dafff676128fe508ab83147b6fb19534fc33f43ec14789da1f1867e9ea06887" +checksum = "4b8038b87341412d9a5dd5fd9a47501a13716bf22bd5f0e091089b2822eaea1e" dependencies = [ - "base64 0.13.1", + "ark-bn254", + "ark-ec", + "ark-ff", + "ark-serialize", + "base64 0.21.7", "bincode", - "bitflags", + "bitflags 2.4.2", "blake3", - "borsh", - "borsh-derive", + "borsh 0.10.3", + "borsh 0.9.3", "bs58 0.4.0", "bv", "bytemuck", @@ -1854,26 +2033,27 @@ dependencies = [ "console_error_panic_hook", "console_log", "curve25519-dalek", - "getrandom 0.2.8", + "getrandom 0.2.12", "itertools", "js-sys", "lazy_static", "libc", "libsecp256k1", + "light-poseidon", "log", - "memoffset 0.6.5", - "num-derive", + "memoffset 0.9.0", + "num-bigint", + "num-derive 0.3.3", "num-traits", "parking_lot", - "rand 0.7.3", - "rand_chacha 0.2.2", + "rand 0.8.5", "rustc_version", "rustversion", "serde", "serde_bytes", "serde_derive", "serde_json", - "sha2 0.10.6", + "sha2 0.10.8", "sha3 0.10.6", "solana-frozen-abi", "solana-frozen-abi-macro", @@ -1886,21 +2066,21 @@ dependencies = [ [[package]] name = "solana-sdk" -version = "1.14.12" +version = "1.17.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c702cc57432bc16eab54ad7b5668c2a3cdc72b0f820175972b4857e26ac4f49" +checksum = "c4fa260b2ddae30b9e31574ea81a2b157dba991e3908b544ab9bc4436c19c891" dependencies = [ "assert_matches", - "base64 0.13.1", + "base64 0.21.7", "bincode", - "bitflags", - "borsh", + "bitflags 2.4.2", + "borsh 0.10.3", "bs58 0.4.0", "bytemuck", "byteorder", "chrono", "derivation-path", - "digest 0.10.6", + "digest 0.10.7", "ed25519-dalek", "ed25519-dalek-bip32", "generic-array", @@ -1911,19 +2091,22 @@ dependencies = [ "libsecp256k1", "log", "memmap2", - "num-derive", + "num-derive 0.3.3", "num-traits", + "num_enum 0.6.1", "pbkdf2 0.11.0", "qstring", + "qualifier_attr", "rand 0.7.3", - "rand_chacha 0.2.2", + "rand 0.8.5", "rustc_version", "rustversion", "serde", "serde_bytes", "serde_derive", "serde_json", - "sha2 0.10.6", + "serde_with 2.3.3", + "sha2 0.10.8", "sha3 0.10.6", "solana-frozen-abi", "solana-frozen-abi-macro", @@ -1937,36 +2120,40 @@ dependencies = [ [[package]] name = "solana-sdk-macro" -version = "1.14.12" +version = "1.17.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89a14a8f1e7708fe19ee3140125e9d8279945ead74cb09e65c94dd5cf0640c3" +checksum = "204ed709eac41f4d67417eaacc8c66b0435b0f2b07ec367c15f97a0624fa09ca" dependencies = [ "bs58 0.4.0", "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.48", ] +[[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.14.12" +version = "1.17.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32395c4561673f7b4aa1f3a5b5a654eaa363041f67d92f5d680de72293ef7d1b" +checksum = "535c3af389866ddaac91a28df6bb351dcf3b73d489ee7f982e043ef10232877a" dependencies = [ "aes-gcm-siv", - "arrayref", - "base64 0.13.1", + "base64 0.21.7", "bincode", "bytemuck", "byteorder", - "cipher 0.4.3", "curve25519-dalek", "getrandom 0.1.16", "itertools", "lazy_static", "merlin", - "num-derive", + "num-derive 0.3.3", "num-traits", "rand 0.7.3", "serde", @@ -1981,62 +2168,263 @@ dependencies = [ [[package]] name = "spl-associated-token-account" -version = "1.1.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc000f0fdf1f12f99d77d398137c1751345b18c88258ce0f99b7872cf6c9bd6" +checksum = "4414117bead33f2a5cf059cefac0685592bdd36f31f3caac49b89bff7f6bbf32" dependencies = [ "assert_matches", - "borsh", - "num-derive", + "borsh 0.10.3", + "num-derive 0.4.2", "num-traits", "solana-program", "spl-token", - "spl-token-2022", + "spl-token-2022 2.0.1", + "thiserror", +] + +[[package]] +name = "spl-discriminator" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa600f2fe56f32e923261719bae640d873edadbc5237681a39b8e37bfd4d263" +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.48", +] + +[[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.48", "thiserror", ] [[package]] name = "spl-memo" -version = "3.0.1" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58e9bae02de3405079a057fe244c867a08f92d48327d231fc60da831f94caf0a" +dependencies = [ + "solana-program", +] + +[[package]] +name = "spl-pod" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5db7e4efb1107b0b8e52a13f035437cdcb36ef99c58f6d467f089d9b2915a" +dependencies = [ + "borsh 0.10.3", + "bytemuck", + "solana-program", + "solana-zk-token-sdk", + "spl-program-error", +] + +[[package]] +name = "spl-program-error" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0dc6f70db6bacea7ff25870b016a65ba1d1b6013536f08e4fd79a8f9005325" +checksum = "7e0657b6490196971d9e729520ba934911ff41fbb2cb9004463dbe23cf8b4b4f" 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.48", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f335787add7fa711819f9e7c573f8145a5358a709446fe2d24bf2a88117c90" +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.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e85e168a785e82564160dcb87b2a8e04cee9bfd1f4d488c729d53d6a4bd300d" +checksum = "95ae123223633a389f95d1da9d49c2d0a50d499e7060b9624626a69e536ad2a4" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", - "num_enum", + "num_enum 0.7.2", "solana-program", "thiserror", ] [[package]] name = "spl-token-2022" -version = "0.5.0" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4abf34a65ba420584a0c35f3903f8d727d1f13ababbdc3f714c6b065a686e86" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive 0.4.2", + "num-traits", + "num_enum 0.7.2", + "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 = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0edb869dbe159b018f17fb9bfa67118c30f232d7f54a73742bc96794dff77ed8" +checksum = "b9fec83597cf7be923c5c3bdfd2fcc08cdfacd2eeb6c4e413da06b6916f50827" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", - "num_enum", + "num_enum 0.7.2", "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.5.1", + "spl-type-length-value", "thiserror", ] +[[package]] +name = "spl-token-group-interface" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb67fbacd587377a400aba81718abe4424d0e9d5ea510034d3b7f130d102153" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", +] + +[[package]] +name = "spl-token-metadata-interface" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16aa8f64b6e0eaab3f5034e84d867c8435d8216497b4543a4978a31f4b6e8a8" +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.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6dfe329fcff44cbe2eea994bd8f737f0b0a69faed39e56f9b6ee03badf7e14" +dependencies = [ + "arrayref", + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-tlv-account-resolution 0.5.2", + "spl-type-length-value", +] + +[[package]] +name = "spl-type-length-value" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f9ebd75d29c5f48de5f6a9c114e08531030b75b8ac2c557600ac7da0b73b1e8" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -2066,6 +2454,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "synstructure" version = "0.12.6" @@ -2074,7 +2473,7 @@ checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", "unicode-xid", ] @@ -2103,22 +2502,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -2258,9 +2657,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2268,24 +2667,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.48", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2293,22 +2692,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" [[package]] name = "web-sys" @@ -2322,18 +2721,18 @@ dependencies = [ [[package]] name = "whirlpool" -version = "0.2.0" +version = "0.3.0" dependencies = [ "anchor-lang", "anchor-spl", - "borsh", - "mpl-token-metadata", + "borsh 0.9.3", "proptest", "serde", "serde_json", - "serde_with", + "serde_with 1.14.0", "solana-program", "spl-token", + "spl-transfer-hook-interface 0.5.1", "thiserror", "uint", ] @@ -2427,10 +2826,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" [[package]] -name = "yansi" -version = "0.5.1" +name = "zerocopy" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] [[package]] name = "zeroize" @@ -2449,6 +2862,6 @@ checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", "synstructure", ] diff --git a/programs/whirlpool/Cargo.toml b/programs/whirlpool/Cargo.toml index 88fec2658..e65f205fa 100644 --- a/programs/whirlpool/Cargo.toml +++ b/programs/whirlpool/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "whirlpool" -version = "0.2.0" +version = "0.3.0" description = "Created with Anchor" edition = "2018" @@ -15,14 +15,14 @@ cpi = ["no-entrypoint"] default = [] [dependencies] -anchor-lang = "0.26" -anchor-spl = "0.26" -spl-token = {version = "3.3", features = ["no-entrypoint"]} -solana-program = "1.14.12" +anchor-lang = "0.29" +anchor-spl = {version = "0.29", features = ["metadata", "memo"]} +spl-token = {version = "4", features = ["no-entrypoint"]} +spl-transfer-hook-interface = "0.5.1" +solana-program = "1.17" thiserror = "1.0" uint = {version = "0.9.1", default-features = false} -borsh = "0.9.1" -mpl-token-metadata = { version = "1.7", features = ["no-entrypoint"] } +borsh09 = {package = "borsh", version = "0.9.1"} [dev-dependencies] proptest = "1.0" diff --git a/programs/whirlpool/src/constants/mod.rs b/programs/whirlpool/src/constants/mod.rs index e8085c433..b4c1e9150 100644 --- a/programs/whirlpool/src/constants/mod.rs +++ b/programs/whirlpool/src/constants/mod.rs @@ -1,4 +1,5 @@ pub mod nft; +pub mod transfer_memo; pub mod test_constants; pub use nft::*; diff --git a/programs/whirlpool/src/constants/transfer_memo.rs b/programs/whirlpool/src/constants/transfer_memo.rs new file mode 100644 index 000000000..cf6b380d7 --- /dev/null +++ b/programs/whirlpool/src/constants/transfer_memo.rs @@ -0,0 +1,5 @@ +pub const TRANSFER_MEMO_COLLECT_PROTOCOL_FEES: &str = "Orca CollectProtocolFees"; +pub const TRANSFER_MEMO_COLLECT_FEES: &str = "Orca CollectFees"; +pub const TRANSFER_MEMO_COLLECT_REWARD: &str = "Orca CollectReward"; +pub const TRANSFER_MEMO_DECREASE_LIQUIDITY: &str = "Orca Withdraw"; +pub const TRANSFER_MEMO_SWAP: &str = "Orca Trade"; diff --git a/programs/whirlpool/src/errors.rs b/programs/whirlpool/src/errors.rs index bdf11e684..4f08051dd 100644 --- a/programs/whirlpool/src/errors.rs +++ b/programs/whirlpool/src/errors.rs @@ -6,115 +6,135 @@ use anchor_lang::prelude::*; #[derive(PartialEq)] pub enum ErrorCode { #[msg("Enum value could not be converted")] - InvalidEnum, // 0x1770 + InvalidEnum, // 0x1770 (6000) #[msg("Invalid start tick index provided.")] - InvalidStartTick, // 0x1771 + InvalidStartTick, // 0x1771 (6001) #[msg("Tick-array already exists in this whirlpool")] - TickArrayExistInPool, // 0x1772 + TickArrayExistInPool, // 0x1772 (6002) #[msg("Attempt to search for a tick-array failed")] - TickArrayIndexOutofBounds, // 0x1773 + TickArrayIndexOutofBounds, // 0x1773 (6003) #[msg("Tick-spacing is not supported")] - InvalidTickSpacing, // 0x1774 + InvalidTickSpacing, // 0x1774 (6004) #[msg("Position is not empty It cannot be closed")] - ClosePositionNotEmpty, // 0x1775 + ClosePositionNotEmpty, // 0x1775 (6005) #[msg("Unable to divide by zero")] - DivideByZero, // 0x1776 + DivideByZero, // 0x1776 (6006) #[msg("Unable to cast number into BigInt")] - NumberCastError, // 0x1777 + NumberCastError, // 0x1777 (6007) #[msg("Unable to down cast number")] - NumberDownCastError, // 0x1778 + NumberDownCastError, // 0x1778 (6008) #[msg("Tick not found within tick array")] - TickNotFound, // 0x1779 + TickNotFound, // 0x1779 (6009) #[msg("Provided tick index is either out of bounds or uninitializable")] - InvalidTickIndex, // 0x177a + InvalidTickIndex, // 0x177a (6010) #[msg("Provided sqrt price out of bounds")] - SqrtPriceOutOfBounds, // 0x177b + SqrtPriceOutOfBounds, // 0x177b (6011) #[msg("Liquidity amount must be greater than zero")] - LiquidityZero, // 0x177c + LiquidityZero, // 0x177c (6012) #[msg("Liquidity amount must be less than i64::MAX")] - LiquidityTooHigh, // 0x177d + LiquidityTooHigh, // 0x177d (6013) #[msg("Liquidity overflow")] - LiquidityOverflow, // 0x177e + LiquidityOverflow, // 0x177e (6014) #[msg("Liquidity underflow")] - LiquidityUnderflow, // 0x177f + LiquidityUnderflow, // 0x177f (6015) #[msg("Tick liquidity net underflowed or overflowed")] - LiquidityNetError, // 0x1780 + LiquidityNetError, // 0x1780 (6016) #[msg("Exceeded token max")] - TokenMaxExceeded, // 0x1781 + TokenMaxExceeded, // 0x1781 (6017) #[msg("Did not meet token min")] - TokenMinSubceeded, // 0x1782 + TokenMinSubceeded, // 0x1782 (6018) #[msg("Position token account has a missing or invalid delegate")] - MissingOrInvalidDelegate, // 0x1783 + MissingOrInvalidDelegate, // 0x1783 (6019) #[msg("Position token amount must be 1")] - InvalidPositionTokenAmount, // 0x1784 + InvalidPositionTokenAmount, // 0x1784 (6020) #[msg("Timestamp should be convertible from i64 to u64")] - InvalidTimestampConversion, // 0x1785 + InvalidTimestampConversion, // 0x1785 (6021) #[msg("Timestamp should be greater than the last updated timestamp")] - InvalidTimestamp, // 0x1786 + InvalidTimestamp, // 0x1786 (6022) #[msg("Invalid tick array sequence provided for instruction.")] - InvalidTickArraySequence, // 0x1787 + InvalidTickArraySequence, // 0x1787 (6023) #[msg("Token Mint in wrong order")] - InvalidTokenMintOrder, // 0x1788 + InvalidTokenMintOrder, // 0x1788 (6024) #[msg("Reward not initialized")] - RewardNotInitialized, // 0x1789 + RewardNotInitialized, // 0x1789 (6025) #[msg("Invalid reward index")] - InvalidRewardIndex, // 0x178a + InvalidRewardIndex, // 0x178a (6026) #[msg("Reward vault requires amount to support emissions for at least one day")] - RewardVaultAmountInsufficient, // 0x178b + RewardVaultAmountInsufficient, // 0x178b (6027) #[msg("Exceeded max fee rate")] - FeeRateMaxExceeded, // 0x178c + FeeRateMaxExceeded, // 0x178c (6028) #[msg("Exceeded max protocol fee rate")] - ProtocolFeeRateMaxExceeded, // 0x178d + ProtocolFeeRateMaxExceeded, // 0x178d (6029) #[msg("Multiplication with shift right overflow")] - MultiplicationShiftRightOverflow, // 0x178e + MultiplicationShiftRightOverflow, // 0x178e (6030) #[msg("Muldiv overflow")] - MulDivOverflow, // 0x178f + MulDivOverflow, // 0x178f (6031) #[msg("Invalid div_u256 input")] - MulDivInvalidInput, //0x1790 + MulDivInvalidInput, // 0x1790 (6032) #[msg("Multiplication overflow")] - MultiplicationOverflow, //0x1791 + MultiplicationOverflow, // 0x1791 (6033) #[msg("Provided SqrtPriceLimit not in the same direction as the swap.")] - InvalidSqrtPriceLimitDirection, //0x1792 + InvalidSqrtPriceLimitDirection, // 0x1792 (6034) #[msg("There are no tradable amount to swap.")] - ZeroTradableAmount, //0x1793 + ZeroTradableAmount, // 0x1793 (6035) #[msg("Amount out below minimum threshold")] - AmountOutBelowMinimum, //0x1794 + AmountOutBelowMinimum, // 0x1794 (6036) #[msg("Amount in above maximum threshold")] - AmountInAboveMaximum, //0x1795 + AmountInAboveMaximum, // 0x1795 (6037) #[msg("Invalid index for tick array sequence")] - TickArraySequenceInvalidIndex, //0x1796 + TickArraySequenceInvalidIndex, // 0x1796 (6038) #[msg("Amount calculated overflows")] - AmountCalcOverflow, //0x1797 + AmountCalcOverflow, // 0x1797 (6039) #[msg("Amount remaining overflows")] - AmountRemainingOverflow, //0x1798 + AmountRemainingOverflow, // 0x1798 (6040) #[msg("Invalid intermediary mint")] - InvalidIntermediaryMint, //0x1799 + InvalidIntermediaryMint, // 0x1799 (6041) #[msg("Duplicate two hop pool")] - DuplicateTwoHopPool, //0x179a + DuplicateTwoHopPool, // 0x179a (6042) #[msg("Bundle index is out of bounds")] - InvalidBundleIndex, //0x179b + InvalidBundleIndex, // 0x179b (6043) #[msg("Position has already been opened")] - BundledPositionAlreadyOpened, //0x179c + BundledPositionAlreadyOpened, // 0x179c (6044) #[msg("Position has already been closed")] - BundledPositionAlreadyClosed, //0x179d + BundledPositionAlreadyClosed, // 0x179d (6045) #[msg("Unable to delete PositionBundle with open positions")] - PositionBundleNotDeletable, //0x179e + PositionBundleNotDeletable, // 0x179e (6046) + + #[msg("Token mint has unsupported attributes")] + UnsupportedTokenMint, // 0x179f (6047) + + #[msg("Invalid remaining accounts")] + RemainingAccountsInvalidSlice, // 0x17a0 (6048) + #[msg("Insufficient remaining accounts")] + RemainingAccountsInsufficient, // 0x17a1 (6049) + + #[msg("Unable to call transfer hook without extra accounts")] + NoExtraAccountsForTransferHook, // 0x17a2 (6050) + + #[msg("Output and input amount mismatch")] + IntermediateTokenAmountMismatch, // 0x17a3 (6051) + + #[msg("Transfer fee calculation failed")] + TransferFeeCalculationError, // 0x17a4 (6052) + + #[msg("Same accounts type is provided more than once")] + RemainingAccountsDuplicatedAccountsType, // 0x17a5 (6053) } impl From for ErrorCode { diff --git a/programs/whirlpool/src/instructions/initialize_pool.rs b/programs/whirlpool/src/instructions/initialize_pool.rs index ff5a58384..2b0fe4567 100644 --- a/programs/whirlpool/src/instructions/initialize_pool.rs +++ b/programs/whirlpool/src/instructions/initialize_pool.rs @@ -3,6 +3,7 @@ use anchor_lang::prelude::*; use anchor_spl::token::{self, Mint, Token, TokenAccount}; #[derive(Accounts)] +// now we don't use bumps, but we must list args in the same order to use tick_spacing arg. #[instruction(bumps: WhirlpoolBumps, tick_spacing: u16)] pub struct InitializePool<'info> { pub whirlpools_config: Box>, @@ -38,7 +39,7 @@ pub struct InitializePool<'info> { token::authority = whirlpool)] pub token_vault_b: Box>, - #[account(has_one = whirlpools_config)] + #[account(has_one = whirlpools_config, constraint = fee_tier.tick_spacing == tick_spacing)] pub fee_tier: Account<'info, FeeTier>, #[account(address = token::ID)] @@ -62,7 +63,7 @@ pub fn handler( let default_fee_rate = ctx.accounts.fee_tier.default_fee_rate; // ignore the bump passed and use one Anchor derived - let bump = *ctx.bumps.get("whirlpool").unwrap(); + let bump = ctx.bumps.whirlpool; Ok(whirlpool.initialize( whirlpools_config, diff --git a/programs/whirlpool/src/instructions/initialize_position_bundle.rs b/programs/whirlpool/src/instructions/initialize_position_bundle.rs index ffd042962..4496098e2 100644 --- a/programs/whirlpool/src/instructions/initialize_position_bundle.rs +++ b/programs/whirlpool/src/instructions/initialize_position_bundle.rs @@ -47,7 +47,7 @@ pub fn handler(ctx: Context) -> Result<()> { position_bundle.initialize(position_bundle_mint.key())?; - let bump = *ctx.bumps.get("position_bundle").unwrap(); + let bump = ctx.bumps.position_bundle; mint_position_bundle_token_and_remove_authority( &ctx.accounts.position_bundle, diff --git a/programs/whirlpool/src/instructions/initialize_position_bundle_with_metadata.rs b/programs/whirlpool/src/instructions/initialize_position_bundle_with_metadata.rs index 98a955393..dc49ca59c 100644 --- a/programs/whirlpool/src/instructions/initialize_position_bundle_with_metadata.rs +++ b/programs/whirlpool/src/instructions/initialize_position_bundle_with_metadata.rs @@ -1,6 +1,7 @@ use anchor_lang::prelude::*; use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token::{self, Mint, Token, TokenAccount}; +use anchor_spl::metadata::Metadata; use crate::constants::nft::whirlpool_nft_update_auth::ID as WPB_NFT_UPDATE_AUTH; use crate::{state::*, util::mint_position_bundle_token_with_metadata_and_remove_authority}; @@ -50,9 +51,7 @@ pub struct InitializePositionBundleWithMetadata<'info> { pub rent: Sysvar<'info, Rent>, pub associated_token_program: Program<'info, AssociatedToken>, - /// CHECK: checked via account constraints - #[account(address = mpl_token_metadata::ID)] - pub metadata_program: UncheckedAccount<'info>, + pub metadata_program: Program<'info, Metadata>, } pub fn handler(ctx: Context) -> Result<()> { @@ -61,7 +60,7 @@ pub fn handler(ctx: Context) -> Result<()> position_bundle.initialize(position_bundle_mint.key())?; - let bump = *ctx.bumps.get("position_bundle").unwrap(); + let bump = ctx.bumps.position_bundle; mint_position_bundle_token_with_metadata_and_remove_authority( &ctx.accounts.funder, diff --git a/programs/whirlpool/src/instructions/mod.rs b/programs/whirlpool/src/instructions/mod.rs index 15f1d24b1..043a47811 100644 --- a/programs/whirlpool/src/instructions/mod.rs +++ b/programs/whirlpool/src/instructions/mod.rs @@ -61,3 +61,6 @@ pub use set_reward_emissions_super_authority::*; pub use swap::*; pub use two_hop_swap::*; pub use update_fees_and_rewards::*; + +pub mod v2; +pub use v2::*; diff --git a/programs/whirlpool/src/instructions/open_position.rs b/programs/whirlpool/src/instructions/open_position.rs index 8738b8327..16ad2a959 100644 --- a/programs/whirlpool/src/instructions/open_position.rs +++ b/programs/whirlpool/src/instructions/open_position.rs @@ -3,9 +3,9 @@ use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token::{self, Mint, Token, TokenAccount}; use crate::{state::*, util::mint_position_token_and_remove_authority}; +use crate::state; #[derive(Accounts)] -#[instruction(bumps: OpenPositionBumps)] pub struct OpenPosition<'info> { #[account(mut)] pub funder: Signer<'info>, @@ -49,7 +49,8 @@ pub struct OpenPosition<'info> { */ pub fn handler( ctx: Context, - _bumps: OpenPositionBumps, + // derive(Accounts) generates OpenPositionBumps, so we need to clarify which one we want to use. + _bumps: state::OpenPositionBumps, tick_lower_index: i32, tick_upper_index: i32, ) -> Result<()> { diff --git a/programs/whirlpool/src/instructions/open_position_with_metadata.rs b/programs/whirlpool/src/instructions/open_position_with_metadata.rs index 7505d7cae..2249ac58a 100644 --- a/programs/whirlpool/src/instructions/open_position_with_metadata.rs +++ b/programs/whirlpool/src/instructions/open_position_with_metadata.rs @@ -1,13 +1,14 @@ use anchor_lang::prelude::*; use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token::{self, Mint, Token, TokenAccount}; +use anchor_spl::metadata::Metadata; use crate::{state::*, util::mint_position_token_with_metadata_and_remove_authority}; +use crate::state; use crate::constants::nft::whirlpool_nft_update_auth::ID as WP_NFT_UPDATE_AUTH; #[derive(Accounts)] -#[instruction(bumps: OpenPositionWithMetadataBumps)] pub struct OpenPositionWithMetadata<'info> { #[account(mut)] pub funder: Signer<'info>, @@ -50,9 +51,7 @@ pub struct OpenPositionWithMetadata<'info> { pub rent: Sysvar<'info, Rent>, pub associated_token_program: Program<'info, AssociatedToken>, - /// CHECK: checked via account constraints - #[account(address = mpl_token_metadata::ID)] - pub metadata_program: UncheckedAccount<'info>, + pub metadata_program: Program<'info, Metadata>, /// CHECK: checked via account constraints #[account(address = WP_NFT_UPDATE_AUTH)] @@ -64,7 +63,8 @@ pub struct OpenPositionWithMetadata<'info> { */ pub fn handler( ctx: Context, - _bumps: OpenPositionWithMetadataBumps, + // derive(Accounts) generates OpenPositionWithMetadataBumps, so we need to clarify which one we want to use. + _bumps: state::OpenPositionWithMetadataBumps, tick_lower_index: i32, tick_upper_index: i32, ) -> Result<()> { diff --git a/programs/whirlpool/src/instructions/v2/collect_fees.rs b/programs/whirlpool/src/instructions/v2/collect_fees.rs new file mode 100644 index 000000000..0f8baa48b --- /dev/null +++ b/programs/whirlpool/src/instructions/v2/collect_fees.rs @@ -0,0 +1,105 @@ +use anchor_lang::prelude::*; +use anchor_spl::token; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use anchor_spl::memo::Memo; + +use crate::util::{parse_remaining_accounts, AccountsType, RemainingAccountsInfo}; +use crate::{ + constants::transfer_memo, + state::*, + util::{v2::transfer_from_vault_to_owner_v2, verify_position_authority}, +}; + +#[derive(Accounts)] +pub struct CollectFeesV2<'info> { + pub whirlpool: Box>, + + pub position_authority: Signer<'info>, + + #[account(mut, has_one = whirlpool)] + pub position: Box>, + #[account( + constraint = position_token_account.mint == position.position_mint, + constraint = position_token_account.amount == 1 + )] + pub position_token_account: Box>, + + #[account(address = whirlpool.token_mint_a)] + pub token_mint_a: InterfaceAccount<'info, Mint>, + #[account(address = whirlpool.token_mint_b)] + pub token_mint_b: InterfaceAccount<'info, Mint>, + + #[account(mut, constraint = token_owner_account_a.mint == whirlpool.token_mint_a)] + pub token_owner_account_a: Box>, + #[account(mut, address = whirlpool.token_vault_a)] + pub token_vault_a: Box>, + + #[account(mut, constraint = token_owner_account_b.mint == whirlpool.token_mint_b)] + pub token_owner_account_b: Box>, + #[account(mut, address = whirlpool.token_vault_b)] + pub token_vault_b: Box>, + + #[account(address = token_mint_a.to_account_info().owner.clone())] + pub token_program_a: Interface<'info, TokenInterface>, + #[account(address = token_mint_b.to_account_info().owner.clone())] + pub token_program_b: Interface<'info, TokenInterface>, + pub memo_program: Program<'info, Memo>, + + // remaining accounts + // - accounts for transfer hook program of token_mint_a + // - accounts for transfer hook program of token_mint_b +} + +pub fn handler<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, CollectFeesV2<'info>>, + remaining_accounts_info: Option, +) -> Result<()> { + verify_position_authority( + &ctx.accounts.position_token_account, + &ctx.accounts.position_authority, + )?; + + // Process remaining accounts + let remaining_accounts = parse_remaining_accounts( + &ctx.remaining_accounts, + &remaining_accounts_info, + &[ + AccountsType::TransferHookA, + AccountsType::TransferHookB, + ], + )?; + + let position = &mut ctx.accounts.position; + + // Store the fees owed to use as transfer amounts. + let fee_owed_a = position.fee_owed_a; + let fee_owed_b = position.fee_owed_b; + + position.reset_fees_owed(); + + transfer_from_vault_to_owner_v2( + &ctx.accounts.whirlpool, + &ctx.accounts.token_mint_a, + &ctx.accounts.token_vault_a, + &ctx.accounts.token_owner_account_a, + &ctx.accounts.token_program_a, + &ctx.accounts.memo_program, + &remaining_accounts.transfer_hook_a, + fee_owed_a, + transfer_memo::TRANSFER_MEMO_COLLECT_FEES.as_bytes(), + )?; + + transfer_from_vault_to_owner_v2( + &ctx.accounts.whirlpool, + &ctx.accounts.token_mint_b, + &ctx.accounts.token_vault_b, + &ctx.accounts.token_owner_account_b, + &ctx.accounts.token_program_b, + &ctx.accounts.memo_program, + &remaining_accounts.transfer_hook_b, + fee_owed_b, + transfer_memo::TRANSFER_MEMO_COLLECT_FEES.as_bytes(), + )?; + + Ok(()) +} diff --git a/programs/whirlpool/src/instructions/v2/collect_protocol_fees.rs b/programs/whirlpool/src/instructions/v2/collect_protocol_fees.rs new file mode 100644 index 000000000..c7f996904 --- /dev/null +++ b/programs/whirlpool/src/instructions/v2/collect_protocol_fees.rs @@ -0,0 +1,86 @@ +use crate::util::{parse_remaining_accounts, AccountsType, RemainingAccountsInfo}; +use crate::{constants::transfer_memo, state::*, util::v2::transfer_from_vault_to_owner_v2}; +use anchor_lang::prelude::*; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use anchor_spl::memo::Memo; + +#[derive(Accounts)] +pub struct CollectProtocolFeesV2<'info> { + pub whirlpools_config: Box>, + + #[account(mut, has_one = whirlpools_config)] + pub whirlpool: Box>, + + #[account(address = whirlpools_config.collect_protocol_fees_authority)] + pub collect_protocol_fees_authority: Signer<'info>, + + #[account(address = whirlpool.token_mint_a)] + pub token_mint_a: InterfaceAccount<'info, Mint>, + #[account(address = whirlpool.token_mint_b)] + pub token_mint_b: InterfaceAccount<'info, Mint>, + + #[account(mut, address = whirlpool.token_vault_a)] + pub token_vault_a: InterfaceAccount<'info, TokenAccount>, + + #[account(mut, address = whirlpool.token_vault_b)] + pub token_vault_b: InterfaceAccount<'info, TokenAccount>, + + #[account(mut, constraint = token_destination_a.mint == whirlpool.token_mint_a)] + pub token_destination_a: InterfaceAccount<'info, TokenAccount>, + + #[account(mut, constraint = token_destination_b.mint == whirlpool.token_mint_b)] + pub token_destination_b: InterfaceAccount<'info, TokenAccount>, + + #[account(address = token_mint_a.to_account_info().owner.clone())] + pub token_program_a: Interface<'info, TokenInterface>, + #[account(address = token_mint_b.to_account_info().owner.clone())] + pub token_program_b: Interface<'info, TokenInterface>, + pub memo_program: Program<'info, Memo>, + + // remaining accounts + // - accounts for transfer hook program of token_mint_a + // - accounts for transfer hook program of token_mint_b +} + +pub fn handler<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, CollectProtocolFeesV2<'info>>, + remaining_accounts_info: Option, +) -> Result<()> { + let whirlpool = &ctx.accounts.whirlpool; + + // Process remaining accounts + let remaining_accounts = parse_remaining_accounts( + &ctx.remaining_accounts, + &remaining_accounts_info, + &[ + AccountsType::TransferHookA, + AccountsType::TransferHookB, + ], + )?; + + transfer_from_vault_to_owner_v2( + whirlpool, + &ctx.accounts.token_mint_a, + &ctx.accounts.token_vault_a, + &ctx.accounts.token_destination_a, + &ctx.accounts.token_program_a, + &ctx.accounts.memo_program, + &remaining_accounts.transfer_hook_a, + whirlpool.protocol_fee_owed_a, + transfer_memo::TRANSFER_MEMO_COLLECT_PROTOCOL_FEES.as_bytes(), + )?; + + transfer_from_vault_to_owner_v2( + whirlpool, + &ctx.accounts.token_mint_b, + &ctx.accounts.token_vault_b, + &ctx.accounts.token_destination_b, + &ctx.accounts.token_program_b, + &ctx.accounts.memo_program, + &remaining_accounts.transfer_hook_b, + whirlpool.protocol_fee_owed_b, + transfer_memo::TRANSFER_MEMO_COLLECT_PROTOCOL_FEES.as_bytes(), + )?; + + Ok(ctx.accounts.whirlpool.reset_protocol_fees_owed()) +} diff --git a/programs/whirlpool/src/instructions/v2/collect_reward.rs b/programs/whirlpool/src/instructions/v2/collect_reward.rs new file mode 100644 index 000000000..111ca54b0 --- /dev/null +++ b/programs/whirlpool/src/instructions/v2/collect_reward.rs @@ -0,0 +1,143 @@ +use anchor_lang::prelude::*; +use anchor_spl::token; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use anchor_spl::memo::Memo; + +use crate::util::{parse_remaining_accounts, AccountsType, RemainingAccountsInfo}; +use crate::{ + constants::transfer_memo, + state::*, + util::{v2::transfer_from_vault_to_owner_v2, verify_position_authority}, +}; + +#[derive(Accounts)] +#[instruction(reward_index: u8)] +pub struct CollectRewardV2<'info> { + pub whirlpool: Box>, + + pub position_authority: Signer<'info>, + + #[account(mut, has_one = whirlpool)] + pub position: Box>, + #[account( + constraint = position_token_account.mint == position.position_mint, + constraint = position_token_account.amount == 1 + )] + pub position_token_account: Box>, + + #[account(mut, + constraint = reward_owner_account.mint == whirlpool.reward_infos[reward_index as usize].mint + )] + pub reward_owner_account: Box>, + + #[account(address = whirlpool.reward_infos[reward_index as usize].mint)] + pub reward_mint: Box>, + + #[account(mut, address = whirlpool.reward_infos[reward_index as usize].vault)] + pub reward_vault: Box>, + + #[account(address = reward_mint.to_account_info().owner.clone())] + pub reward_token_program: Interface<'info, TokenInterface>, + pub memo_program: Program<'info, Memo>, + + // remaining accounts + // - accounts for transfer hook program of reward_mint +} + +/// Collects all harvestable tokens for a specified reward. +/// +/// If the Whirlpool reward vault does not have enough tokens, the maximum number of available +/// tokens will be debited to the user. The unharvested amount remains tracked, and it can be +/// harvested in the future. +/// +/// # Parameters +/// - `reward_index` - The reward to harvest. Acceptable values are 0, 1, and 2. +/// +/// # Returns +/// - `Ok`: Reward tokens at the specified reward index have been successfully harvested +/// - `Err`: `RewardNotInitialized` if the specified reward has not been initialized +/// `InvalidRewardIndex` if the reward index is not 0, 1, or 2 +pub fn handler<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, CollectRewardV2<'info>>, + reward_index: u8, + remaining_accounts_info: Option, +) -> Result<()> { + verify_position_authority( + &ctx.accounts.position_token_account, + &ctx.accounts.position_authority, + )?; + + // Process remaining accounts + let remaining_accounts = parse_remaining_accounts( + &ctx.remaining_accounts, + &remaining_accounts_info, + &[ + AccountsType::TransferHookReward, + ], + )?; + + let index = reward_index as usize; + + let position = &mut ctx.accounts.position; + let (transfer_amount, updated_amount_owed) = calculate_collect_reward( + position.reward_infos[index], + ctx.accounts.reward_vault.amount, + ); + + position.update_reward_owed(index, updated_amount_owed); + + Ok(transfer_from_vault_to_owner_v2( + &ctx.accounts.whirlpool, + &ctx.accounts.reward_mint, + &ctx.accounts.reward_vault, + &ctx.accounts.reward_owner_account, + &ctx.accounts.reward_token_program, + &ctx.accounts.memo_program, + &remaining_accounts.transfer_hook_reward, + transfer_amount, + transfer_memo::TRANSFER_MEMO_COLLECT_REWARD.as_bytes(), + )?) +} + +// TODO: refactor (remove (dup)) +fn calculate_collect_reward(position_reward: PositionRewardInfo, vault_amount: u64) -> (u64, u64) { + let amount_owed = position_reward.amount_owed; + let (transfer_amount, updated_amount_owed) = if amount_owed > vault_amount { + (vault_amount, amount_owed - vault_amount) + } else { + (amount_owed, 0) + }; + + (transfer_amount, updated_amount_owed) +} + +#[cfg(test)] +mod unit_tests { + use super::calculate_collect_reward; + use crate::state::PositionRewardInfo; + + #[test] + fn test_calculate_collect_reward_vault_insufficient_tokens() { + let (transfer_amount, updated_amount_owed) = + calculate_collect_reward(position_reward(10), 1); + + assert_eq!(transfer_amount, 1); + assert_eq!(updated_amount_owed, 9); + } + + #[test] + fn test_calculate_collect_reward_vault_sufficient_tokens() { + let (transfer_amount, updated_amount_owed) = + calculate_collect_reward(position_reward(10), 10); + + assert_eq!(transfer_amount, 10); + assert_eq!(updated_amount_owed, 0); + } + + fn position_reward(amount_owed: u64) -> PositionRewardInfo { + PositionRewardInfo { + amount_owed, + ..Default::default() + } + } +} diff --git a/programs/whirlpool/src/instructions/v2/decrease_liquidity.rs b/programs/whirlpool/src/instructions/v2/decrease_liquidity.rs new file mode 100644 index 000000000..14f977bce --- /dev/null +++ b/programs/whirlpool/src/instructions/v2/decrease_liquidity.rs @@ -0,0 +1,115 @@ +use anchor_lang::prelude::*; + +use crate::errors::ErrorCode; +use crate::manager::liquidity_manager::{ + calculate_liquidity_token_deltas, calculate_modify_liquidity, sync_modify_liquidity_values, +}; +use crate::math::convert_to_liquidity_delta; +use crate::util::{calculate_transfer_fee_excluded_amount, parse_remaining_accounts, AccountsType, RemainingAccountsInfo}; +use crate::util::{to_timestamp_u64, v2::transfer_from_vault_to_owner_v2, verify_position_authority}; +use crate::constants::transfer_memo; + +use super::ModifyLiquidityV2; + +/* + Removes liquidity from an existing Whirlpool Position. +*/ +pub fn handler<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, ModifyLiquidityV2<'info>>, + liquidity_amount: u128, + token_min_a: u64, + token_min_b: u64, + remaining_accounts_info: Option, +) -> Result<()> { + verify_position_authority( + &ctx.accounts.position_token_account, + &ctx.accounts.position_authority, + )?; + + let clock = Clock::get()?; + + if liquidity_amount == 0 { + return Err(ErrorCode::LiquidityZero.into()); + } + + // Process remaining accounts + let remaining_accounts = parse_remaining_accounts( + &ctx.remaining_accounts, + &remaining_accounts_info, + &[ + AccountsType::TransferHookA, + AccountsType::TransferHookB, + ], + )?; + + let liquidity_delta = convert_to_liquidity_delta(liquidity_amount, false)?; + let timestamp = to_timestamp_u64(clock.unix_timestamp)?; + + let update = calculate_modify_liquidity( + &ctx.accounts.whirlpool, + &ctx.accounts.position, + &ctx.accounts.tick_array_lower, + &ctx.accounts.tick_array_upper, + liquidity_delta, + timestamp, + )?; + + sync_modify_liquidity_values( + &mut ctx.accounts.whirlpool, + &mut ctx.accounts.position, + &ctx.accounts.tick_array_lower, + &ctx.accounts.tick_array_upper, + update, + timestamp, + )?; + + let (delta_a, delta_b) = calculate_liquidity_token_deltas( + ctx.accounts.whirlpool.tick_current_index, + ctx.accounts.whirlpool.sqrt_price, + &ctx.accounts.position, + liquidity_delta, + )?; + + let transfer_fee_excluded_delta_a = calculate_transfer_fee_excluded_amount( + &ctx.accounts.token_mint_a, + delta_a + )?; + let transfer_fee_excluded_delta_b = calculate_transfer_fee_excluded_amount( + &ctx.accounts.token_mint_b, + delta_b + )?; + + // token_min_a and token_min_b should be applied to the transfer fee excluded amount + if transfer_fee_excluded_delta_a.amount < token_min_a { + return Err(ErrorCode::TokenMinSubceeded.into()); + } + if transfer_fee_excluded_delta_b.amount < token_min_b { + return Err(ErrorCode::TokenMinSubceeded.into()); + } + + transfer_from_vault_to_owner_v2( + &ctx.accounts.whirlpool, + &ctx.accounts.token_mint_a, + &ctx.accounts.token_vault_a, + &ctx.accounts.token_owner_account_a, + &ctx.accounts.token_program_a, + &ctx.accounts.memo_program, + &remaining_accounts.transfer_hook_a, + delta_a, + transfer_memo::TRANSFER_MEMO_DECREASE_LIQUIDITY.as_bytes(), + )?; + + transfer_from_vault_to_owner_v2( + &ctx.accounts.whirlpool, + &ctx.accounts.token_mint_b, + &ctx.accounts.token_vault_b, + &ctx.accounts.token_owner_account_b, + &ctx.accounts.token_program_b, + &ctx.accounts.memo_program, + &remaining_accounts.transfer_hook_b, + delta_b, + transfer_memo::TRANSFER_MEMO_DECREASE_LIQUIDITY.as_bytes(), + )?; + + Ok(()) +} diff --git a/programs/whirlpool/src/instructions/v2/delete_token_badge.rs b/programs/whirlpool/src/instructions/v2/delete_token_badge.rs new file mode 100644 index 000000000..09a653ff6 --- /dev/null +++ b/programs/whirlpool/src/instructions/v2/delete_token_badge.rs @@ -0,0 +1,39 @@ +use crate::state::*; +use anchor_lang::prelude::*; +use anchor_spl::token_interface::Mint; + +#[derive(Accounts)] +pub struct DeleteTokenBadge<'info> { + pub whirlpools_config: Box>, + + #[account(has_one = whirlpools_config)] + pub whirlpools_config_extension: Box>, + + #[account(address = whirlpools_config_extension.token_badge_authority)] + pub token_badge_authority: Signer<'info>, + + pub token_mint: InterfaceAccount<'info, Mint>, + + #[account( + mut, + seeds = [ + b"token_badge", + whirlpools_config.key().as_ref(), + token_mint.key().as_ref(), + ], + bump, + has_one = whirlpools_config, + close = receiver + )] + pub token_badge: Account<'info, TokenBadge>, + + /// CHECK: safe, for receiving rent only + #[account(mut)] + pub receiver: UncheckedAccount<'info>, +} + +pub fn handler( + _ctx: Context, +) -> Result<()> { + Ok(()) +} diff --git a/programs/whirlpool/src/instructions/v2/increase_liquidity.rs b/programs/whirlpool/src/instructions/v2/increase_liquidity.rs new file mode 100644 index 000000000..e0d0d824b --- /dev/null +++ b/programs/whirlpool/src/instructions/v2/increase_liquidity.rs @@ -0,0 +1,158 @@ +use anchor_lang::prelude::*; +use anchor_spl::token; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use anchor_spl::memo::Memo; + +use crate::errors::ErrorCode; +use crate::manager::liquidity_manager::{ + calculate_liquidity_token_deltas, calculate_modify_liquidity, sync_modify_liquidity_values, +}; +use crate::math::convert_to_liquidity_delta; +use crate::state::*; +use crate::util::{calculate_transfer_fee_included_amount, parse_remaining_accounts, AccountsType, RemainingAccountsInfo}; +use crate::util::{to_timestamp_u64, v2::transfer_from_owner_to_vault_v2, verify_position_authority}; + +#[derive(Accounts)] +pub struct ModifyLiquidityV2<'info> { + #[account(mut)] + pub whirlpool: Account<'info, Whirlpool>, + + #[account(address = token_mint_a.to_account_info().owner.clone())] + pub token_program_a: Interface<'info, TokenInterface>, + #[account(address = token_mint_b.to_account_info().owner.clone())] + pub token_program_b: Interface<'info, TokenInterface>, + + pub memo_program: Program<'info, Memo>, + + pub position_authority: Signer<'info>, + + #[account(mut, has_one = whirlpool)] + pub position: Account<'info, Position>, + #[account( + constraint = position_token_account.mint == position.position_mint, + constraint = position_token_account.amount == 1 + )] + pub position_token_account: Box>, + + #[account(address = whirlpool.token_mint_a)] + pub token_mint_a: InterfaceAccount<'info, Mint>, + #[account(address = whirlpool.token_mint_b)] + pub token_mint_b: InterfaceAccount<'info, Mint>, + + #[account(mut, constraint = token_owner_account_a.mint == whirlpool.token_mint_a)] + pub token_owner_account_a: Box>, + #[account(mut, constraint = token_owner_account_b.mint == whirlpool.token_mint_b)] + pub token_owner_account_b: Box>, + + #[account(mut, constraint = token_vault_a.key() == whirlpool.token_vault_a)] + pub token_vault_a: Box>, + #[account(mut, constraint = token_vault_b.key() == whirlpool.token_vault_b)] + pub token_vault_b: Box>, + + #[account(mut, has_one = whirlpool)] + pub tick_array_lower: AccountLoader<'info, TickArray>, + #[account(mut, has_one = whirlpool)] + pub tick_array_upper: AccountLoader<'info, TickArray>, + + // remaining accounts + // - accounts for transfer hook program of token_mint_a + // - accounts for transfer hook program of token_mint_b +} + +pub fn handler<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, ModifyLiquidityV2<'info>>, + liquidity_amount: u128, + token_max_a: u64, + token_max_b: u64, + remaining_accounts_info: Option, +) -> Result<()> { + verify_position_authority( + &ctx.accounts.position_token_account, + &ctx.accounts.position_authority, + )?; + + let clock = Clock::get()?; + + if liquidity_amount == 0 { + return Err(ErrorCode::LiquidityZero.into()); + } + + // Process remaining accounts + let remaining_accounts = parse_remaining_accounts( + &ctx.remaining_accounts, + &remaining_accounts_info, + &[ + AccountsType::TransferHookA, + AccountsType::TransferHookB, + ], + )?; + + let liquidity_delta = convert_to_liquidity_delta(liquidity_amount, true)?; + let timestamp = to_timestamp_u64(clock.unix_timestamp)?; + + let update = calculate_modify_liquidity( + &ctx.accounts.whirlpool, + &ctx.accounts.position, + &ctx.accounts.tick_array_lower, + &ctx.accounts.tick_array_upper, + liquidity_delta, + timestamp, + )?; + + sync_modify_liquidity_values( + &mut ctx.accounts.whirlpool, + &mut ctx.accounts.position, + &ctx.accounts.tick_array_lower, + &ctx.accounts.tick_array_upper, + update, + timestamp, + )?; + + let (delta_a, delta_b) = calculate_liquidity_token_deltas( + ctx.accounts.whirlpool.tick_current_index, + ctx.accounts.whirlpool.sqrt_price, + &ctx.accounts.position, + liquidity_delta, + )?; + + let transfer_fee_included_delta_a = calculate_transfer_fee_included_amount( + &ctx.accounts.token_mint_a, + delta_a + )?; + let transfer_fee_included_delta_b = calculate_transfer_fee_included_amount( + &ctx.accounts.token_mint_b, + delta_b + )?; + + // token_max_a and token_max_b should be applied to the transfer fee included amount + if transfer_fee_included_delta_a.amount > token_max_a { + return Err(ErrorCode::TokenMaxExceeded.into()); + } + if transfer_fee_included_delta_b.amount > token_max_b { + return Err(ErrorCode::TokenMaxExceeded.into()); + } + + transfer_from_owner_to_vault_v2( + &ctx.accounts.position_authority, + &ctx.accounts.token_mint_a, + &ctx.accounts.token_owner_account_a, + &ctx.accounts.token_vault_a, + &ctx.accounts.token_program_a, + &ctx.accounts.memo_program, + &remaining_accounts.transfer_hook_a, + transfer_fee_included_delta_a.amount, + )?; + + transfer_from_owner_to_vault_v2( + &ctx.accounts.position_authority, + &ctx.accounts.token_mint_b, + &ctx.accounts.token_owner_account_b, + &ctx.accounts.token_vault_b, + &ctx.accounts.token_program_b, + &ctx.accounts.memo_program, + &remaining_accounts.transfer_hook_b, + transfer_fee_included_delta_b.amount, + )?; + + Ok(()) +} diff --git a/programs/whirlpool/src/instructions/v2/initialize_config_extension.rs b/programs/whirlpool/src/instructions/v2/initialize_config_extension.rs new file mode 100644 index 000000000..32a5004cc --- /dev/null +++ b/programs/whirlpool/src/instructions/v2/initialize_config_extension.rs @@ -0,0 +1,38 @@ +use crate::state::*; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct InitializeConfigExtension<'info> { + pub config: Box>, + + #[account(init, + payer = funder, + seeds = [ + b"config_extension", + config.key().as_ref(), + ], + bump, + space = WhirlpoolsConfigExtension::LEN)] + pub config_extension: Account<'info, WhirlpoolsConfigExtension>, + + #[account(mut)] + pub funder: Signer<'info>, + + // fee_authority can initialize config extension + #[account(address = config.fee_authority)] + pub fee_authority: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +pub fn handler( + ctx: Context, +) -> Result<()> { + Ok(ctx + .accounts + .config_extension + .initialize( + ctx.accounts.config.key(), + ctx.accounts.fee_authority.key(), + )?) +} diff --git a/programs/whirlpool/src/instructions/v2/initialize_pool.rs b/programs/whirlpool/src/instructions/v2/initialize_pool.rs new file mode 100644 index 000000000..9c34ae41f --- /dev/null +++ b/programs/whirlpool/src/instructions/v2/initialize_pool.rs @@ -0,0 +1,114 @@ +use anchor_lang::prelude::*; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; + +use crate::{ + errors::ErrorCode, + state::*, + util::{is_token_badge_initialized, v2::is_supported_token_mint} +}; + +#[derive(Accounts)] +#[instruction(tick_spacing: u16)] +pub struct InitializePoolV2<'info> { + pub whirlpools_config: Box>, + + pub token_mint_a: InterfaceAccount<'info, Mint>, + pub token_mint_b: InterfaceAccount<'info, Mint>, + + #[account(seeds = [b"token_badge", whirlpools_config.key().as_ref(), token_mint_a.key().as_ref()], bump)] + /// CHECK: checked in the handler + pub token_badge_a: UncheckedAccount<'info>, + #[account(seeds = [b"token_badge", whirlpools_config.key().as_ref(), token_mint_b.key().as_ref()], bump)] + /// CHECK: checked in the handler + pub token_badge_b: UncheckedAccount<'info>, + + #[account(mut)] + pub funder: Signer<'info>, + + #[account(init, + seeds = [ + b"whirlpool".as_ref(), + whirlpools_config.key().as_ref(), + token_mint_a.key().as_ref(), + token_mint_b.key().as_ref(), + tick_spacing.to_le_bytes().as_ref() + ], + bump, + payer = funder, + space = Whirlpool::LEN)] + pub whirlpool: Box>, + + #[account(init, + payer = funder, + token::token_program = token_program_a, + token::mint = token_mint_a, + token::authority = whirlpool)] + pub token_vault_a: Box>, + + #[account(init, + payer = funder, + token::token_program = token_program_b, + token::mint = token_mint_b, + token::authority = whirlpool)] + pub token_vault_b: Box>, + + #[account(has_one = whirlpools_config, constraint = fee_tier.tick_spacing == tick_spacing)] + pub fee_tier: Account<'info, FeeTier>, + + #[account(address = token_mint_a.to_account_info().owner.clone())] + pub token_program_a: Interface<'info, TokenInterface>, + #[account(address = token_mint_b.to_account_info().owner.clone())] + pub token_program_b: Interface<'info, TokenInterface>, + pub system_program: Program<'info, System>, + pub rent: Sysvar<'info, Rent>, +} + +pub fn handler( + ctx: Context, + tick_spacing: u16, + initial_sqrt_price: u128, +) -> Result<()> { + let token_mint_a = ctx.accounts.token_mint_a.key(); + let token_mint_b = ctx.accounts.token_mint_b.key(); + + let whirlpool = &mut ctx.accounts.whirlpool; + let whirlpools_config = &ctx.accounts.whirlpools_config; + + let default_fee_rate = ctx.accounts.fee_tier.default_fee_rate; + + // ignore the bump passed and use one Anchor derived + let bump = ctx.bumps.whirlpool; + + // Don't allow creating a pool with unsupported token mints + let is_token_badge_initialized_a = is_token_badge_initialized( + whirlpools_config.key(), + token_mint_a, + &ctx.accounts.token_badge_a + )?; + + if !is_supported_token_mint(&ctx.accounts.token_mint_a, is_token_badge_initialized_a).unwrap() { + return Err(ErrorCode::UnsupportedTokenMint.into()); + } + + let is_token_badge_initialized_b = is_token_badge_initialized( + whirlpools_config.key(), + token_mint_b, + &ctx.accounts.token_badge_b + )?; + + if !is_supported_token_mint(&ctx.accounts.token_mint_b, is_token_badge_initialized_b).unwrap() { + return Err(ErrorCode::UnsupportedTokenMint.into()); + } + + Ok(whirlpool.initialize( + whirlpools_config, + bump, + tick_spacing, + initial_sqrt_price, + default_fee_rate, + token_mint_a, + ctx.accounts.token_vault_a.key(), + token_mint_b, + ctx.accounts.token_vault_b.key(), + )?) +} diff --git a/programs/whirlpool/src/instructions/v2/initialize_reward.rs b/programs/whirlpool/src/instructions/v2/initialize_reward.rs new file mode 100644 index 000000000..264eb308d --- /dev/null +++ b/programs/whirlpool/src/instructions/v2/initialize_reward.rs @@ -0,0 +1,62 @@ +use anchor_lang::prelude::*; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; + +use crate::{ + errors::ErrorCode, + state::Whirlpool, + util::{is_token_badge_initialized, v2::is_supported_token_mint} +}; + +#[derive(Accounts)] +#[instruction(reward_index: u8)] +pub struct InitializeRewardV2<'info> { + #[account(address = whirlpool.reward_infos[reward_index as usize].authority)] + pub reward_authority: Signer<'info>, + + #[account(mut)] + pub funder: Signer<'info>, + + #[account(mut)] + pub whirlpool: Box>, + + pub reward_mint: Box>, + + #[account(seeds = [b"token_badge", whirlpool.whirlpools_config.as_ref(), reward_mint.key().as_ref()], bump)] + /// CHECK: checked in the handler + pub reward_token_badge: UncheckedAccount<'info>, + + #[account( + init, + payer = funder, + token::token_program = reward_token_program, + token::mint = reward_mint, + token::authority = whirlpool + )] + pub reward_vault: Box>, + + #[account(address = reward_mint.to_account_info().owner.clone())] + pub reward_token_program: Interface<'info, TokenInterface>, + pub system_program: Program<'info, System>, + pub rent: Sysvar<'info, Rent>, +} + +pub fn handler(ctx: Context, reward_index: u8) -> Result<()> { + let whirlpool = &mut ctx.accounts.whirlpool; + + // Don't allow initializing a reward with an unsupported token mint + let is_token_badge_initialized = is_token_badge_initialized( + whirlpool.whirlpools_config, + ctx.accounts.reward_mint.key(), + &ctx.accounts.reward_token_badge, + )?; + + if !is_supported_token_mint(&ctx.accounts.reward_mint, is_token_badge_initialized).unwrap() { + return Err(ErrorCode::UnsupportedTokenMint.into()); + } + + Ok(whirlpool.initialize_reward( + reward_index as usize, + ctx.accounts.reward_mint.key(), + ctx.accounts.reward_vault.key(), + )?) +} diff --git a/programs/whirlpool/src/instructions/v2/initialize_token_badge.rs b/programs/whirlpool/src/instructions/v2/initialize_token_badge.rs new file mode 100644 index 000000000..a9c541f0b --- /dev/null +++ b/programs/whirlpool/src/instructions/v2/initialize_token_badge.rs @@ -0,0 +1,44 @@ +use crate::state::*; +use anchor_lang::prelude::*; +use anchor_spl::token_interface::Mint; + +#[derive(Accounts)] +pub struct InitializeTokenBadge<'info> { + pub whirlpools_config: Box>, + + #[account(has_one = whirlpools_config)] + pub whirlpools_config_extension: Box>, + + #[account(address = whirlpools_config_extension.token_badge_authority)] + pub token_badge_authority: Signer<'info>, + + pub token_mint: InterfaceAccount<'info, Mint>, + + #[account(init, + payer = funder, + seeds = [ + b"token_badge", + whirlpools_config.key().as_ref(), + token_mint.key().as_ref(), + ], + bump, + space = TokenBadge::LEN)] + pub token_badge: Account<'info, TokenBadge>, + + #[account(mut)] + pub funder: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +pub fn handler( + ctx: Context, +) -> Result<()> { + Ok(ctx + .accounts + .token_badge + .initialize( + ctx.accounts.whirlpools_config.key(), + ctx.accounts.token_mint.key(), + )?) +} diff --git a/programs/whirlpool/src/instructions/v2/mod.rs b/programs/whirlpool/src/instructions/v2/mod.rs new file mode 100644 index 000000000..cab331a13 --- /dev/null +++ b/programs/whirlpool/src/instructions/v2/mod.rs @@ -0,0 +1,33 @@ +pub mod collect_fees; +pub mod collect_protocol_fees; +pub mod collect_reward; +pub mod decrease_liquidity; +pub mod increase_liquidity; +pub mod initialize_pool; +pub mod initialize_reward; +pub mod set_reward_emissions; +pub mod swap; +pub mod two_hop_swap; + +pub mod initialize_config_extension; +pub mod set_config_extension_authority; +pub mod set_token_badge_authority; +pub mod initialize_token_badge; +pub mod delete_token_badge; + +pub use collect_fees::*; +pub use collect_protocol_fees::*; +pub use collect_reward::*; +pub use decrease_liquidity::*; +pub use increase_liquidity::*; +pub use initialize_pool::*; +pub use initialize_reward::*; +pub use set_reward_emissions::*; +pub use swap::*; +pub use two_hop_swap::*; + +pub use initialize_config_extension::*; +pub use set_config_extension_authority::*; +pub use set_token_badge_authority::*; +pub use initialize_token_badge::*; +pub use delete_token_badge::*; diff --git a/programs/whirlpool/src/instructions/v2/set_config_extension_authority.rs b/programs/whirlpool/src/instructions/v2/set_config_extension_authority.rs new file mode 100644 index 000000000..90f26eda5 --- /dev/null +++ b/programs/whirlpool/src/instructions/v2/set_config_extension_authority.rs @@ -0,0 +1,25 @@ +use anchor_lang::prelude::*; + +use crate::state::{WhirlpoolsConfig, WhirlpoolsConfigExtension}; + +#[derive(Accounts)] +pub struct SetConfigExtensionAuthority<'info> { + pub whirlpools_config: Box>, + + #[account(mut, has_one = whirlpools_config)] + pub whirlpools_config_extension: Account<'info, WhirlpoolsConfigExtension>, + + #[account(address = whirlpools_config_extension.config_extension_authority)] + pub config_extension_authority: Signer<'info>, + + /// CHECK: safe, the account that will be new authority can be arbitrary + pub new_config_extension_authority: UncheckedAccount<'info>, +} + +/// Set the config extension authority. Only the current config extension authority has permission to invoke this instruction. +pub fn handler(ctx: Context) -> Result<()> { + Ok(ctx + .accounts + .whirlpools_config_extension + .update_config_extension_authority(ctx.accounts.new_config_extension_authority.key())) +} diff --git a/programs/whirlpool/src/instructions/v2/set_reward_emissions.rs b/programs/whirlpool/src/instructions/v2/set_reward_emissions.rs new file mode 100644 index 000000000..f1ff7301e --- /dev/null +++ b/programs/whirlpool/src/instructions/v2/set_reward_emissions.rs @@ -0,0 +1,48 @@ +use anchor_lang::prelude::*; +use anchor_spl::token_interface::TokenAccount; + +use crate::errors::ErrorCode; +use crate::manager::whirlpool_manager::next_whirlpool_reward_infos; +use crate::math::checked_mul_shift_right; +use crate::state::Whirlpool; +use crate::util::to_timestamp_u64; + +const DAY_IN_SECONDS: u128 = 60 * 60 * 24; + +#[derive(Accounts)] +#[instruction(reward_index: u8)] +pub struct SetRewardEmissionsV2<'info> { + #[account(mut)] + pub whirlpool: Account<'info, Whirlpool>, + + #[account(address = whirlpool.reward_infos[reward_index as usize].authority)] + pub reward_authority: Signer<'info>, + + #[account(address = whirlpool.reward_infos[reward_index as usize].vault)] + pub reward_vault: InterfaceAccount<'info, TokenAccount>, +} + +pub fn handler( + ctx: Context, + reward_index: u8, + emissions_per_second_x64: u128, +) -> Result<()> { + let whirlpool = &ctx.accounts.whirlpool; + let reward_vault = &ctx.accounts.reward_vault; + + let emissions_per_day = checked_mul_shift_right(DAY_IN_SECONDS, emissions_per_second_x64)?; + if reward_vault.amount < emissions_per_day { + return Err(ErrorCode::RewardVaultAmountInsufficient.into()); + } + + let clock = Clock::get()?; + let timestamp = to_timestamp_u64(clock.unix_timestamp)?; + let next_reward_infos = next_whirlpool_reward_infos(whirlpool, timestamp)?; + + Ok(ctx.accounts.whirlpool.update_emissions( + reward_index as usize, + next_reward_infos, + timestamp, + emissions_per_second_x64, + )?) +} diff --git a/programs/whirlpool/src/instructions/v2/set_token_badge_authority.rs b/programs/whirlpool/src/instructions/v2/set_token_badge_authority.rs new file mode 100644 index 000000000..198739989 --- /dev/null +++ b/programs/whirlpool/src/instructions/v2/set_token_badge_authority.rs @@ -0,0 +1,25 @@ +use anchor_lang::prelude::*; + +use crate::state::{WhirlpoolsConfig, WhirlpoolsConfigExtension}; + +#[derive(Accounts)] +pub struct SetTokenBadgeAuthority<'info> { + pub whirlpools_config: Box>, + + #[account(mut, has_one = whirlpools_config)] + pub whirlpools_config_extension: Account<'info, WhirlpoolsConfigExtension>, + + #[account(address = whirlpools_config_extension.config_extension_authority)] + pub config_extension_authority: Signer<'info>, + + /// CHECK: safe, the account that will be new authority can be arbitrary + pub new_token_badge_authority: UncheckedAccount<'info>, +} + +/// Set the token badge authority. Only the config extension authority has permission to invoke this instruction. +pub fn handler(ctx: Context) -> Result<()> { + Ok(ctx + .accounts + .whirlpools_config_extension + .update_token_badge_authority(ctx.accounts.new_token_badge_authority.key())) +} diff --git a/programs/whirlpool/src/instructions/v2/swap.rs b/programs/whirlpool/src/instructions/v2/swap.rs new file mode 100644 index 000000000..7aabc5597 --- /dev/null +++ b/programs/whirlpool/src/instructions/v2/swap.rs @@ -0,0 +1,266 @@ +use anchor_lang::prelude::*; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use anchor_spl::memo::Memo; + +use crate::util::{calculate_transfer_fee_excluded_amount, calculate_transfer_fee_included_amount, parse_remaining_accounts, AccountsType, RemainingAccountsInfo}; +use crate::{ + errors::ErrorCode, + manager::swap_manager::*, + state::{TickArray, Whirlpool}, + util::{to_timestamp_u64, v2::update_and_swap_whirlpool_v2, SwapTickSequence}, + constants::transfer_memo, +}; + +#[derive(Accounts)] +pub struct SwapV2<'info> { + #[account(address = token_mint_a.to_account_info().owner.clone())] + pub token_program_a: Interface<'info, TokenInterface>, + #[account(address = token_mint_b.to_account_info().owner.clone())] + pub token_program_b: Interface<'info, TokenInterface>, + + pub memo_program: Program<'info, Memo>, + + pub token_authority: Signer<'info>, + + #[account(mut)] + pub whirlpool: Box>, + + #[account(address = whirlpool.token_mint_a)] + pub token_mint_a: InterfaceAccount<'info, Mint>, + #[account(address = whirlpool.token_mint_b)] + pub token_mint_b: InterfaceAccount<'info, Mint>, + + #[account(mut, constraint = token_owner_account_a.mint == whirlpool.token_mint_a)] + pub token_owner_account_a: Box>, + #[account(mut, address = whirlpool.token_vault_a)] + pub token_vault_a: Box>, + + #[account(mut, constraint = token_owner_account_b.mint == whirlpool.token_mint_b)] + pub token_owner_account_b: Box>, + #[account(mut, address = whirlpool.token_vault_b)] + pub token_vault_b: Box>, + + #[account(mut, has_one = whirlpool)] + pub tick_array_0: AccountLoader<'info, TickArray>, + + #[account(mut, has_one = whirlpool)] + pub tick_array_1: AccountLoader<'info, TickArray>, + + #[account(mut, has_one = whirlpool)] + pub tick_array_2: AccountLoader<'info, TickArray>, + + #[account(mut, seeds = [b"oracle", whirlpool.key().as_ref()], bump)] + /// CHECK: Oracle is currently unused and will be enabled on subsequent updates + pub oracle: UncheckedAccount<'info>, + + // remaining accounts + // - accounts for transfer hook program of token_mint_a + // - accounts for transfer hook program of token_mint_b +} + +pub fn handler<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, SwapV2<'info>>, + amount: u64, + other_amount_threshold: u64, + sqrt_price_limit: u128, + amount_specified_is_input: bool, + a_to_b: bool, // Zero for one + remaining_accounts_info: Option, +) -> Result<()> { + let whirlpool = &mut ctx.accounts.whirlpool; + let clock = Clock::get()?; + // Update the global reward growth which increases as a function of time. + let timestamp = to_timestamp_u64(clock.unix_timestamp)?; + + // Process remaining accounts + let remaining_accounts = parse_remaining_accounts( + &ctx.remaining_accounts, + &remaining_accounts_info, + &[ + AccountsType::TransferHookA, + AccountsType::TransferHookB, + ], + )?; + + let mut swap_tick_sequence = SwapTickSequence::new( + ctx.accounts.tick_array_0.load_mut().unwrap(), + ctx.accounts.tick_array_1.load_mut().ok(), + ctx.accounts.tick_array_2.load_mut().ok(), + ); + + let swap_update = swap_with_transfer_fee_extension( + &whirlpool, + &ctx.accounts.token_mint_a, + &ctx.accounts.token_mint_b, + &mut swap_tick_sequence, + amount, + sqrt_price_limit, + amount_specified_is_input, + a_to_b, + timestamp, + )?; + + if amount_specified_is_input { + let transfer_fee_excluded_output_amount = if a_to_b { + calculate_transfer_fee_excluded_amount( + &ctx.accounts.token_mint_b, + swap_update.amount_b + )?.amount + } else { + calculate_transfer_fee_excluded_amount( + &ctx.accounts.token_mint_a, + swap_update.amount_a + )?.amount + }; + if transfer_fee_excluded_output_amount < other_amount_threshold { + return Err(ErrorCode::AmountOutBelowMinimum.into()); + } + } else { + let transfer_fee_included_input_amount = if a_to_b { + swap_update.amount_a + } else { + swap_update.amount_b + }; + if transfer_fee_included_input_amount > other_amount_threshold { + return Err(ErrorCode::AmountInAboveMaximum.into()); + } + } + + update_and_swap_whirlpool_v2( + whirlpool, + &ctx.accounts.token_authority, + &ctx.accounts.token_mint_a, + &ctx.accounts.token_mint_b, + &ctx.accounts.token_owner_account_a, + &ctx.accounts.token_owner_account_b, + &ctx.accounts.token_vault_a, + &ctx.accounts.token_vault_b, + &remaining_accounts.transfer_hook_a, + &remaining_accounts.transfer_hook_b, + &ctx.accounts.token_program_a, + &ctx.accounts.token_program_b, + &ctx.accounts.memo_program, + swap_update, + a_to_b, + timestamp, + transfer_memo::TRANSFER_MEMO_SWAP.as_bytes(), + ) +} + +pub fn swap_with_transfer_fee_extension<'info>( + whirlpool: &Whirlpool, + token_mint_a: &InterfaceAccount<'info, Mint>, + token_mint_b: &InterfaceAccount<'info, Mint>, + swap_tick_sequence: &mut SwapTickSequence, + amount: u64, + sqrt_price_limit: u128, + amount_specified_is_input: bool, + a_to_b: bool, + timestamp: u64, +) -> Result { + let (input_token_mint, output_token_mint) = if a_to_b { + (token_mint_a, token_mint_b) + } else { + (token_mint_b, token_mint_a) + }; + + // ExactIn + if amount_specified_is_input { + let transfer_fee_included_input = amount; + let transfer_fee_excluded_input = calculate_transfer_fee_excluded_amount( + input_token_mint, + transfer_fee_included_input + )?.amount; + + let swap_update = swap( + whirlpool, + swap_tick_sequence, + transfer_fee_excluded_input, + sqrt_price_limit, + amount_specified_is_input, + a_to_b, + timestamp, + )?; + + let (swap_update_amount_input, swap_update_amount_output) = if a_to_b { + (swap_update.amount_a, swap_update.amount_b) + } else { + (swap_update.amount_b, swap_update.amount_a) + }; + + let fullfilled = swap_update_amount_input == transfer_fee_excluded_input; + + let adjusted_transfer_fee_included_input = if fullfilled { + transfer_fee_included_input + } else { + calculate_transfer_fee_included_amount( + input_token_mint, + swap_update_amount_input + )?.amount + }; + + let transfer_fee_included_output = swap_update_amount_output; + + let (amount_a, amount_b) = if a_to_b { + (adjusted_transfer_fee_included_input, transfer_fee_included_output) + } else { + (transfer_fee_included_output, adjusted_transfer_fee_included_input) + }; + return Ok(PostSwapUpdate { + amount_a, // updated (transfer fee included) + amount_b, // updated (transfer fee included) + next_liquidity: swap_update.next_liquidity, + next_tick_index: swap_update.next_tick_index, + next_sqrt_price: swap_update.next_sqrt_price, + next_fee_growth_global: swap_update.next_fee_growth_global, + next_reward_infos: swap_update.next_reward_infos, + next_protocol_fee: swap_update.next_protocol_fee, + }); + } + + // ExactOut + let transfer_fee_excluded_output = amount; + let transfer_fee_included_output = calculate_transfer_fee_included_amount( + output_token_mint, + transfer_fee_excluded_output + )?.amount; + + let swap_update = swap( + whirlpool, + swap_tick_sequence, + transfer_fee_included_output, + sqrt_price_limit, + amount_specified_is_input, + a_to_b, + timestamp, + )?; + + let (swap_update_amount_input, swap_update_amount_output) = if a_to_b { + (swap_update.amount_a, swap_update.amount_b) + } else { + (swap_update.amount_b, swap_update.amount_a) + }; + + let transfer_fee_included_input = calculate_transfer_fee_included_amount( + input_token_mint, + swap_update_amount_input + )?.amount; + + let adjusted_transfer_fee_included_output = swap_update_amount_output; + + let (amount_a, amount_b) = if a_to_b { + (transfer_fee_included_input, adjusted_transfer_fee_included_output) + } else { + (adjusted_transfer_fee_included_output, transfer_fee_included_input) + }; + Ok(PostSwapUpdate { + amount_a, // updated (transfer fee included) + amount_b, // updated (transfer fee included) + next_liquidity: swap_update.next_liquidity, + next_tick_index: swap_update.next_tick_index, + next_sqrt_price: swap_update.next_sqrt_price, + next_fee_growth_global: swap_update.next_fee_growth_global, + next_reward_infos: swap_update.next_reward_infos, + next_protocol_fee: swap_update.next_protocol_fee, + }) +} diff --git a/programs/whirlpool/src/instructions/v2/two_hop_swap.rs b/programs/whirlpool/src/instructions/v2/two_hop_swap.rs new file mode 100644 index 000000000..f97916774 --- /dev/null +++ b/programs/whirlpool/src/instructions/v2/two_hop_swap.rs @@ -0,0 +1,342 @@ +use anchor_lang::prelude::*; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use anchor_spl::memo::Memo; + +use crate::swap_with_transfer_fee_extension; +use crate::util::{calculate_transfer_fee_excluded_amount, parse_remaining_accounts, update_and_two_hop_swap_whirlpool_v2, AccountsType, RemainingAccountsInfo}; +use crate::{ + errors::ErrorCode, + state::{TickArray, Whirlpool}, + util::{to_timestamp_u64, SwapTickSequence}, + constants::transfer_memo, +}; + +#[derive(Accounts)] +#[instruction( + amount: u64, + other_amount_threshold: u64, + amount_specified_is_input: bool, + a_to_b_one: bool, + a_to_b_two: bool, +)] +pub struct TwoHopSwapV2<'info> { + #[account(mut)] + pub whirlpool_one: Box>, + #[account(mut)] + pub whirlpool_two: Box>, + + #[account(address = whirlpool_one.input_token_mint(a_to_b_one))] + pub token_mint_input: InterfaceAccount<'info, Mint>, + #[account(address = whirlpool_one.output_token_mint(a_to_b_one))] + pub token_mint_intermediate: InterfaceAccount<'info, Mint>, + #[account(address = whirlpool_two.output_token_mint(a_to_b_two))] + pub token_mint_output: InterfaceAccount<'info, Mint>, + + #[account(address = token_mint_input.to_account_info().owner.clone())] + pub token_program_input: Interface<'info, TokenInterface>, + #[account(address = token_mint_intermediate.to_account_info().owner.clone())] + pub token_program_intermediate: Interface<'info, TokenInterface>, + #[account(address = token_mint_output.to_account_info().owner.clone())] + pub token_program_output: Interface<'info, TokenInterface>, + + #[account(mut, constraint = token_owner_account_input.mint == token_mint_input.key())] + pub token_owner_account_input: Box>, + #[account(mut, address = whirlpool_one.input_token_vault(a_to_b_one))] + pub token_vault_one_input: Box>, + #[account(mut, address = whirlpool_one.output_token_vault(a_to_b_one))] + pub token_vault_one_intermediate: Box>, + + #[account(mut, address = whirlpool_two.input_token_vault(a_to_b_two))] + pub token_vault_two_intermediate: Box>, + #[account(mut, address = whirlpool_two.output_token_vault(a_to_b_two))] + pub token_vault_two_output: Box>, + #[account(mut, constraint = token_owner_account_output.mint == token_mint_output.key())] + pub token_owner_account_output: Box>, + + pub token_authority: Signer<'info>, + + #[account(mut, constraint = tick_array_one_0.load()?.whirlpool == whirlpool_one.key())] + pub tick_array_one_0: AccountLoader<'info, TickArray>, + + #[account(mut, constraint = tick_array_one_1.load()?.whirlpool == whirlpool_one.key())] + pub tick_array_one_1: AccountLoader<'info, TickArray>, + + #[account(mut, constraint = tick_array_one_2.load()?.whirlpool == whirlpool_one.key())] + pub tick_array_one_2: AccountLoader<'info, TickArray>, + + #[account(mut, constraint = tick_array_two_0.load()?.whirlpool == whirlpool_two.key())] + pub tick_array_two_0: AccountLoader<'info, TickArray>, + + #[account(mut, constraint = tick_array_two_1.load()?.whirlpool == whirlpool_two.key())] + pub tick_array_two_1: AccountLoader<'info, TickArray>, + + #[account(mut, constraint = tick_array_two_2.load()?.whirlpool == whirlpool_two.key())] + pub tick_array_two_2: AccountLoader<'info, TickArray>, + + #[account(mut, seeds = [b"oracle", whirlpool_one.key().as_ref()], bump)] + /// CHECK: Oracle is currently unused and will be enabled on subsequent updates + pub oracle_one: UncheckedAccount<'info>, + + #[account(mut, seeds = [b"oracle", whirlpool_two.key().as_ref()], bump)] + /// CHECK: Oracle is currently unused and will be enabled on subsequent updates + pub oracle_two: UncheckedAccount<'info>, + + pub memo_program: Program<'info, Memo>, + + // remaining accounts + // - accounts for transfer hook program of token_mint_input + // - accounts for transfer hook program of token_mint_intermediate + // - accounts for transfer hook program of token_mint_output +} + +pub fn handler<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, TwoHopSwapV2<'info>>, + amount: u64, + other_amount_threshold: u64, + amount_specified_is_input: bool, + a_to_b_one: bool, + a_to_b_two: bool, + sqrt_price_limit_one: u128, + sqrt_price_limit_two: u128, + remaining_accounts_info: Option, +) -> Result<()> { + let clock = Clock::get()?; + // Update the global reward growth which increases as a function of time. + let timestamp = to_timestamp_u64(clock.unix_timestamp)?; + + let whirlpool_one = &mut ctx.accounts.whirlpool_one; + let whirlpool_two = &mut ctx.accounts.whirlpool_two; + + // Don't allow swaps on the same whirlpool + if whirlpool_one.key() == whirlpool_two.key() { + return Err(ErrorCode::DuplicateTwoHopPool.into()); + } + + let swap_one_output_mint = if a_to_b_one { + whirlpool_one.token_mint_b + } else { + whirlpool_one.token_mint_a + }; + + let swap_two_input_mint = if a_to_b_two { + whirlpool_two.token_mint_a + } else { + whirlpool_two.token_mint_b + }; + if swap_one_output_mint != swap_two_input_mint { + return Err(ErrorCode::InvalidIntermediaryMint.into()); + } + + // Process remaining accounts + let remaining_accounts = parse_remaining_accounts( + &ctx.remaining_accounts, + &remaining_accounts_info, + &[ + AccountsType::TransferHookInput, + AccountsType::TransferHookIntermediate, + AccountsType::TransferHookOutput, + ], + )?; + + let mut swap_tick_sequence_one = SwapTickSequence::new( + ctx.accounts.tick_array_one_0.load_mut().unwrap(), + ctx.accounts.tick_array_one_1.load_mut().ok(), + ctx.accounts.tick_array_one_2.load_mut().ok(), + ); + + let mut swap_tick_sequence_two = SwapTickSequence::new( + ctx.accounts.tick_array_two_0.load_mut().unwrap(), + ctx.accounts.tick_array_two_1.load_mut().ok(), + ctx.accounts.tick_array_two_2.load_mut().ok(), + ); + + // TODO: WLOG, we could extend this to N-swaps, but the account inputs to the instruction would + // need to be jankier and we may need to programatically map/verify rather than using anchor constraints + let (swap_update_one, swap_update_two) = if amount_specified_is_input { + // If the amount specified is input, this means we are doing exact-in + // and the swap calculations occur from Swap 1 => Swap 2 + // and the swaps occur from Swap 1 => Swap 2 + let swap_calc_one = swap_with_transfer_fee_extension( + &whirlpool_one, + if a_to_b_one { &ctx.accounts.token_mint_input } else { &ctx.accounts.token_mint_intermediate }, + if a_to_b_one { &ctx.accounts.token_mint_intermediate } else { &ctx.accounts.token_mint_input }, + &mut swap_tick_sequence_one, + amount, + sqrt_price_limit_one, + amount_specified_is_input, // true + a_to_b_one, + timestamp, + )?; + + // Swap two input is the output of swap one + // We use vault to vault transfer, so transfer fee will be collected once. + let swap_two_input_amount = if a_to_b_one { + swap_calc_one.amount_b + } else { + swap_calc_one.amount_a + }; + + let swap_calc_two = swap_with_transfer_fee_extension( + &whirlpool_two, + if a_to_b_two { &ctx.accounts.token_mint_intermediate } else { &ctx.accounts.token_mint_output }, + if a_to_b_two { &ctx.accounts.token_mint_output } else { &ctx.accounts.token_mint_intermediate }, + &mut swap_tick_sequence_two, + swap_two_input_amount, + sqrt_price_limit_two, + amount_specified_is_input, // true + a_to_b_two, + timestamp, + )?; + (swap_calc_one, swap_calc_two) + } else { + // If the amount specified is output, this means we need to invert the ordering of the calculations + // and the swap calculations occur from Swap 2 => Swap 1 + // but the actual swaps occur from Swap 1 => Swap 2 (to ensure that the intermediate token exists in the account) + let swap_calc_two = swap_with_transfer_fee_extension( + &whirlpool_two, + if a_to_b_two { &ctx.accounts.token_mint_intermediate } else { &ctx.accounts.token_mint_output }, + if a_to_b_two { &ctx.accounts.token_mint_output } else { &ctx.accounts.token_mint_intermediate }, + &mut swap_tick_sequence_two, + amount, + sqrt_price_limit_two, + amount_specified_is_input, // false + a_to_b_two, + timestamp, + )?; + + // The output of swap 1 is input of swap_calc_two + let swap_one_output_amount = if a_to_b_two { + calculate_transfer_fee_excluded_amount( + &ctx.accounts.token_mint_intermediate, + swap_calc_two.amount_a + )?.amount + } else { + calculate_transfer_fee_excluded_amount( + &ctx.accounts.token_mint_intermediate, + swap_calc_two.amount_b + )?.amount + }; + + let swap_calc_one = swap_with_transfer_fee_extension( + &whirlpool_one, + if a_to_b_one { &ctx.accounts.token_mint_input } else { &ctx.accounts.token_mint_intermediate }, + if a_to_b_one { &ctx.accounts.token_mint_intermediate } else { &ctx.accounts.token_mint_input }, + &mut swap_tick_sequence_one, + swap_one_output_amount, + sqrt_price_limit_one, + amount_specified_is_input, // false + a_to_b_one, + timestamp, + )?; + (swap_calc_one, swap_calc_two) + }; + + // All output token should be consumed by the second swap + let swap_calc_one_output = if a_to_b_one { swap_update_one.amount_b } else { swap_update_one.amount_a }; + let swap_calc_two_input = if a_to_b_two { swap_update_two.amount_a } else { swap_update_two.amount_b }; + if swap_calc_one_output != swap_calc_two_input { + return Err(ErrorCode::IntermediateTokenAmountMismatch.into()); + } + + if amount_specified_is_input { + // If amount_specified_is_input == true, then we have a variable amount of output + // The slippage we care about is the output of the second swap. + let output_amount = if a_to_b_two { + calculate_transfer_fee_excluded_amount( + &ctx.accounts.token_mint_output, + swap_update_two.amount_b + )?.amount + } else { + calculate_transfer_fee_excluded_amount( + &ctx.accounts.token_mint_output, + swap_update_two.amount_a + )?.amount + }; + + // If we have received less than the minimum out, throw an error + if output_amount < other_amount_threshold { + return Err(ErrorCode::AmountOutBelowMinimum.into()); + } + } else { + // amount_specified_is_output == false, then we have a variable amount of input + // The slippage we care about is the input of the first swap + let input_amount = if a_to_b_one { + swap_update_one.amount_a + } else { + swap_update_one.amount_b + }; + if input_amount > other_amount_threshold { + return Err(ErrorCode::AmountInAboveMaximum.into()); + } + } + + /* + update_and_swap_whirlpool_v2( + whirlpool_one, + &ctx.accounts.token_authority, + &ctx.accounts.token_mint_one_a, + &ctx.accounts.token_mint_one_b, + &ctx.accounts.token_owner_account_one_a, + &ctx.accounts.token_owner_account_one_b, + &ctx.accounts.token_vault_one_a, + &ctx.accounts.token_vault_one_b, + &remaining_accounts.transfer_hook_one_a, + &remaining_accounts.transfer_hook_one_b, + &ctx.accounts.token_program_one_a, + &ctx.accounts.token_program_one_b, + &ctx.accounts.memo_program, + swap_update_one, + a_to_b_one, + timestamp, + transfer_memo::TRANSFER_MEMO_SWAP.as_bytes(), + )?; + + update_and_swap_whirlpool_v2( + whirlpool_two, + &ctx.accounts.token_authority, + &ctx.accounts.token_mint_two_a, + &ctx.accounts.token_mint_two_b, + &ctx.accounts.token_owner_account_two_a, + &ctx.accounts.token_owner_account_two_b, + &ctx.accounts.token_vault_two_a, + &ctx.accounts.token_vault_two_b, + &remaining_accounts.transfer_hook_two_a, + &remaining_accounts.transfer_hook_two_b, + &ctx.accounts.token_program_two_a, + &ctx.accounts.token_program_two_b, + &ctx.accounts.memo_program, + swap_update_two, + a_to_b_two, + timestamp, + transfer_memo::TRANSFER_MEMO_SWAP.as_bytes(), + ) + */ + + update_and_two_hop_swap_whirlpool_v2( + swap_update_one, + swap_update_two, + whirlpool_one, + whirlpool_two, + a_to_b_one, + a_to_b_two, + &ctx.accounts.token_mint_input, + &ctx.accounts.token_mint_intermediate, + &ctx.accounts.token_mint_output, + &ctx.accounts.token_program_input, + &ctx.accounts.token_program_intermediate, + &ctx.accounts.token_program_output, + &ctx.accounts.token_owner_account_input, + &ctx.accounts.token_vault_one_input, + &ctx.accounts.token_vault_one_intermediate, + &ctx.accounts.token_vault_two_intermediate, + &ctx.accounts.token_vault_two_output, + &ctx.accounts.token_owner_account_output, + &remaining_accounts.transfer_hook_input, + &remaining_accounts.transfer_hook_intermediate, + &remaining_accounts.transfer_hook_output, + &ctx.accounts.token_authority, + &ctx.accounts.memo_program, + timestamp, + transfer_memo::TRANSFER_MEMO_SWAP.as_bytes(), + ) +} diff --git a/programs/whirlpool/src/lib.rs b/programs/whirlpool/src/lib.rs index 2c6864e82..17340a203 100644 --- a/programs/whirlpool/src/lib.rs +++ b/programs/whirlpool/src/lib.rs @@ -20,6 +20,7 @@ pub mod tests; pub mod util; use crate::state::{OpenPositionBumps, OpenPositionWithMetadataBumps, WhirlpoolBumps}; +use crate::util::RemainingAccountsInfo; use instructions::*; #[program] @@ -606,4 +607,284 @@ pub mod whirlpool { ) -> Result<()> { return instructions::close_bundled_position::handler(ctx, bundle_index); } + + //////////////////////////////////////////////////////////////////////////////// + // V2 instructions (TokenExtensions) + //////////////////////////////////////////////////////////////////////////////// + + // TODO: update comments + + /// Collect fees accrued for this position. + /// + /// ### Authority + /// - `position_authority` - authority that owns the token corresponding to this desired position. + pub fn collect_fees_v2<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, CollectFeesV2<'info>>, + remaining_accounts_info: Option, + ) -> Result<()> { + return instructions::v2::collect_fees::handler(ctx, remaining_accounts_info); + } + + /// Collect the protocol fees accrued in this Whirlpool + /// + /// ### Authority + /// - `collect_protocol_fees_authority` - assigned authority in the WhirlpoolConfig that can collect protocol fees + pub fn collect_protocol_fees_v2<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, CollectProtocolFeesV2<'info>>, + remaining_accounts_info: Option, + ) -> Result<()> { + return instructions::v2::collect_protocol_fees::handler(ctx, remaining_accounts_info); + } + + /// Collect rewards accrued for this position. + /// + /// ### Authority + /// - `position_authority` - authority that owns the token corresponding to this desired position. + pub fn collect_reward_v2<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, CollectRewardV2<'info>>, + reward_index: u8, + remaining_accounts_info: Option, + ) -> Result<()> { + return instructions::v2::collect_reward::handler(ctx, reward_index, remaining_accounts_info); + } + + /// Withdraw liquidity from a position in the Whirlpool. This call also updates the position's accrued fees and rewards. + /// + /// ### Authority + /// - `position_authority` - authority that owns the token corresponding to this desired position. + /// + /// ### Parameters + /// - `liquidity_amount` - The total amount of Liquidity the user desires to withdraw. + /// - `token_min_a` - The minimum amount of tokenA the user is willing to withdraw. + /// - `token_min_b` - The minimum amount of tokenB the user is willing to withdraw. + /// + /// #### Special Errors + /// - `LiquidityZero` - Provided liquidity amount is zero. + /// - `LiquidityTooHigh` - Provided liquidity exceeds u128::max. + /// - `TokenMinSubceeded` - The required token to perform this operation subceeds the user defined amount. + pub fn decrease_liquidity_v2<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, ModifyLiquidityV2<'info>>, + liquidity_amount: u128, + token_min_a: u64, + token_min_b: u64, + remaining_accounts_info: Option, + ) -> Result<()> { + return instructions::v2::decrease_liquidity::handler( + ctx, + liquidity_amount, + token_min_a, + token_min_b, + remaining_accounts_info, + ); + } + + /// Add liquidity to a position in the Whirlpool. This call also updates the position's accrued fees and rewards. + /// + /// ### Authority + /// - `position_authority` - authority that owns the token corresponding to this desired position. + /// + /// ### Parameters + /// - `liquidity_amount` - The total amount of Liquidity the user is willing to deposit. + /// - `token_max_a` - The maximum amount of tokenA the user is willing to deposit. + /// - `token_max_b` - The maximum amount of tokenB the user is willing to deposit. + /// + /// #### Special Errors + /// - `LiquidityZero` - Provided liquidity amount is zero. + /// - `LiquidityTooHigh` - Provided liquidity exceeds u128::max. + /// - `TokenMaxExceeded` - The required token to perform this operation exceeds the user defined amount. + pub fn increase_liquidity_v2<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, ModifyLiquidityV2<'info>>, + liquidity_amount: u128, + token_max_a: u64, + token_max_b: u64, + remaining_accounts_info: Option, + ) -> Result<()> { + return instructions::v2::increase_liquidity::handler( + ctx, + liquidity_amount, + token_max_a, + token_max_b, + remaining_accounts_info, + ); + } + + /// Initializes a Whirlpool account. + /// Fee rate is set to the default values on the config and supplied fee_tier. + /// + /// ### Parameters + /// - `bumps` - The bump value when deriving the PDA of the Whirlpool address. + /// - `tick_spacing` - The desired tick spacing for this pool. + /// - `initial_sqrt_price` - The desired initial sqrt-price for this pool + /// + /// #### Special Errors + /// `InvalidTokenMintOrder` - The order of mints have to be ordered by + /// `SqrtPriceOutOfBounds` - provided initial_sqrt_price is not between 2^-64 to 2^64 + /// + pub fn initialize_pool_v2( + ctx: Context, + tick_spacing: u16, + initial_sqrt_price: u128, + ) -> Result<()> { + return instructions::v2::initialize_pool::handler( + ctx, + tick_spacing, + initial_sqrt_price, + ); + } + + /// Initialize reward for a Whirlpool. A pool can only support up to a set number of rewards. + /// + /// ### Authority + /// - "reward_authority" - assigned authority by the reward_super_authority for the specified + /// reward-index in this Whirlpool + /// + /// ### Parameters + /// - `reward_index` - The reward index that we'd like to initialize. (0 <= index <= NUM_REWARDS) + /// + /// #### Special Errors + /// - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized + /// index in this pool, or exceeds NUM_REWARDS, or + /// all reward slots for this pool has been initialized. + pub fn initialize_reward_v2(ctx: Context, reward_index: u8) -> Result<()> { + return instructions::v2::initialize_reward::handler(ctx, reward_index); + } + + /// Set the reward emissions for a reward in a Whirlpool. + /// + /// ### Authority + /// - "reward_authority" - assigned authority by the reward_super_authority for the specified + /// reward-index in this Whirlpool + /// + /// ### Parameters + /// - `reward_index` - The reward index (0 <= index <= NUM_REWARDS) that we'd like to modify. + /// - `emissions_per_second_x64` - The amount of rewards emitted in this pool. + /// + /// #### Special Errors + /// - `RewardVaultAmountInsufficient` - The amount of rewards in the reward vault cannot emit + /// more than a day of desired emissions. + /// - `InvalidTimestamp` - Provided timestamp is not in order with the previous timestamp. + /// - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized + /// index in this pool, or exceeds NUM_REWARDS, or + /// all reward slots for this pool has been initialized. + pub fn set_reward_emissions_v2( + ctx: Context, + reward_index: u8, + emissions_per_second_x64: u128, + ) -> Result<()> { + return instructions::v2::set_reward_emissions::handler( + ctx, + reward_index, + emissions_per_second_x64, + ); + } + + /// Perform a swap in this Whirlpool + /// + /// ### Authority + /// - "token_authority" - The authority to withdraw tokens from the input token account. + /// + /// ### Parameters + /// - `amount` - The amount of input or output token to swap from (depending on amount_specified_is_input). + /// - `other_amount_threshold` - The maximum/minimum of input/output token to swap into (depending on amount_specified_is_input). + /// - `sqrt_price_limit` - The maximum/minimum price the swap will swap to. + /// - `amount_specified_is_input` - Specifies the token the parameter `amount`represents. If true, the amount represents the input token of the swap. + /// - `a_to_b` - The direction of the swap. True if swapping from A to B. False if swapping from B to A. + /// + /// #### Special Errors + /// - `ZeroTradableAmount` - User provided parameter `amount` is 0. + /// - `InvalidSqrtPriceLimitDirection` - User provided parameter `sqrt_price_limit` does not match the direction of the trade. + /// - `SqrtPriceOutOfBounds` - User provided parameter `sqrt_price_limit` is over Whirlppool's max/min bounds for sqrt-price. + /// - `InvalidTickArraySequence` - User provided tick-arrays are not in sequential order required to proceed in this trade direction. + /// - `TickArraySequenceInvalidIndex` - The swap loop attempted to access an invalid array index during the query of the next initialized tick. + /// - `TickArrayIndexOutofBounds` - The swap loop attempted to access an invalid array index during tick crossing. + /// - `LiquidityOverflow` - Liquidity value overflowed 128bits during tick crossing. + /// - `InvalidTickSpacing` - The swap pool was initialized with tick-spacing of 0. + pub fn swap_v2<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, SwapV2<'info>>, + amount: u64, + other_amount_threshold: u64, + sqrt_price_limit: u128, + amount_specified_is_input: bool, + a_to_b: bool, + remaining_accounts_info: Option, + ) -> Result<()> { + return instructions::v2::swap::handler( + ctx, + amount, + other_amount_threshold, + sqrt_price_limit, + amount_specified_is_input, + a_to_b, + remaining_accounts_info, + ); + } + + /// Perform a two-hop swap in this Whirlpool + /// + /// ### Authority + /// - "token_authority" - The authority to withdraw tokens from the input token account. + /// + /// ### Parameters + /// - `amount` - The amount of input or output token to swap from (depending on amount_specified_is_input). + /// - `other_amount_threshold` - The maximum/minimum of input/output token to swap into (depending on amount_specified_is_input). + /// - `amount_specified_is_input` - Specifies the token the parameter `amount`represents. If true, the amount represents the input token of the swap. + /// - `a_to_b_one` - The direction of the swap of hop one. True if swapping from A to B. False if swapping from B to A. + /// - `a_to_b_two` - The direction of the swap of hop two. True if swapping from A to B. False if swapping from B to A. + /// - `sqrt_price_limit_one` - The maximum/minimum price the swap will swap to in the first hop. + /// - `sqrt_price_limit_two` - The maximum/minimum price the swap will swap to in the second hop. + /// + /// #### Special Errors + /// - `ZeroTradableAmount` - User provided parameter `amount` is 0. + /// - `InvalidSqrtPriceLimitDirection` - User provided parameter `sqrt_price_limit` does not match the direction of the trade. + /// - `SqrtPriceOutOfBounds` - User provided parameter `sqrt_price_limit` is over Whirlppool's max/min bounds for sqrt-price. + /// - `InvalidTickArraySequence` - User provided tick-arrays are not in sequential order required to proceed in this trade direction. + /// - `TickArraySequenceInvalidIndex` - The swap loop attempted to access an invalid array index during the query of the next initialized tick. + /// - `TickArrayIndexOutofBounds` - The swap loop attempted to access an invalid array index during tick crossing. + /// - `LiquidityOverflow` - Liquidity value overflowed 128bits during tick crossing. + /// - `InvalidTickSpacing` - The swap pool was initialized with tick-spacing of 0. + /// - `InvalidIntermediaryMint` - Error if the intermediary mint between hop one and two do not equal. + /// - `DuplicateTwoHopPool` - Error if whirlpool one & two are the same pool. + pub fn two_hop_swap_v2<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, TwoHopSwapV2<'info>>, + amount: u64, + other_amount_threshold: u64, + amount_specified_is_input: bool, + a_to_b_one: bool, + a_to_b_two: bool, + sqrt_price_limit_one: u128, + sqrt_price_limit_two: u128, + remaining_accounts_info: Option, + ) -> Result<()> { + return instructions::v2::two_hop_swap::handler( + ctx, + amount, + other_amount_threshold, + amount_specified_is_input, + a_to_b_one, + a_to_b_two, + sqrt_price_limit_one, + sqrt_price_limit_two, + remaining_accounts_info, + ); + } + + pub fn initialize_config_extension(ctx: Context) -> Result<()> { + return instructions::v2::initialize_config_extension::handler(ctx); + } + + pub fn set_config_extension_authority(ctx: Context) -> Result<()> { + return instructions::v2::set_config_extension_authority::handler(ctx); + } + + pub fn set_token_badge_authority(ctx: Context) -> Result<()> { + return instructions::v2::set_token_badge_authority::handler(ctx); + } + + pub fn initialize_token_badge(ctx: Context) -> Result<()> { + return instructions::v2::initialize_token_badge::handler(ctx); + } + + pub fn delete_token_badge(ctx: Context) -> Result<()> { + return instructions::v2::delete_token_badge::handler(ctx); + } } diff --git a/programs/whirlpool/src/manager/tick_manager.rs b/programs/whirlpool/src/manager/tick_manager.rs index a8f27aba3..63516d1fe 100644 --- a/programs/whirlpool/src/manager/tick_manager.rs +++ b/programs/whirlpool/src/manager/tick_manager.rs @@ -149,7 +149,7 @@ pub fn next_reward_growths_inside( tick_upper: &Tick, tick_upper_index: i32, reward_infos: &[WhirlpoolRewardInfo; NUM_REWARDS], -) -> ([u128; NUM_REWARDS]) { +) -> [u128; NUM_REWARDS] { let mut reward_growths_inside = [0; NUM_REWARDS]; for i in 0..NUM_REWARDS { diff --git a/programs/whirlpool/src/math/bn.rs b/programs/whirlpool/src/math/bn.rs index 612b4b999..2c9107602 100644 --- a/programs/whirlpool/src/math/bn.rs +++ b/programs/whirlpool/src/math/bn.rs @@ -19,7 +19,7 @@ /// U256 reference: /// https://crates.parity.io/sp_core/struct.U256.html /// -use borsh::{BorshDeserialize, BorshSerialize}; +use borsh09::{BorshDeserialize, BorshSerialize}; use std::borrow::BorrowMut; use std::convert::TryInto; use std::io::{Error, ErrorKind, Write}; diff --git a/programs/whirlpool/src/state/config_extension.rs b/programs/whirlpool/src/state/config_extension.rs new file mode 100644 index 000000000..b35a2e520 --- /dev/null +++ b/programs/whirlpool/src/state/config_extension.rs @@ -0,0 +1,108 @@ +use anchor_lang::prelude::*; + +#[account] +pub struct WhirlpoolsConfigExtension { + pub whirlpools_config: Pubkey, // 32 + pub config_extension_authority: Pubkey, // 32 + pub token_badge_authority: Pubkey, // 32 + // 512 RESERVE +} + +impl WhirlpoolsConfigExtension { + pub const LEN: usize = 8 + 32 + 32 + 32 + 512; + + pub fn initialize( + &mut self, + whirlpools_config: Pubkey, + default_authority: Pubkey, + ) -> Result<()> { + self.whirlpools_config = whirlpools_config; + self.config_extension_authority = default_authority; + self.token_badge_authority = default_authority; + Ok(()) + } + + pub fn update_config_extension_authority( + &mut self, + config_extension_authority: Pubkey, + ) { + self.config_extension_authority = config_extension_authority; + } + + pub fn update_token_badge_authority( + &mut self, + token_badge_authority: Pubkey, + ) { + self.token_badge_authority = token_badge_authority; + } +} + +#[cfg(test)] +mod whirlpools_config_extension_initialize_tests { + use super::*; + use std::str::FromStr; + + #[test] + fn test_initialize() { + let mut config_extension = WhirlpoolsConfigExtension { + whirlpools_config: Pubkey::default(), + config_extension_authority: Pubkey::default(), + token_badge_authority: Pubkey::default(), + }; + + let whirlpools_config = + Pubkey::from_str("2LecshUwdy9xi7meFgHtFJQNSKk4KdTrcpvaB56dP2NQ").unwrap(); + let default_authority = + Pubkey::from_str("orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE").unwrap(); + + let result = config_extension.initialize( + whirlpools_config, + default_authority, + ); + assert!(result.is_ok()); + + assert_eq!(whirlpools_config, config_extension.whirlpools_config); + assert_eq!(default_authority, config_extension.config_extension_authority); + assert_eq!(default_authority, config_extension.token_badge_authority); + } +} + +#[cfg(test)] +mod whirlpools_config_extension_update_tests { + use super::*; + use std::str::FromStr; + + #[test] + fn test_update_config_extension_authority() { + let mut config_extension = WhirlpoolsConfigExtension { + whirlpools_config: Pubkey::default(), + config_extension_authority: Pubkey::default(), + token_badge_authority: Pubkey::default(), + }; + + let config_extension_authority = + Pubkey::from_str("orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE").unwrap(); + + config_extension.update_config_extension_authority(config_extension_authority); + + assert_eq!(config_extension_authority, config_extension.config_extension_authority); + assert_eq!(Pubkey::default(), config_extension.token_badge_authority); + } + + #[test] + fn test_update_token_badge_authority() { + let mut config_extension = WhirlpoolsConfigExtension { + whirlpools_config: Pubkey::default(), + config_extension_authority: Pubkey::default(), + token_badge_authority: Pubkey::default(), + }; + + let token_badge_authority = + Pubkey::from_str("orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE").unwrap(); + + config_extension.update_token_badge_authority(token_badge_authority); + + assert_eq!(token_badge_authority, config_extension.token_badge_authority); + assert_eq!(Pubkey::default(), config_extension.config_extension_authority); + } +} diff --git a/programs/whirlpool/src/state/mod.rs b/programs/whirlpool/src/state/mod.rs index fa24e3f2f..f736325d3 100644 --- a/programs/whirlpool/src/state/mod.rs +++ b/programs/whirlpool/src/state/mod.rs @@ -4,6 +4,8 @@ pub mod position; pub mod position_bundle; pub mod tick; pub mod whirlpool; +pub mod config_extension; +pub mod token_badge; pub use self::whirlpool::*; pub use config::*; @@ -11,3 +13,5 @@ pub use fee_tier::*; pub use position::*; pub use position_bundle::*; pub use tick::*; +pub use config_extension::*; +pub use token_badge::*; diff --git a/programs/whirlpool/src/state/tick.rs b/programs/whirlpool/src/state/tick.rs index a59b420a2..cc336740c 100644 --- a/programs/whirlpool/src/state/tick.rs +++ b/programs/whirlpool/src/state/tick.rs @@ -13,7 +13,7 @@ pub const MIN_TICK_INDEX: i32 = -443636; pub const TICK_ARRAY_SIZE: i32 = 88; pub const TICK_ARRAY_SIZE_USIZE: usize = 88; -#[zero_copy] +#[zero_copy(unsafe)] #[repr(packed)] #[derive(Default, Debug, PartialEq)] pub struct Tick { @@ -138,7 +138,7 @@ impl TickUpdate { } } -#[account(zero_copy)] +#[account(zero_copy(unsafe))] #[repr(packed)] pub struct TickArray { pub start_tick_index: i32, diff --git a/programs/whirlpool/src/state/token_badge.rs b/programs/whirlpool/src/state/token_badge.rs new file mode 100644 index 000000000..c915e1b9b --- /dev/null +++ b/programs/whirlpool/src/state/token_badge.rs @@ -0,0 +1,58 @@ +use anchor_lang::prelude::*; + +#[account] +#[derive(Default)] +pub struct TokenBadge { + pub whirlpools_config: Pubkey, // 32 + pub token_mint: Pubkey, // 32 + // 128 RESERVE +} + +impl TokenBadge { + pub const LEN: usize = 8 + 32 + 32 + 128; + + pub fn initialize( + &mut self, + whirlpools_config: Pubkey, + token_mint: Pubkey, + ) -> Result<()> { + self.whirlpools_config = whirlpools_config; + self.token_mint = token_mint; + Ok(()) + } +} + +#[cfg(test)] +mod token_badge_initialize_tests { + use super::*; + use std::str::FromStr; + + #[test] + fn test_default() { + let token_badge = TokenBadge { + ..Default::default() + }; + assert_eq!(token_badge.whirlpools_config, Pubkey::default()); + assert_eq!(token_badge.token_mint, Pubkey::default()); + } + + #[test] + fn test_initialize() { + let mut token_badge = TokenBadge { + ..Default::default() + }; + let whirlpools_config = + Pubkey::from_str("2LecshUwdy9xi7meFgHtFJQNSKk4KdTrcpvaB56dP2NQ").unwrap(); + let token_mint = + Pubkey::from_str("orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE").unwrap(); + + let result = token_badge.initialize( + whirlpools_config, + token_mint, + ); + assert!(result.is_ok()); + + assert_eq!(whirlpools_config, token_badge.whirlpools_config); + assert_eq!(token_mint, token_badge.token_mint); + } +} diff --git a/programs/whirlpool/src/state/whirlpool.rs b/programs/whirlpool/src/state/whirlpool.rs index ad1ff887d..49988e048 100644 --- a/programs/whirlpool/src/state/whirlpool.rs +++ b/programs/whirlpool/src/state/whirlpool.rs @@ -69,6 +69,38 @@ impl Whirlpool { ] } + pub fn input_token_mint(&self, a_to_b: bool) -> Pubkey { + if a_to_b { + self.token_mint_a + } else { + self.token_mint_b + } + } + + pub fn input_token_vault(&self, a_to_b: bool) -> Pubkey { + if a_to_b { + self.token_vault_a + } else { + self.token_vault_b + } + } + + pub fn output_token_mint(&self, a_to_b: bool) -> Pubkey { + if a_to_b { + self.token_mint_b + } else { + self.token_mint_a + } + } + + pub fn output_token_vault(&self, a_to_b: bool) -> Pubkey { + if a_to_b { + self.token_vault_b + } else { + self.token_vault_a + } + } + pub fn initialize( &mut self, whirlpools_config: &Account, diff --git a/programs/whirlpool/src/util/mod.rs b/programs/whirlpool/src/util/mod.rs index 68281040b..80071548e 100644 --- a/programs/whirlpool/src/util/mod.rs +++ b/programs/whirlpool/src/util/mod.rs @@ -2,11 +2,13 @@ pub mod swap_tick_sequence; pub mod swap_utils; pub mod token; pub mod util; +pub mod v2; pub use swap_tick_sequence::*; pub use swap_utils::*; pub use token::*; pub use util::*; +pub use v2::*; #[cfg(test)] pub mod test_utils; diff --git a/programs/whirlpool/src/util/token.rs b/programs/whirlpool/src/util/token.rs index 24c8a8d99..4ae33750b 100644 --- a/programs/whirlpool/src/util/token.rs +++ b/programs/whirlpool/src/util/token.rs @@ -1,7 +1,7 @@ use crate::state::{PositionBundle, Whirlpool}; use anchor_lang::prelude::*; use anchor_spl::token::{self, Mint, Token, TokenAccount, Transfer}; -use mpl_token_metadata::instruction::create_metadata_accounts_v3; +use anchor_spl::metadata::{self, CreateMetadataAccountsV3, mpl_token_metadata::types::DataV2}; use solana_program::program::invoke_signed; use spl_token::instruction::{burn_checked, close_account, mint_to, set_authority, AuthorityType}; @@ -120,7 +120,7 @@ pub fn mint_position_token_with_metadata_and_remove_authority<'info>( position_metadata_account: &UncheckedAccount<'info>, metadata_update_auth: &UncheckedAccount<'info>, funder: &Signer<'info>, - metadata_program: &UncheckedAccount<'info>, + metadata_program: &Program<'info, metadata::Metadata>, token_program: &Program<'info, Token>, system_program: &Program<'info, System>, rent: &Sysvar<'info, Rent>, @@ -133,36 +133,32 @@ pub fn mint_position_token_with_metadata_and_remove_authority<'info>( )?; let metadata_mint_auth_account = whirlpool; - invoke_signed( - &create_metadata_accounts_v3( - metadata_program.key(), - position_metadata_account.key(), - position_mint.key(), - metadata_mint_auth_account.key(), - funder.key(), - metadata_update_auth.key(), - WP_METADATA_NAME.to_string(), - WP_METADATA_SYMBOL.to_string(), - WP_METADATA_URI.to_string(), - None, - 0, - false, - true, - None, - None, - None, - ), - &[ - position_metadata_account.to_account_info(), - position_mint.to_account_info(), - metadata_mint_auth_account.to_account_info(), - metadata_update_auth.to_account_info(), - funder.to_account_info(), + metadata::create_metadata_accounts_v3( + CpiContext::new_with_signer( metadata_program.to_account_info(), - system_program.to_account_info(), - rent.to_account_info(), - ], - &[&metadata_mint_auth_account.seeds()], + CreateMetadataAccountsV3 { + metadata: position_metadata_account.to_account_info(), + mint: position_mint.to_account_info(), + mint_authority: metadata_mint_auth_account.to_account_info(), + update_authority: metadata_update_auth.to_account_info(), + payer: funder.to_account_info(), + rent: rent.to_account_info(), + system_program: system_program.to_account_info(), + }, + &[&metadata_mint_auth_account.seeds()], + ), + DataV2 { + name: WP_METADATA_NAME.to_string(), + symbol: WP_METADATA_SYMBOL.to_string(), + uri: WP_METADATA_URI.to_string(), + creators: None, + seller_fee_basis_points: 0, + collection: None, + uses: None + }, + true, + false, + None )?; remove_position_token_mint_authority(whirlpool, position_mint, token_program) @@ -247,7 +243,7 @@ pub fn mint_position_bundle_token_with_metadata_and_remove_authority<'info>( position_bundle_token_account: &Account<'info, TokenAccount>, position_bundle_metadata: &UncheckedAccount<'info>, metadata_update_auth: &UncheckedAccount<'info>, - metadata_program: &UncheckedAccount<'info>, + metadata_program: &Program<'info, metadata::Metadata>, token_program: &Program<'info, Token>, system_program: &Program<'info, System>, rent: &Sysvar<'info, Rent>, @@ -271,36 +267,32 @@ pub fn mint_position_bundle_token_with_metadata_and_remove_authority<'info>( nft_name += "..."; nft_name += &mint_address[mint_address.len() - 4..]; - invoke_signed( - &create_metadata_accounts_v3( - metadata_program.key(), - position_bundle_metadata.key(), - position_bundle_mint.key(), - position_bundle.key(), - funder.key(), - metadata_update_auth.key(), - nft_name, - WPB_METADATA_SYMBOL.to_string(), - WPB_METADATA_URI.to_string(), - None, - 0, - false, - true, - None, - None, - None, - ), - &[ - position_bundle.to_account_info(), - position_bundle_metadata.to_account_info(), - position_bundle_mint.to_account_info(), - metadata_update_auth.to_account_info(), - funder.to_account_info(), + metadata::create_metadata_accounts_v3( + CpiContext::new_with_signer( metadata_program.to_account_info(), - system_program.to_account_info(), - rent.to_account_info(), - ], - &[position_bundle_seeds], + CreateMetadataAccountsV3 { + metadata: position_bundle_metadata.to_account_info(), + mint: position_bundle_mint.to_account_info(), + mint_authority: position_bundle.to_account_info(), + update_authority: metadata_update_auth.to_account_info(), + payer: funder.to_account_info(), + rent: rent.to_account_info(), + system_program: system_program.to_account_info(), + }, + &[position_bundle_seeds], + ), + DataV2 { + name: nft_name, + symbol: WPB_METADATA_SYMBOL.to_string(), + uri: WPB_METADATA_URI.to_string(), + creators: None, + seller_fee_basis_points: 0, + collection: None, + uses: None + }, + true, + false, + None )?; remove_position_bundle_token_mint_authority( diff --git a/programs/whirlpool/src/util/v2/mod.rs b/programs/whirlpool/src/util/v2/mod.rs new file mode 100644 index 000000000..4cc3c9d39 --- /dev/null +++ b/programs/whirlpool/src/util/v2/mod.rs @@ -0,0 +1,7 @@ +pub mod token; +pub mod swap_utils; +pub mod remaining_accounts_utils; + +pub use token::*; +pub use swap_utils::*; +pub use remaining_accounts_utils::*; diff --git a/programs/whirlpool/src/util/v2/remaining_accounts_utils.rs b/programs/whirlpool/src/util/v2/remaining_accounts_utils.rs new file mode 100644 index 000000000..72f670ee9 --- /dev/null +++ b/programs/whirlpool/src/util/v2/remaining_accounts_utils.rs @@ -0,0 +1,124 @@ +use anchor_lang::prelude::*; +use crate::errors::ErrorCode; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)] +pub enum AccountsType { + TransferHookA, + TransferHookB, + TransferHookReward, + TransferHookInput, + TransferHookIntermediate, + TransferHookOutput, + //TickArray, + //TickArrayOne, + //TickArrayTwo, +} + +#[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<'info> { + pub transfer_hook_a: Option>>, + pub transfer_hook_b: Option>>, + pub transfer_hook_reward: Option>>, + pub transfer_hook_input: Option>>, + pub transfer_hook_intermediate: Option>>, + pub transfer_hook_output: Option>>, + //pub tick_array: Option>>, + //pub tick_array_one: Option>>, + //pub tick_array_two: Option>>, +} + +pub fn parse_remaining_accounts<'info>( + remaining_accounts: &[AccountInfo<'info>], + remaining_accounts_info: &Option, + valid_accounts_type_list: &[AccountsType], +) -> Result> { + let mut remaining_accounts_iter = remaining_accounts.iter(); + let mut parsed_remaining_accounts = ParsedRemainingAccounts::default(); + + if remaining_accounts_info.is_none() { + return Ok(parsed_remaining_accounts); + } + + let remaining_accounts_info = remaining_accounts_info.as_ref().unwrap(); + + for slice in remaining_accounts_info.slices.iter() { + if !valid_accounts_type_list.contains(&slice.accounts_type) { + return Err(ErrorCode::RemainingAccountsInvalidSlice.into()); + } + if slice.length == 0 { + continue; + } + + let mut accounts: Vec> = Vec::with_capacity(slice.length as usize); + for _ in 0..slice.length { + if let Some(account) = remaining_accounts_iter.next() { + accounts.push(account.clone()); + } else { + return Err(ErrorCode::RemainingAccountsInsufficient.into()); + } + } + + match slice.accounts_type { + AccountsType::TransferHookA => { + if parsed_remaining_accounts.transfer_hook_a.is_some() { + return Err(ErrorCode::RemainingAccountsDuplicatedAccountsType.into()); + } + parsed_remaining_accounts.transfer_hook_a = Some(accounts); + } + AccountsType::TransferHookB => { + if parsed_remaining_accounts.transfer_hook_b.is_some() { + return Err(ErrorCode::RemainingAccountsDuplicatedAccountsType.into()); + } + parsed_remaining_accounts.transfer_hook_b = Some(accounts); + } + AccountsType::TransferHookReward => { + if parsed_remaining_accounts.transfer_hook_reward.is_some() { + return Err(ErrorCode::RemainingAccountsDuplicatedAccountsType.into()); + } + parsed_remaining_accounts.transfer_hook_reward = Some(accounts); + } + AccountsType::TransferHookInput => { + if parsed_remaining_accounts.transfer_hook_input.is_some() { + return Err(ErrorCode::RemainingAccountsDuplicatedAccountsType.into()); + } + parsed_remaining_accounts.transfer_hook_input = Some(accounts); + } + AccountsType::TransferHookIntermediate => { + if parsed_remaining_accounts.transfer_hook_intermediate.is_some() { + return Err(ErrorCode::RemainingAccountsDuplicatedAccountsType.into()); + } + parsed_remaining_accounts.transfer_hook_intermediate = Some(accounts); + } + AccountsType::TransferHookOutput => { + if parsed_remaining_accounts.transfer_hook_output.is_some() { + return Err(ErrorCode::RemainingAccountsDuplicatedAccountsType.into()); + } + parsed_remaining_accounts.transfer_hook_output = Some(accounts); + } + /* + AccountsType::TickArray => { + parsed_remaining_accounts.tick_array = Some(accounts); + } + AccountsType::TickArrayOne => { + parsed_remaining_accounts.tick_array_one = Some(accounts); + } + AccountsType::TickArrayTwo => { + parsed_remaining_accounts.tick_array_two = Some(accounts); + } + */ + } + } + + Ok(parsed_remaining_accounts) +} diff --git a/programs/whirlpool/src/util/v2/swap_utils.rs b/programs/whirlpool/src/util/v2/swap_utils.rs new file mode 100644 index 000000000..802bc3f7d --- /dev/null +++ b/programs/whirlpool/src/util/v2/swap_utils.rs @@ -0,0 +1,253 @@ +use anchor_lang::prelude::*; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use anchor_spl::memo::Memo; + +use crate::{manager::swap_manager::PostSwapUpdate, state::Whirlpool}; + +use super::{transfer_from_owner_to_vault_v2, transfer_from_vault_to_owner_v2}; + +pub fn update_and_swap_whirlpool_v2<'info>( + whirlpool: &mut Account<'info, Whirlpool>, + token_authority: &Signer<'info>, + token_mint_a: &InterfaceAccount<'info, Mint>, + token_mint_b: &InterfaceAccount<'info, Mint>, + token_owner_account_a: &InterfaceAccount<'info, TokenAccount>, + token_owner_account_b: &InterfaceAccount<'info, TokenAccount>, + token_vault_a: &InterfaceAccount<'info, TokenAccount>, + token_vault_b: &InterfaceAccount<'info, TokenAccount>, + transfer_hook_accounts_a: &Option>>, + transfer_hook_accounts_b: &Option>>, + token_program_a: &Interface<'info, TokenInterface>, + token_program_b: &Interface<'info, TokenInterface>, + memo_program: &Program<'info, Memo>, + swap_update: PostSwapUpdate, + is_token_fee_in_a: bool, + reward_last_updated_timestamp: u64, + memo: &[u8], +) -> Result<()> { + whirlpool.update_after_swap( + swap_update.next_liquidity, + swap_update.next_tick_index, + swap_update.next_sqrt_price, + swap_update.next_fee_growth_global, + swap_update.next_reward_infos, + swap_update.next_protocol_fee, + is_token_fee_in_a, + reward_last_updated_timestamp, + ); + + perform_swap_v2( + whirlpool, + token_authority, + token_mint_a, + token_mint_b, + token_owner_account_a, + token_owner_account_b, + token_vault_a, + token_vault_b, + transfer_hook_accounts_a, + transfer_hook_accounts_b, + token_program_a, + token_program_b, + memo_program, + swap_update.amount_a, + swap_update.amount_b, + is_token_fee_in_a, + memo, + ) +} + +fn perform_swap_v2<'info>( + whirlpool: &Account<'info, Whirlpool>, + token_authority: &Signer<'info>, + token_mint_a: &InterfaceAccount<'info, Mint>, + token_mint_b: &InterfaceAccount<'info, Mint>, + token_owner_account_a: &InterfaceAccount<'info, TokenAccount>, + token_owner_account_b: &InterfaceAccount<'info, TokenAccount>, + token_vault_a: &InterfaceAccount<'info, TokenAccount>, + token_vault_b: &InterfaceAccount<'info, TokenAccount>, + transfer_hook_accounts_a: &Option>>, + transfer_hook_accounts_b: &Option>>, + token_program_a: &Interface<'info, TokenInterface>, + token_program_b: &Interface<'info, TokenInterface>, + memo_program: &Program<'info, Memo>, + amount_a: u64, + amount_b: u64, + a_to_b: bool, + memo: &[u8], +) -> Result<()> { + // Transfer from user to pool + let deposit_token_program; + let deposit_mint; + let deposit_account_user; + let deposit_account_pool; + let deposit_transfer_hook_accounts; + let deposit_amount; + + // Transfer from pool to user + let withdrawal_token_program; + let withdrawal_mint; + let withdrawal_account_user; + let withdrawal_account_pool; + let withdrawal_transfer_hook_accounts; + let withdrawal_amount; + + if a_to_b { + deposit_token_program = token_program_a; + deposit_mint = token_mint_a; + deposit_account_user = token_owner_account_a; + deposit_account_pool = token_vault_a; + deposit_transfer_hook_accounts = transfer_hook_accounts_a; + deposit_amount = amount_a; + + withdrawal_token_program = token_program_b; + withdrawal_mint = token_mint_b; + withdrawal_account_user = token_owner_account_b; + withdrawal_account_pool = token_vault_b; + withdrawal_transfer_hook_accounts = transfer_hook_accounts_b; + withdrawal_amount = amount_b; + } else { + deposit_token_program = token_program_b; + deposit_mint = token_mint_b; + deposit_account_user = token_owner_account_b; + deposit_account_pool = token_vault_b; + deposit_transfer_hook_accounts = transfer_hook_accounts_b; + deposit_amount = amount_b; + + withdrawal_token_program = token_program_a; + withdrawal_mint = token_mint_a; + withdrawal_account_user = token_owner_account_a; + withdrawal_account_pool = token_vault_a; + withdrawal_transfer_hook_accounts = transfer_hook_accounts_a; + withdrawal_amount = amount_a; + } + + transfer_from_owner_to_vault_v2( + token_authority, + deposit_mint, + deposit_account_user, + deposit_account_pool, + deposit_token_program, + memo_program, + deposit_transfer_hook_accounts, + deposit_amount, + )?; + + transfer_from_vault_to_owner_v2( + whirlpool, + withdrawal_mint, + withdrawal_account_pool, + withdrawal_account_user, + withdrawal_token_program, + memo_program, + withdrawal_transfer_hook_accounts, + withdrawal_amount, + memo, + )?; + + Ok(()) +} + +pub fn update_and_two_hop_swap_whirlpool_v2<'info>( + // update + swap_update_one: PostSwapUpdate, + swap_update_two: PostSwapUpdate, + // whirlpool + whirlpool_one: &mut Account<'info, Whirlpool>, + whirlpool_two: &mut Account<'info, Whirlpool>, + // direction + is_token_fee_in_one_a: bool, + is_token_fee_in_two_a: bool, + // mint + token_mint_input: &InterfaceAccount<'info, Mint>, + token_mint_intermediate: &InterfaceAccount<'info, Mint>, + token_mint_output: &InterfaceAccount<'info, Mint>, + // token program + token_program_input: &Interface<'info, TokenInterface>, + token_program_intermediate: &Interface<'info, TokenInterface>, + token_program_output: &Interface<'info, TokenInterface>, + // token accounts + token_owner_account_input: &InterfaceAccount<'info, TokenAccount>, + token_vault_one_input: &InterfaceAccount<'info, TokenAccount>, + token_vault_one_intermediate: &InterfaceAccount<'info, TokenAccount>, + token_vault_two_intermediate: &InterfaceAccount<'info, TokenAccount>, + token_vault_two_output: &InterfaceAccount<'info, TokenAccount>, + token_owner_account_output: &InterfaceAccount<'info, TokenAccount>, + // hook + transfer_hook_accounts_input: &Option>>, + transfer_hook_accounts_intermediate: &Option>>, + transfer_hook_accounts_output: &Option>>, + // common + token_authority: &Signer<'info>, + memo_program: &Program<'info, Memo>, + reward_last_updated_timestamp: u64, + memo: &[u8], +) -> Result<()> { + whirlpool_one.update_after_swap( + swap_update_one.next_liquidity, + swap_update_one.next_tick_index, + swap_update_one.next_sqrt_price, + swap_update_one.next_fee_growth_global, + swap_update_one.next_reward_infos, + swap_update_one.next_protocol_fee, + is_token_fee_in_one_a, + reward_last_updated_timestamp, + ); + + whirlpool_two.update_after_swap( + swap_update_two.next_liquidity, + swap_update_two.next_tick_index, + swap_update_two.next_sqrt_price, + swap_update_two.next_fee_growth_global, + swap_update_two.next_reward_infos, + swap_update_two.next_protocol_fee, + is_token_fee_in_two_a, + reward_last_updated_timestamp, + ); + + // amount + let (input_amount, intermediate_amount) = if is_token_fee_in_one_a { + (swap_update_one.amount_a, swap_update_one.amount_b) + } else { + (swap_update_one.amount_b, swap_update_one.amount_a) + }; + let output_amount = if is_token_fee_in_two_a { swap_update_two.amount_b } else { swap_update_two.amount_a }; + + transfer_from_owner_to_vault_v2( + token_authority, + token_mint_input, + token_owner_account_input, + token_vault_one_input, + token_program_input, + memo_program, + transfer_hook_accounts_input, + input_amount, + )?; + + // Transfer from pool to pool + transfer_from_vault_to_owner_v2( + whirlpool_one, + token_mint_intermediate, + token_vault_one_intermediate, + token_vault_two_intermediate, + token_program_intermediate, + memo_program, + transfer_hook_accounts_intermediate, + intermediate_amount, + memo, + )?; + + transfer_from_vault_to_owner_v2( + whirlpool_two, + token_mint_output, + token_vault_two_output, + token_owner_account_output, + token_program_output, + memo_program, + transfer_hook_accounts_output, + output_amount, + memo, + )?; + + Ok(()) +} diff --git a/programs/whirlpool/src/util/v2/token.rs b/programs/whirlpool/src/util/v2/token.rs new file mode 100644 index 000000000..4ebd4b871 --- /dev/null +++ b/programs/whirlpool/src/util/v2/token.rs @@ -0,0 +1,494 @@ +use crate::state::{TokenBadge, Whirlpool}; +use crate::errors::ErrorCode; +use anchor_lang::prelude::*; +use anchor_spl::token_2022::spl_token_2022::extension::transfer_fee::{TransferFee, MAX_FEE_BASIS_POINTS}; +use anchor_spl::token_interface::spl_token_2022::extension::BaseStateWithExtensions; + +use anchor_spl::token::Token; +use anchor_spl::token_2022::spl_token_2022::{self, extension::{self, StateWithExtensions}, state::AccountState}; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use anchor_spl::memo::{self, Memo, BuildMemo}; +use spl_transfer_hook_interface; + +pub fn transfer_from_owner_to_vault_v2<'info>( + authority: &Signer<'info>, + token_mint: &InterfaceAccount<'info, Mint>, + token_owner_account: &InterfaceAccount<'info, TokenAccount>, + token_vault: &InterfaceAccount<'info, TokenAccount>, + token_program: &Interface<'info, TokenInterface>, + memo_program: &Program<'info, Memo>, + transfer_hook_accounts: &Option>>, + amount: u64, +) -> Result<()> { + // TransferFee extension + if let Some(epoch_transfer_fee) = get_epoch_transfer_fee(token_mint)? { + // log applied transfer fee + // - Not must, but important for ease of investigation and replay when problems occur + // - Use Memo because logs risk being truncated + let transfer_fee_memo = format!( + "TFe: {}, {}", + u16::from(epoch_transfer_fee.transfer_fee_basis_points), + u64::from(epoch_transfer_fee.maximum_fee), + ); + memo::build_memo( + CpiContext::new( + memo_program.to_account_info(), + BuildMemo {} + ), + transfer_fee_memo.as_bytes() + )?; + } + + // MemoTransfer extension + // The vault doesn't have MemoTransfer extension, so we don't need to use memo_program here + + let mut instruction = spl_token_2022::instruction::transfer_checked( + token_program.key, + &token_owner_account.key(), // from + &token_mint.key(), // mint + &token_vault.key(), // to + authority.key, // authority + &[], + amount, + token_mint.decimals, + )?; + + let mut account_infos = vec![ + token_program.to_account_info(), + token_owner_account.to_account_info(), + token_mint.to_account_info(), + token_vault.to_account_info(), + authority.to_account_info(), + ]; + + // TransferHook extension + if let Some(hook_program_id) = get_transfer_hook_program_id(token_mint)? { + if transfer_hook_accounts.is_none() { + return Err(ErrorCode::NoExtraAccountsForTransferHook.into()); + } + + spl_transfer_hook_interface::onchain::add_extra_accounts_for_execute_cpi( + &mut instruction, + &mut account_infos, + &hook_program_id, + token_owner_account.to_account_info(), + token_mint.to_account_info(), + token_vault.to_account_info(), + authority.to_account_info(), + amount, + &transfer_hook_accounts.clone().unwrap(), + )?; + } + + solana_program::program::invoke_signed( + &instruction, + &account_infos, + &[], + )?; + + Ok(()) +} + +pub fn transfer_from_vault_to_owner_v2<'info>( + whirlpool: &Account<'info, Whirlpool>, + token_mint: &InterfaceAccount<'info, Mint>, + token_vault: &InterfaceAccount<'info, TokenAccount>, + token_owner_account: &InterfaceAccount<'info, TokenAccount>, + token_program: &Interface<'info, TokenInterface>, + memo_program: &Program<'info, Memo>, + transfer_hook_accounts: &Option>>, + amount: u64, + memo: &[u8], +) -> Result<()> { + // TransferFee extension + if let Some(epoch_transfer_fee) = get_epoch_transfer_fee(token_mint)? { + // log applied transfer fee + // - Not must, but important for ease of investigation and replay when problems occur + // - Use Memo because logs risk being truncated + let transfer_fee_memo = format!( + "TFe: {}, {}", + u16::from(epoch_transfer_fee.transfer_fee_basis_points), + u64::from(epoch_transfer_fee.maximum_fee), + ); + memo::build_memo( + CpiContext::new( + memo_program.to_account_info(), + BuildMemo {} + ), + transfer_fee_memo.as_bytes() + )?; + } + + // MemoTransfer extension + if is_transfer_memo_required(&token_owner_account)? { + memo::build_memo( + CpiContext::new( + memo_program.to_account_info(), + BuildMemo {} + ), + memo + )?; + } + + let mut instruction = spl_token_2022::instruction::transfer_checked( + token_program.key, + &token_vault.key(), // from + &token_mint.key(), // mint + &token_owner_account.key(), // to + &whirlpool.key(), // authority + &[], + amount, + token_mint.decimals, + )?; + + let mut account_infos = vec![ + token_program.to_account_info(), + token_vault.to_account_info(), + token_mint.to_account_info(), + token_owner_account.to_account_info(), + whirlpool.to_account_info(), + ]; + + // TransferHook extension + if let Some(hook_program_id) = get_transfer_hook_program_id(token_mint)? { + if transfer_hook_accounts.is_none() { + return Err(ErrorCode::NoExtraAccountsForTransferHook.into()); + } + + spl_transfer_hook_interface::onchain::add_extra_accounts_for_execute_cpi( + &mut instruction, + &mut account_infos, + &hook_program_id, + token_owner_account.to_account_info(), + token_mint.to_account_info(), + token_vault.to_account_info(), + whirlpool.to_account_info(), + amount, + &transfer_hook_accounts.clone().unwrap(), + )?; + } + + solana_program::program::invoke_signed( + &instruction, + &account_infos, + &[&whirlpool.seeds()], + )?; + + Ok(()) +} + +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_transfer_memo_required<'info>(token_account: &InterfaceAccount<'info, TokenAccount>) -> 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::(); + + if let Ok(memo_transfer) = extension { + return Ok(memo_transfer.require_incoming_transfer_memos.into()); + } else { + return Ok(false); + } +} + +pub fn is_supported_token_mint<'info>( + token_mint: &InterfaceAccount<'info, Mint>, + is_token_badge_initialized: bool, +) -> Result { + let token_mint_info = token_mint.to_account_info(); + + // if mint is owned by Token Program, it is supported (compatible to initialize_pool / initialize_reward) + if *token_mint_info.owner == Token::id() { + return Ok(true); + } + + // now mint is owned by Token-2022 Program + + // reject native mint of Token-2022 Program to avoid SOL liquidity fragmentation + if spl_token_2022::native_mint::check_id(&token_mint.key()) { + return Ok(false); + } + + // reject if mint has freeze_authority + 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 + // + // WhirlpoolProgram invokes TransferChecked instruction and it supports non-confidential transfer only. + // + // Because the vault accounts are not configured to support confidential transfer, + // it is impossible to send tokens directly to the vault accounts confidentially. + // Note: Only the owner (Whirlpool account) can call ConfidentialTransferInstruction::ConfigureAccount. + } + // 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); } + } + extension::ExtensionType::DefaultAccountState => { + if !is_token_badge_initialized { return Ok(false); } + + // reject if default state is not Initialized even if it has token badge + let default_state = token_mint_unpacked.get_extension::()?; + let initialized: u8 = AccountState::Initialized.into(); + if default_state.state != initialized { + return Ok(false); + } + } + // No possibility to support the following extensions + extension::ExtensionType::NonTransferable => { return Ok(false); } + // mint has unknown or unsupported extensions + _ => { return Ok(false); } + } + } + + return Ok(true); +} + +pub fn is_token_badge_initialized<'info>( + whirlpools_config_key: Pubkey, + token_mint_key: Pubkey, + token_badge: &UncheckedAccount<'info>, +) -> 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.whirlpools_config == whirlpools_config_key && + token_badge.token_mint == token_mint_key + ) +} + +#[derive(Debug)] +pub struct TransferFeeIncludedAmount { + pub amount: u64, + pub transfer_fee: u64, +} + +#[derive(Debug)] +pub struct TransferFeeExcludedAmount { + pub amount: u64, + pub transfer_fee: u64, +} + +pub fn calculate_transfer_fee_excluded_amount<'info>( + token_mint: &InterfaceAccount<'info, Mint>, + transfer_fee_included_amount: u64, +) -> Result { + if let Some(epoch_transfer_fee) = get_epoch_transfer_fee(token_mint)? { + let transfer_fee = epoch_transfer_fee.calculate_fee(transfer_fee_included_amount).unwrap(); + let transfer_fee_excluded_amount = transfer_fee_included_amount.checked_sub(transfer_fee).unwrap(); + return Ok(TransferFeeExcludedAmount { amount: transfer_fee_excluded_amount, transfer_fee }); + } + + Ok(TransferFeeExcludedAmount { amount: transfer_fee_included_amount, transfer_fee: 0 }) +} + +pub fn calculate_transfer_fee_included_amount<'info>( + token_mint: &InterfaceAccount<'info, Mint>, + transfer_fee_excluded_amount: u64, +) -> Result { + if transfer_fee_excluded_amount == 0 { + return Ok(TransferFeeIncludedAmount { amount: 0, transfer_fee: 0 }); + } + + // now transfer_fee_excluded_amount > 0 + + if let Some(epoch_transfer_fee) = get_epoch_transfer_fee(token_mint)? { + let transfer_fee: u64 = if u16::from(epoch_transfer_fee.transfer_fee_basis_points) == MAX_FEE_BASIS_POINTS { + // edge-case: if transfer fee rate is 100%, current SPL implementation returns 0 as inverse fee. + // https://github.com/solana-labs/solana-program-library/blob/fe1ac9a2c4e5d85962b78c3fc6aaf028461e9026/token/program-2022/src/extension/transfer_fee/mod.rs#L95 + + // But even if transfer fee is 100%, we can use maximum_fee as transfer fee. + // if transfer_fee_excluded_amount + maximum_fee > u64 max, the following checked_add should fail. + u64::from(epoch_transfer_fee.maximum_fee) + } else { + epoch_transfer_fee.calculate_inverse_fee(transfer_fee_excluded_amount) + .ok_or(ErrorCode::TransferFeeCalculationError)? + }; + + let transfer_fee_included_amount = transfer_fee_excluded_amount.checked_add(transfer_fee) + .ok_or(ErrorCode::TransferFeeCalculationError)?; + + // verify transfer fee calculation for safety + let transfer_fee_verification = epoch_transfer_fee.calculate_fee(transfer_fee_included_amount).unwrap(); + if transfer_fee != transfer_fee_verification { + // We believe this should never happen + return Err(ErrorCode::TransferFeeCalculationError.into()); + } + + return Ok(TransferFeeIncludedAmount { amount: transfer_fee_included_amount, transfer_fee }); + } + + Ok(TransferFeeIncludedAmount { amount: transfer_fee_excluded_amount, transfer_fee: 0 }) +} + +pub fn get_epoch_transfer_fee<'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)?; + if let Ok(transfer_fee_config) = token_mint_unpacked.get_extension::() { + let epoch = Clock::get()?.epoch; + return Ok(Some(transfer_fee_config.get_epoch_fee(epoch).clone())); + } + + Ok(None) +} + +// special thanks for OtterSec +#[cfg(test)] +mod fuzz_tests { + use proptest::prelude::*; + use super::*; + + struct SyscallStubs {} + impl solana_program::program_stubs::SyscallStubs for SyscallStubs { + fn sol_get_clock_sysvar(&self, _var_addr: *mut u8) -> u64 { + 0 + } + } + + #[derive(Default, AnchorSerialize)] + struct MintWithTransferFeeConfigLayout { + // 82 for Mint + pub coption_mint_authority: u32, // 4 + pub mint_authority: Pubkey, // 32 + pub supply: u64, // 8 + pub decimals: u8, // 1 + pub is_initialized: bool, // 1 + pub coption_freeze_authority: u32, // 4 + pub freeze_authority: Pubkey, // 4 + 32 + + // 83 for padding + pub padding1: [u8; 32], + pub padding2: [u8; 32], + pub padding3: [u8; 19], + + pub account_type: u8, // 1 + + pub extension_type: u16, // 2 + pub extension_length: u16, // 2 + // 108 for TransferFeeConfig data + pub transfer_fee_config_authority: Pubkey, // 32 + pub withdraw_withheld_authority: Pubkey, // 32 + pub withheld_amount: u64, // 8 + pub older_epoch: u64, // 8 + pub older_maximum_fee: u64, // 8 + pub older_transfer_fee_basis_point: u16, // 2 + pub newer_epoch: u64, // 8 + pub newer_maximum_fee: u64, // 8 + pub newer_transfer_fee_basis_point: u16, // 2 + } + impl MintWithTransferFeeConfigLayout { + pub const LEN: usize = 82 + 83 + 1 + 2 + 2 + 108; + } + + /// Maximum possible fee in basis points is 100%, aka 10_000 basis points + const MAX_FEE_BASIS_POINTS: u16 = 10_000; + const MAX_FEE: u64 = 1_000_000_000; + const MAX_AMOUNT: u64 = 0xFFFFFFFF; + + proptest! { + #![proptest_config(ProptestConfig::with_cases(100000))] + #[test] + fn test_calculate_transfer_fee_included_amount( + amount in 0..MAX_AMOUNT, + maximum_fee in 0..MAX_FEE, + transfer_fee_basis_point in 0..MAX_FEE_BASIS_POINTS + ) { + // stub Clock + solana_program::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {})); + assert_eq!(Clock::get().unwrap().epoch, 0); + + let mint_with_transfer_fee_config = MintWithTransferFeeConfigLayout { + is_initialized: true, + account_type: 1, // Mint + extension_type: 1, // TransferFeeConfig + extension_length: 108, + older_epoch: 0, + older_maximum_fee: maximum_fee, + older_transfer_fee_basis_point: transfer_fee_basis_point, + newer_epoch: 0, + newer_maximum_fee: maximum_fee, + newer_transfer_fee_basis_point: transfer_fee_basis_point, + ..Default::default() + }; + + let mut data = Vec::::new(); + mint_with_transfer_fee_config.serialize(&mut data).unwrap(); + assert_eq!(data.len(), MintWithTransferFeeConfigLayout::LEN); + + let key = Pubkey::default(); + let mut lamports = 0u64; + let owner = anchor_spl::token_2022::ID; + let rent_epoch = 0; + let is_signer = false; + let is_writable = false; + let executable = false; + let account_info = AccountInfo::new( + &key, + is_signer, + is_writable, + &mut lamports, + &mut data, + &owner, + executable, + rent_epoch + ); + + let interface_account_mint = InterfaceAccount::::try_from(&account_info).unwrap(); + + let transfer_fee = get_epoch_transfer_fee(&interface_account_mint).unwrap().unwrap(); + assert_eq!(u64::from(transfer_fee.maximum_fee), maximum_fee); + assert_eq!(u16::from(transfer_fee.transfer_fee_basis_points), transfer_fee_basis_point); + + let _ = calculate_transfer_fee_included_amount(&interface_account_mint, amount)?; + } + } +} \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 182daa82e..56532bb8e 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,26 +1,27 @@ { "name": "@orca-so/whirlpools-sdk", - "version": "0.12.5", + "version": "0.13.0-alpha.1", "description": "Typescript SDK to interact with Orca's Whirlpool program.", "license": "Apache-2.0", "main": "dist/index.js", "types": "dist/index.d.ts", "peerDependencies": { - "@coral-xyz/anchor": "~0.27.0", - "@orca-so/common-sdk": "^0.5.2", - "@solana/spl-token": "^0.3.8", - "@solana/web3.js": "^1.75.0", - "decimal.js": "^10.3.1" + "@coral-xyz/anchor": "~0.29.0", + "@orca-so/common-sdk": "0.6.0-alpha.1", + "@solana/spl-token": "^0.4.1", + "@solana/web3.js": "^1.90.0", + "decimal.js": "^10.4.3" }, "dependencies": { "tiny-invariant": "^1.3.1" }, "devDependencies": { - "@coral-xyz/anchor": "^0.27.0", - "@metaplex-foundation/mpl-token-metadata": "2.12.0", - "@orca-so/common-sdk": "^0.5.3", - "@solana/spl-token": "^0.3.11", - "@solana/web3.js": "^1.88.0", + "@coral-xyz/anchor": "^0.29.0", + "@orca-so/common-sdk": "0.6.0-alpha.2", + "@solana/spl-token": "^0.4.1", + "@solana/spl-token-group": "^0.0.1", + "@solana/spl-token-metadata": "^0.1.2", + "@solana/web3.js": "^1.90.0", "@types/bn.js": "~5.1.5", "@types/mocha": "^10.0.6", "@types/node": "^20.10.8", diff --git a/sdk/src/artifacts/whirlpool.json b/sdk/src/artifacts/whirlpool.json index be5999c49..d9c038722 100644 --- a/sdk/src/artifacts/whirlpool.json +++ b/sdk/src/artifacts/whirlpool.json @@ -1,5 +1,5 @@ { - "version": "0.2.0", + "version": "0.3.0", "name": "whirlpool", "instructions": [ { @@ -1843,9 +1843,1204 @@ "type": "u16" } ] + }, + { + "name": "collectFeesV2", + "docs": [ + "Collect fees accrued for this position.", + "", + "### Authority", + "- `position_authority` - authority that owns the token corresponding to this desired position." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": false, + "isSigner": false + }, + { + "name": "positionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "positionTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintB", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenOwnerAccountA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenOwnerAccountB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgramA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramB", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } + } + ] + }, + { + "name": "collectProtocolFeesV2", + "docs": [ + "Collect the protocol fees accrued in this Whirlpool", + "", + "### Authority", + "- `collect_protocol_fees_authority` - assigned authority in the WhirlpoolConfig that can collect protocol fees" + ], + "accounts": [ + { + "name": "whirlpoolsConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "collectProtocolFeesAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenMintA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintB", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenDestinationA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenDestinationB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgramA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramB", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } + } + ] + }, + { + "name": "collectRewardV2", + "docs": [ + "Collect rewards accrued for this position.", + "", + "### Authority", + "- `position_authority` - authority that owns the token corresponding to this desired position." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": false, + "isSigner": false + }, + { + "name": "positionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "positionTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "rewardOwnerAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "rewardMint", + "isMut": false, + "isSigner": false + }, + { + "name": "rewardVault", + "isMut": true, + "isSigner": false + }, + { + "name": "rewardTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "rewardIndex", + "type": "u8" + }, + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } + } + ] + }, + { + "name": "decreaseLiquidityV2", + "docs": [ + "Withdraw liquidity from a position in the Whirlpool. This call also updates the position's accrued fees and rewards.", + "", + "### Authority", + "- `position_authority` - authority that owns the token corresponding to this desired position.", + "", + "### Parameters", + "- `liquidity_amount` - The total amount of Liquidity the user desires to withdraw.", + "- `token_min_a` - The minimum amount of tokenA the user is willing to withdraw.", + "- `token_min_b` - The minimum amount of tokenB the user is willing to withdraw.", + "", + "#### Special Errors", + "- `LiquidityZero` - Provided liquidity amount is zero.", + "- `LiquidityTooHigh` - Provided liquidity exceeds u128::max.", + "- `TokenMinSubceeded` - The required token to perform this operation subceeds the user defined amount." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgramA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramB", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "positionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "positionTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintB", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenOwnerAccountA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenOwnerAccountB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayLower", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayUpper", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "liquidityAmount", + "type": "u128" + }, + { + "name": "tokenMinA", + "type": "u64" + }, + { + "name": "tokenMinB", + "type": "u64" + }, + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } + } + ] + }, + { + "name": "increaseLiquidityV2", + "docs": [ + "Add liquidity to a position in the Whirlpool. This call also updates the position's accrued fees and rewards.", + "", + "### Authority", + "- `position_authority` - authority that owns the token corresponding to this desired position.", + "", + "### Parameters", + "- `liquidity_amount` - The total amount of Liquidity the user is willing to deposit.", + "- `token_max_a` - The maximum amount of tokenA the user is willing to deposit.", + "- `token_max_b` - The maximum amount of tokenB the user is willing to deposit.", + "", + "#### Special Errors", + "- `LiquidityZero` - Provided liquidity amount is zero.", + "- `LiquidityTooHigh` - Provided liquidity exceeds u128::max.", + "- `TokenMaxExceeded` - The required token to perform this operation exceeds the user defined amount." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgramA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramB", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "positionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "positionTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintB", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenOwnerAccountA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenOwnerAccountB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayLower", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayUpper", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "liquidityAmount", + "type": "u128" + }, + { + "name": "tokenMaxA", + "type": "u64" + }, + { + "name": "tokenMaxB", + "type": "u64" + }, + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } + } + ] + }, + { + "name": "initializePoolV2", + "docs": [ + "Initializes a Whirlpool account.", + "Fee rate is set to the default values on the config and supplied fee_tier.", + "", + "### Parameters", + "- `bumps` - The bump value when deriving the PDA of the Whirlpool address.", + "- `tick_spacing` - The desired tick spacing for this pool.", + "- `initial_sqrt_price` - The desired initial sqrt-price for this pool", + "", + "#### Special Errors", + "`InvalidTokenMintOrder` - The order of mints have to be ordered by", + "`SqrtPriceOutOfBounds` - provided initial_sqrt_price is not between 2^-64 to 2^64", + "" + ], + "accounts": [ + { + "name": "whirlpoolsConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintB", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenBadgeA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenBadgeB", + "isMut": false, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": true + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": true + }, + { + "name": "feeTier", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramB", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "tickSpacing", + "type": "u16" + }, + { + "name": "initialSqrtPrice", + "type": "u128" + } + ] + }, + { + "name": "initializeRewardV2", + "docs": [ + "Initialize reward for a Whirlpool. A pool can only support up to a set number of rewards.", + "", + "### Authority", + "- \"reward_authority\" - assigned authority by the reward_super_authority for the specified", + "reward-index in this Whirlpool", + "", + "### Parameters", + "- `reward_index` - The reward index that we'd like to initialize. (0 <= index <= NUM_REWARDS)", + "", + "#### Special Errors", + "- `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized", + "index in this pool, or exceeds NUM_REWARDS, or", + "all reward slots for this pool has been initialized." + ], + "accounts": [ + { + "name": "rewardAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "rewardMint", + "isMut": false, + "isSigner": false + }, + { + "name": "rewardTokenBadge", + "isMut": false, + "isSigner": false + }, + { + "name": "rewardVault", + "isMut": true, + "isSigner": true + }, + { + "name": "rewardTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "rewardIndex", + "type": "u8" + } + ] + }, + { + "name": "setRewardEmissionsV2", + "docs": [ + "Set the reward emissions for a reward in a Whirlpool.", + "", + "### Authority", + "- \"reward_authority\" - assigned authority by the reward_super_authority for the specified", + "reward-index in this Whirlpool", + "", + "### Parameters", + "- `reward_index` - The reward index (0 <= index <= NUM_REWARDS) that we'd like to modify.", + "- `emissions_per_second_x64` - The amount of rewards emitted in this pool.", + "", + "#### Special Errors", + "- `RewardVaultAmountInsufficient` - The amount of rewards in the reward vault cannot emit", + "more than a day of desired emissions.", + "- `InvalidTimestamp` - Provided timestamp is not in order with the previous timestamp.", + "- `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized", + "index in this pool, or exceeds NUM_REWARDS, or", + "all reward slots for this pool has been initialized." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "rewardAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "rewardVault", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "rewardIndex", + "type": "u8" + }, + { + "name": "emissionsPerSecondX64", + "type": "u128" + } + ] + }, + { + "name": "swapV2", + "docs": [ + "Perform a swap in this Whirlpool", + "", + "### Authority", + "- \"token_authority\" - The authority to withdraw tokens from the input token account.", + "", + "### Parameters", + "- `amount` - The amount of input or output token to swap from (depending on amount_specified_is_input).", + "- `other_amount_threshold` - The maximum/minimum of input/output token to swap into (depending on amount_specified_is_input).", + "- `sqrt_price_limit` - The maximum/minimum price the swap will swap to.", + "- `amount_specified_is_input` - Specifies the token the parameter `amount`represents. If true, the amount represents the input token of the swap.", + "- `a_to_b` - The direction of the swap. True if swapping from A to B. False if swapping from B to A.", + "", + "#### Special Errors", + "- `ZeroTradableAmount` - User provided parameter `amount` is 0.", + "- `InvalidSqrtPriceLimitDirection` - User provided parameter `sqrt_price_limit` does not match the direction of the trade.", + "- `SqrtPriceOutOfBounds` - User provided parameter `sqrt_price_limit` is over Whirlppool's max/min bounds for sqrt-price.", + "- `InvalidTickArraySequence` - User provided tick-arrays are not in sequential order required to proceed in this trade direction.", + "- `TickArraySequenceInvalidIndex` - The swap loop attempted to access an invalid array index during the query of the next initialized tick.", + "- `TickArrayIndexOutofBounds` - The swap loop attempted to access an invalid array index during tick crossing.", + "- `LiquidityOverflow` - Liquidity value overflowed 128bits during tick crossing.", + "- `InvalidTickSpacing` - The swap pool was initialized with tick-spacing of 0." + ], + "accounts": [ + { + "name": "tokenProgramA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramB", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenMintA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintB", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenOwnerAccountA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenOwnerAccountB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArray0", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArray1", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArray2", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "otherAmountThreshold", + "type": "u64" + }, + { + "name": "sqrtPriceLimit", + "type": "u128" + }, + { + "name": "amountSpecifiedIsInput", + "type": "bool" + }, + { + "name": "aToB", + "type": "bool" + }, + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } + } + ] + }, + { + "name": "twoHopSwapV2", + "docs": [ + "Perform a two-hop swap in this Whirlpool", + "", + "### Authority", + "- \"token_authority\" - The authority to withdraw tokens from the input token account.", + "", + "### Parameters", + "- `amount` - The amount of input or output token to swap from (depending on amount_specified_is_input).", + "- `other_amount_threshold` - The maximum/minimum of input/output token to swap into (depending on amount_specified_is_input).", + "- `amount_specified_is_input` - Specifies the token the parameter `amount`represents. If true, the amount represents the input token of the swap.", + "- `a_to_b_one` - The direction of the swap of hop one. True if swapping from A to B. False if swapping from B to A.", + "- `a_to_b_two` - The direction of the swap of hop two. True if swapping from A to B. False if swapping from B to A.", + "- `sqrt_price_limit_one` - The maximum/minimum price the swap will swap to in the first hop.", + "- `sqrt_price_limit_two` - The maximum/minimum price the swap will swap to in the second hop.", + "", + "#### Special Errors", + "- `ZeroTradableAmount` - User provided parameter `amount` is 0.", + "- `InvalidSqrtPriceLimitDirection` - User provided parameter `sqrt_price_limit` does not match the direction of the trade.", + "- `SqrtPriceOutOfBounds` - User provided parameter `sqrt_price_limit` is over Whirlppool's max/min bounds for sqrt-price.", + "- `InvalidTickArraySequence` - User provided tick-arrays are not in sequential order required to proceed in this trade direction.", + "- `TickArraySequenceInvalidIndex` - The swap loop attempted to access an invalid array index during the query of the next initialized tick.", + "- `TickArrayIndexOutofBounds` - The swap loop attempted to access an invalid array index during tick crossing.", + "- `LiquidityOverflow` - Liquidity value overflowed 128bits during tick crossing.", + "- `InvalidTickSpacing` - The swap pool was initialized with tick-spacing of 0.", + "- `InvalidIntermediaryMint` - Error if the intermediary mint between hop one and two do not equal.", + "- `DuplicateTwoHopPool` - Error if whirlpool one & two are the same pool." + ], + "accounts": [ + { + "name": "whirlpoolOne", + "isMut": true, + "isSigner": false + }, + { + "name": "whirlpoolTwo", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenMintInput", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintIntermediate", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintOutput", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramInput", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramIntermediate", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramOutput", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenOwnerAccountInput", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultOneInput", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultOneIntermediate", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultTwoIntermediate", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultTwoOutput", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenOwnerAccountOutput", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "tickArrayOne0", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayOne1", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayOne2", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayTwo0", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayTwo1", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayTwo2", + "isMut": true, + "isSigner": false + }, + { + "name": "oracleOne", + "isMut": true, + "isSigner": false + }, + { + "name": "oracleTwo", + "isMut": true, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "otherAmountThreshold", + "type": "u64" + }, + { + "name": "amountSpecifiedIsInput", + "type": "bool" + }, + { + "name": "aToBOne", + "type": "bool" + }, + { + "name": "aToBTwo", + "type": "bool" + }, + { + "name": "sqrtPriceLimitOne", + "type": "u128" + }, + { + "name": "sqrtPriceLimitTwo", + "type": "u128" + }, + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } + } + ] + }, + { + "name": "initializeConfigExtension", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "configExtension", + "isMut": true, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "feeAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setConfigExtensionAuthority", + "accounts": [ + { + "name": "whirlpoolsConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "whirlpoolsConfigExtension", + "isMut": true, + "isSigner": false + }, + { + "name": "configExtensionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "newConfigExtensionAuthority", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setTokenBadgeAuthority", + "accounts": [ + { + "name": "whirlpoolsConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "whirlpoolsConfigExtension", + "isMut": true, + "isSigner": false + }, + { + "name": "configExtensionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "newTokenBadgeAuthority", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initializeTokenBadge", + "accounts": [ + { + "name": "whirlpoolsConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "whirlpoolsConfigExtension", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenBadgeAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenMint", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenBadge", + "isMut": true, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "deleteTokenBadge", + "accounts": [ + { + "name": "whirlpoolsConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "whirlpoolsConfigExtension", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenBadgeAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenMint", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenBadge", + "isMut": true, + "isSigner": false + }, + { + "name": "receiver", + "isMut": true, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ + { + "name": "WhirlpoolsConfigExtension", + "type": { + "kind": "struct", + "fields": [ + { + "name": "whirlpoolsConfig", + "type": "publicKey" + }, + { + "name": "configExtensionAuthority", + "type": "publicKey" + }, + { + "name": "tokenBadgeAuthority", + "type": "publicKey" + } + ] + } + }, { "name": "WhirlpoolsConfig", "type": { @@ -1993,6 +3188,22 @@ ] } }, + { + "name": "TokenBadge", + "type": { + "kind": "struct", + "fields": [ + { + "name": "whirlpoolsConfig", + "type": "publicKey" + }, + { + "name": "tokenMint", + "type": "publicKey" + } + ] + } + }, { "name": "Whirlpool", "type": { @@ -2239,6 +3450,40 @@ ] } }, + { + "name": "RemainingAccountsSlice", + "type": { + "kind": "struct", + "fields": [ + { + "name": "accountsType", + "type": { + "defined": "AccountsType" + } + }, + { + "name": "length", + "type": "u8" + } + ] + } + }, + { + "name": "RemainingAccountsInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "slices", + "type": { + "vec": { + "defined": "RemainingAccountsSlice" + } + } + } + ] + } + }, { "name": "CurrIndex", "type": { @@ -2283,6 +3528,32 @@ } ] } + }, + { + "name": "AccountsType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "TransferHookA" + }, + { + "name": "TransferHookB" + }, + { + "name": "TransferHookReward" + }, + { + "name": "TransferHookInput" + }, + { + "name": "TransferHookIntermediate" + }, + { + "name": "TransferHookOutput" + } + ] + } } ], "errors": [ @@ -2520,6 +3791,41 @@ "code": 6046, "name": "PositionBundleNotDeletable", "msg": "Unable to delete PositionBundle with open positions" + }, + { + "code": 6047, + "name": "UnsupportedTokenMint", + "msg": "Token mint has unsupported attributes" + }, + { + "code": 6048, + "name": "RemainingAccountsInvalidSlice", + "msg": "Invalid remaining accounts" + }, + { + "code": 6049, + "name": "RemainingAccountsInsufficient", + "msg": "Insufficient remaining accounts" + }, + { + "code": 6050, + "name": "NoExtraAccountsForTransferHook", + "msg": "Unable to call transfer hook without extra accounts" + }, + { + "code": 6051, + "name": "IntermediateTokenAmountMismatch", + "msg": "Output and input amount mismatch" + }, + { + "code": 6052, + "name": "TransferFeeCalculationError", + "msg": "Transfer fee calculation failed" + }, + { + "code": 6053, + "name": "RemainingAccountsDuplicatedAccountsType", + "msg": "Same accounts type is provided more than once" } ] } \ No newline at end of file diff --git a/sdk/src/artifacts/whirlpool.ts b/sdk/src/artifacts/whirlpool.ts index 23d8a17e5..ac79d989c 100644 --- a/sdk/src/artifacts/whirlpool.ts +++ b/sdk/src/artifacts/whirlpool.ts @@ -1,5 +1,5 @@ export type Whirlpool = { - "version": "0.2.0", + "version": "0.3.0", "name": "whirlpool", "instructions": [ { @@ -1843,220 +1843,1431 @@ export type Whirlpool = { "type": "u16" } ] - } - ], - "accounts": [ - { - "name": "whirlpoolsConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "feeAuthority", - "type": "publicKey" - }, - { - "name": "collectProtocolFeesAuthority", - "type": "publicKey" - }, - { - "name": "rewardEmissionsSuperAuthority", - "type": "publicKey" - }, - { - "name": "defaultProtocolFeeRate", - "type": "u16" - } - ] - } }, { - "name": "feeTier", - "type": { - "kind": "struct", - "fields": [ - { - "name": "whirlpoolsConfig", - "type": "publicKey" - }, - { - "name": "tickSpacing", - "type": "u16" - }, - { - "name": "defaultFeeRate", - "type": "u16" + "name": "collectFeesV2", + "docs": [ + "Collect fees accrued for this position.", + "", + "### Authority", + "- `position_authority` - authority that owns the token corresponding to this desired position." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": false, + "isSigner": false + }, + { + "name": "positionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "positionTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintB", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenOwnerAccountA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenOwnerAccountB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgramA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramB", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } } - ] - } + } + ] }, { - "name": "positionBundle", - "type": { - "kind": "struct", - "fields": [ - { - "name": "positionBundleMint", - "type": "publicKey" - }, - { - "name": "positionBitmap", - "type": { - "array": [ - "u8", - 32 - ] + "name": "collectProtocolFeesV2", + "docs": [ + "Collect the protocol fees accrued in this Whirlpool", + "", + "### Authority", + "- `collect_protocol_fees_authority` - assigned authority in the WhirlpoolConfig that can collect protocol fees" + ], + "accounts": [ + { + "name": "whirlpoolsConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "collectProtocolFeesAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenMintA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintB", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenDestinationA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenDestinationB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgramA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramB", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" } } - ] - } + } + ] }, { - "name": "position", - "type": { - "kind": "struct", - "fields": [ - { - "name": "whirlpool", - "type": "publicKey" - }, - { - "name": "positionMint", - "type": "publicKey" - }, - { - "name": "liquidity", - "type": "u128" - }, - { - "name": "tickLowerIndex", - "type": "i32" - }, - { - "name": "tickUpperIndex", - "type": "i32" - }, - { - "name": "feeGrowthCheckpointA", - "type": "u128" - }, - { - "name": "feeOwedA", - "type": "u64" - }, - { - "name": "feeGrowthCheckpointB", - "type": "u128" - }, - { - "name": "feeOwedB", - "type": "u64" - }, - { - "name": "rewardInfos", - "type": { - "array": [ - { - "defined": "PositionRewardInfo" - }, - 3 - ] - } + "name": "collectRewardV2", + "docs": [ + "Collect rewards accrued for this position.", + "", + "### Authority", + "- `position_authority` - authority that owns the token corresponding to this desired position." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": false, + "isSigner": false + }, + { + "name": "positionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "positionTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "rewardOwnerAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "rewardMint", + "isMut": false, + "isSigner": false + }, + { + "name": "rewardVault", + "isMut": true, + "isSigner": false + }, + { + "name": "rewardTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "rewardIndex", + "type": "u8" + }, + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } } - ] - } + } + ] }, { - "name": "tickArray", - "type": { - "kind": "struct", - "fields": [ - { - "name": "startTickIndex", - "type": "i32" - }, - { - "name": "ticks", - "type": { - "array": [ - { - "defined": "Tick" - }, - 88 - ] + "name": "decreaseLiquidityV2", + "docs": [ + "Withdraw liquidity from a position in the Whirlpool. This call also updates the position's accrued fees and rewards.", + "", + "### Authority", + "- `position_authority` - authority that owns the token corresponding to this desired position.", + "", + "### Parameters", + "- `liquidity_amount` - The total amount of Liquidity the user desires to withdraw.", + "- `token_min_a` - The minimum amount of tokenA the user is willing to withdraw.", + "- `token_min_b` - The minimum amount of tokenB the user is willing to withdraw.", + "", + "#### Special Errors", + "- `LiquidityZero` - Provided liquidity amount is zero.", + "- `LiquidityTooHigh` - Provided liquidity exceeds u128::max.", + "- `TokenMinSubceeded` - The required token to perform this operation subceeds the user defined amount." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgramA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramB", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "positionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "positionTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintB", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenOwnerAccountA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenOwnerAccountB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayLower", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayUpper", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "liquidityAmount", + "type": "u128" + }, + { + "name": "tokenMinA", + "type": "u64" + }, + { + "name": "tokenMinB", + "type": "u64" + }, + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" } - }, - { - "name": "whirlpool", - "type": "publicKey" } - ] - } + } + ] }, { - "name": "whirlpool", - "type": { - "kind": "struct", - "fields": [ - { - "name": "whirlpoolsConfig", - "type": "publicKey" - }, - { - "name": "whirlpoolBump", - "type": { - "array": [ - "u8", - 1 - ] - } - }, - { - "name": "tickSpacing", - "type": "u16" - }, - { - "name": "tickSpacingSeed", - "type": { - "array": [ - "u8", - 2 - ] - } - }, - { - "name": "feeRate", - "type": "u16" - }, - { - "name": "protocolFeeRate", - "type": "u16" - }, - { - "name": "liquidity", - "type": "u128" - }, - { - "name": "sqrtPrice", - "type": "u128" - }, - { - "name": "tickCurrentIndex", - "type": "i32" - }, - { - "name": "protocolFeeOwedA", - "type": "u64" - }, - { - "name": "protocolFeeOwedB", - "type": "u64" - }, - { - "name": "tokenMintA", - "type": "publicKey" - }, - { + "name": "increaseLiquidityV2", + "docs": [ + "Add liquidity to a position in the Whirlpool. This call also updates the position's accrued fees and rewards.", + "", + "### Authority", + "- `position_authority` - authority that owns the token corresponding to this desired position.", + "", + "### Parameters", + "- `liquidity_amount` - The total amount of Liquidity the user is willing to deposit.", + "- `token_max_a` - The maximum amount of tokenA the user is willing to deposit.", + "- `token_max_b` - The maximum amount of tokenB the user is willing to deposit.", + "", + "#### Special Errors", + "- `LiquidityZero` - Provided liquidity amount is zero.", + "- `LiquidityTooHigh` - Provided liquidity exceeds u128::max.", + "- `TokenMaxExceeded` - The required token to perform this operation exceeds the user defined amount." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgramA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramB", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "positionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "positionTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintB", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenOwnerAccountA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenOwnerAccountB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayLower", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayUpper", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "liquidityAmount", + "type": "u128" + }, + { + "name": "tokenMaxA", + "type": "u64" + }, + { + "name": "tokenMaxB", + "type": "u64" + }, + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } + } + ] + }, + { + "name": "initializePoolV2", + "docs": [ + "Initializes a Whirlpool account.", + "Fee rate is set to the default values on the config and supplied fee_tier.", + "", + "### Parameters", + "- `bumps` - The bump value when deriving the PDA of the Whirlpool address.", + "- `tick_spacing` - The desired tick spacing for this pool.", + "- `initial_sqrt_price` - The desired initial sqrt-price for this pool", + "", + "#### Special Errors", + "`InvalidTokenMintOrder` - The order of mints have to be ordered by", + "`SqrtPriceOutOfBounds` - provided initial_sqrt_price is not between 2^-64 to 2^64", + "" + ], + "accounts": [ + { + "name": "whirlpoolsConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintB", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenBadgeA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenBadgeB", + "isMut": false, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": true + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": true + }, + { + "name": "feeTier", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramB", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "tickSpacing", + "type": "u16" + }, + { + "name": "initialSqrtPrice", + "type": "u128" + } + ] + }, + { + "name": "initializeRewardV2", + "docs": [ + "Initialize reward for a Whirlpool. A pool can only support up to a set number of rewards.", + "", + "### Authority", + "- \"reward_authority\" - assigned authority by the reward_super_authority for the specified", + "reward-index in this Whirlpool", + "", + "### Parameters", + "- `reward_index` - The reward index that we'd like to initialize. (0 <= index <= NUM_REWARDS)", + "", + "#### Special Errors", + "- `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized", + "index in this pool, or exceeds NUM_REWARDS, or", + "all reward slots for this pool has been initialized." + ], + "accounts": [ + { + "name": "rewardAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "rewardMint", + "isMut": false, + "isSigner": false + }, + { + "name": "rewardTokenBadge", + "isMut": false, + "isSigner": false + }, + { + "name": "rewardVault", + "isMut": true, + "isSigner": true + }, + { + "name": "rewardTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "rewardIndex", + "type": "u8" + } + ] + }, + { + "name": "setRewardEmissionsV2", + "docs": [ + "Set the reward emissions for a reward in a Whirlpool.", + "", + "### Authority", + "- \"reward_authority\" - assigned authority by the reward_super_authority for the specified", + "reward-index in this Whirlpool", + "", + "### Parameters", + "- `reward_index` - The reward index (0 <= index <= NUM_REWARDS) that we'd like to modify.", + "- `emissions_per_second_x64` - The amount of rewards emitted in this pool.", + "", + "#### Special Errors", + "- `RewardVaultAmountInsufficient` - The amount of rewards in the reward vault cannot emit", + "more than a day of desired emissions.", + "- `InvalidTimestamp` - Provided timestamp is not in order with the previous timestamp.", + "- `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized", + "index in this pool, or exceeds NUM_REWARDS, or", + "all reward slots for this pool has been initialized." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "rewardAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "rewardVault", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "rewardIndex", + "type": "u8" + }, + { + "name": "emissionsPerSecondX64", + "type": "u128" + } + ] + }, + { + "name": "swapV2", + "docs": [ + "Perform a swap in this Whirlpool", + "", + "### Authority", + "- \"token_authority\" - The authority to withdraw tokens from the input token account.", + "", + "### Parameters", + "- `amount` - The amount of input or output token to swap from (depending on amount_specified_is_input).", + "- `other_amount_threshold` - The maximum/minimum of input/output token to swap into (depending on amount_specified_is_input).", + "- `sqrt_price_limit` - The maximum/minimum price the swap will swap to.", + "- `amount_specified_is_input` - Specifies the token the parameter `amount`represents. If true, the amount represents the input token of the swap.", + "- `a_to_b` - The direction of the swap. True if swapping from A to B. False if swapping from B to A.", + "", + "#### Special Errors", + "- `ZeroTradableAmount` - User provided parameter `amount` is 0.", + "- `InvalidSqrtPriceLimitDirection` - User provided parameter `sqrt_price_limit` does not match the direction of the trade.", + "- `SqrtPriceOutOfBounds` - User provided parameter `sqrt_price_limit` is over Whirlppool's max/min bounds for sqrt-price.", + "- `InvalidTickArraySequence` - User provided tick-arrays are not in sequential order required to proceed in this trade direction.", + "- `TickArraySequenceInvalidIndex` - The swap loop attempted to access an invalid array index during the query of the next initialized tick.", + "- `TickArrayIndexOutofBounds` - The swap loop attempted to access an invalid array index during tick crossing.", + "- `LiquidityOverflow` - Liquidity value overflowed 128bits during tick crossing.", + "- `InvalidTickSpacing` - The swap pool was initialized with tick-spacing of 0." + ], + "accounts": [ + { + "name": "tokenProgramA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramB", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenMintA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintB", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenOwnerAccountA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenOwnerAccountB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArray0", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArray1", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArray2", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "otherAmountThreshold", + "type": "u64" + }, + { + "name": "sqrtPriceLimit", + "type": "u128" + }, + { + "name": "amountSpecifiedIsInput", + "type": "bool" + }, + { + "name": "aToB", + "type": "bool" + }, + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } + } + ] + }, + { + "name": "twoHopSwapV2", + "docs": [ + "Perform a two-hop swap in this Whirlpool", + "", + "### Authority", + "- \"token_authority\" - The authority to withdraw tokens from the input token account.", + "", + "### Parameters", + "- `amount` - The amount of input or output token to swap from (depending on amount_specified_is_input).", + "- `other_amount_threshold` - The maximum/minimum of input/output token to swap into (depending on amount_specified_is_input).", + "- `amount_specified_is_input` - Specifies the token the parameter `amount`represents. If true, the amount represents the input token of the swap.", + "- `a_to_b_one` - The direction of the swap of hop one. True if swapping from A to B. False if swapping from B to A.", + "- `a_to_b_two` - The direction of the swap of hop two. True if swapping from A to B. False if swapping from B to A.", + "- `sqrt_price_limit_one` - The maximum/minimum price the swap will swap to in the first hop.", + "- `sqrt_price_limit_two` - The maximum/minimum price the swap will swap to in the second hop.", + "", + "#### Special Errors", + "- `ZeroTradableAmount` - User provided parameter `amount` is 0.", + "- `InvalidSqrtPriceLimitDirection` - User provided parameter `sqrt_price_limit` does not match the direction of the trade.", + "- `SqrtPriceOutOfBounds` - User provided parameter `sqrt_price_limit` is over Whirlppool's max/min bounds for sqrt-price.", + "- `InvalidTickArraySequence` - User provided tick-arrays are not in sequential order required to proceed in this trade direction.", + "- `TickArraySequenceInvalidIndex` - The swap loop attempted to access an invalid array index during the query of the next initialized tick.", + "- `TickArrayIndexOutofBounds` - The swap loop attempted to access an invalid array index during tick crossing.", + "- `LiquidityOverflow` - Liquidity value overflowed 128bits during tick crossing.", + "- `InvalidTickSpacing` - The swap pool was initialized with tick-spacing of 0.", + "- `InvalidIntermediaryMint` - Error if the intermediary mint between hop one and two do not equal.", + "- `DuplicateTwoHopPool` - Error if whirlpool one & two are the same pool." + ], + "accounts": [ + { + "name": "whirlpoolOne", + "isMut": true, + "isSigner": false + }, + { + "name": "whirlpoolTwo", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenMintInput", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintIntermediate", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintOutput", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramInput", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramIntermediate", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramOutput", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenOwnerAccountInput", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultOneInput", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultOneIntermediate", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultTwoIntermediate", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultTwoOutput", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenOwnerAccountOutput", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "tickArrayOne0", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayOne1", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayOne2", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayTwo0", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayTwo1", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayTwo2", + "isMut": true, + "isSigner": false + }, + { + "name": "oracleOne", + "isMut": true, + "isSigner": false + }, + { + "name": "oracleTwo", + "isMut": true, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "otherAmountThreshold", + "type": "u64" + }, + { + "name": "amountSpecifiedIsInput", + "type": "bool" + }, + { + "name": "aToBOne", + "type": "bool" + }, + { + "name": "aToBTwo", + "type": "bool" + }, + { + "name": "sqrtPriceLimitOne", + "type": "u128" + }, + { + "name": "sqrtPriceLimitTwo", + "type": "u128" + }, + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } + } + ] + }, + { + "name": "initializeConfigExtension", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "configExtension", + "isMut": true, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "feeAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setConfigExtensionAuthority", + "accounts": [ + { + "name": "whirlpoolsConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "whirlpoolsConfigExtension", + "isMut": true, + "isSigner": false + }, + { + "name": "configExtensionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "newConfigExtensionAuthority", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setTokenBadgeAuthority", + "accounts": [ + { + "name": "whirlpoolsConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "whirlpoolsConfigExtension", + "isMut": true, + "isSigner": false + }, + { + "name": "configExtensionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "newTokenBadgeAuthority", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initializeTokenBadge", + "accounts": [ + { + "name": "whirlpoolsConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "whirlpoolsConfigExtension", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenBadgeAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenMint", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenBadge", + "isMut": true, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "deleteTokenBadge", + "accounts": [ + { + "name": "whirlpoolsConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "whirlpoolsConfigExtension", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenBadgeAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenMint", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenBadge", + "isMut": true, + "isSigner": false + }, + { + "name": "receiver", + "isMut": true, + "isSigner": false + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "whirlpoolsConfigExtension", + "type": { + "kind": "struct", + "fields": [ + { + "name": "whirlpoolsConfig", + "type": "publicKey" + }, + { + "name": "configExtensionAuthority", + "type": "publicKey" + }, + { + "name": "tokenBadgeAuthority", + "type": "publicKey" + } + ] + } + }, + { + "name": "whirlpoolsConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "feeAuthority", + "type": "publicKey" + }, + { + "name": "collectProtocolFeesAuthority", + "type": "publicKey" + }, + { + "name": "rewardEmissionsSuperAuthority", + "type": "publicKey" + }, + { + "name": "defaultProtocolFeeRate", + "type": "u16" + } + ] + } + }, + { + "name": "feeTier", + "type": { + "kind": "struct", + "fields": [ + { + "name": "whirlpoolsConfig", + "type": "publicKey" + }, + { + "name": "tickSpacing", + "type": "u16" + }, + { + "name": "defaultFeeRate", + "type": "u16" + } + ] + } + }, + { + "name": "positionBundle", + "type": { + "kind": "struct", + "fields": [ + { + "name": "positionBundleMint", + "type": "publicKey" + }, + { + "name": "positionBitmap", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "position", + "type": { + "kind": "struct", + "fields": [ + { + "name": "whirlpool", + "type": "publicKey" + }, + { + "name": "positionMint", + "type": "publicKey" + }, + { + "name": "liquidity", + "type": "u128" + }, + { + "name": "tickLowerIndex", + "type": "i32" + }, + { + "name": "tickUpperIndex", + "type": "i32" + }, + { + "name": "feeGrowthCheckpointA", + "type": "u128" + }, + { + "name": "feeOwedA", + "type": "u64" + }, + { + "name": "feeGrowthCheckpointB", + "type": "u128" + }, + { + "name": "feeOwedB", + "type": "u64" + }, + { + "name": "rewardInfos", + "type": { + "array": [ + { + "defined": "PositionRewardInfo" + }, + 3 + ] + } + } + ] + } + }, + { + "name": "tickArray", + "type": { + "kind": "struct", + "fields": [ + { + "name": "startTickIndex", + "type": "i32" + }, + { + "name": "ticks", + "type": { + "array": [ + { + "defined": "Tick" + }, + 88 + ] + } + }, + { + "name": "whirlpool", + "type": "publicKey" + } + ] + } + }, + { + "name": "tokenBadge", + "type": { + "kind": "struct", + "fields": [ + { + "name": "whirlpoolsConfig", + "type": "publicKey" + }, + { + "name": "tokenMint", + "type": "publicKey" + } + ] + } + }, + { + "name": "whirlpool", + "type": { + "kind": "struct", + "fields": [ + { + "name": "whirlpoolsConfig", + "type": "publicKey" + }, + { + "name": "whirlpoolBump", + "type": { + "array": [ + "u8", + 1 + ] + } + }, + { + "name": "tickSpacing", + "type": "u16" + }, + { + "name": "tickSpacingSeed", + "type": { + "array": [ + "u8", + 2 + ] + } + }, + { + "name": "feeRate", + "type": "u16" + }, + { + "name": "protocolFeeRate", + "type": "u16" + }, + { + "name": "liquidity", + "type": "u128" + }, + { + "name": "sqrtPrice", + "type": "u128" + }, + { + "name": "tickCurrentIndex", + "type": "i32" + }, + { + "name": "protocolFeeOwedA", + "type": "u64" + }, + { + "name": "protocolFeeOwedB", + "type": "u64" + }, + { + "name": "tokenMintA", + "type": "publicKey" + }, + { "name": "tokenVaultA", "type": "publicKey" }, @@ -2239,6 +3450,40 @@ export type Whirlpool = { ] } }, + { + "name": "RemainingAccountsSlice", + "type": { + "kind": "struct", + "fields": [ + { + "name": "accountsType", + "type": { + "defined": "AccountsType" + } + }, + { + "name": "length", + "type": "u8" + } + ] + } + }, + { + "name": "RemainingAccountsInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "slices", + "type": { + "vec": { + "defined": "RemainingAccountsSlice" + } + } + } + ] + } + }, { "name": "CurrIndex", "type": { @@ -2283,6 +3528,32 @@ export type Whirlpool = { } ] } + }, + { + "name": "AccountsType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "TransferHookA" + }, + { + "name": "TransferHookB" + }, + { + "name": "TransferHookReward" + }, + { + "name": "TransferHookInput" + }, + { + "name": "TransferHookIntermediate" + }, + { + "name": "TransferHookOutput" + } + ] + } } ], "errors": [ @@ -2452,370 +3723,1414 @@ export type Whirlpool = { "msg": "Invalid div_u256 input" }, { - "code": 6033, - "name": "MultiplicationOverflow", - "msg": "Multiplication overflow" + "code": 6033, + "name": "MultiplicationOverflow", + "msg": "Multiplication overflow" + }, + { + "code": 6034, + "name": "InvalidSqrtPriceLimitDirection", + "msg": "Provided SqrtPriceLimit not in the same direction as the swap." + }, + { + "code": 6035, + "name": "ZeroTradableAmount", + "msg": "There are no tradable amount to swap." + }, + { + "code": 6036, + "name": "AmountOutBelowMinimum", + "msg": "Amount out below minimum threshold" + }, + { + "code": 6037, + "name": "AmountInAboveMaximum", + "msg": "Amount in above maximum threshold" + }, + { + "code": 6038, + "name": "TickArraySequenceInvalidIndex", + "msg": "Invalid index for tick array sequence" + }, + { + "code": 6039, + "name": "AmountCalcOverflow", + "msg": "Amount calculated overflows" + }, + { + "code": 6040, + "name": "AmountRemainingOverflow", + "msg": "Amount remaining overflows" + }, + { + "code": 6041, + "name": "InvalidIntermediaryMint", + "msg": "Invalid intermediary mint" + }, + { + "code": 6042, + "name": "DuplicateTwoHopPool", + "msg": "Duplicate two hop pool" + }, + { + "code": 6043, + "name": "InvalidBundleIndex", + "msg": "Bundle index is out of bounds" + }, + { + "code": 6044, + "name": "BundledPositionAlreadyOpened", + "msg": "Position has already been opened" + }, + { + "code": 6045, + "name": "BundledPositionAlreadyClosed", + "msg": "Position has already been closed" + }, + { + "code": 6046, + "name": "PositionBundleNotDeletable", + "msg": "Unable to delete PositionBundle with open positions" + }, + { + "code": 6047, + "name": "UnsupportedTokenMint", + "msg": "Token mint has unsupported attributes" + }, + { + "code": 6048, + "name": "RemainingAccountsInvalidSlice", + "msg": "Invalid remaining accounts" + }, + { + "code": 6049, + "name": "RemainingAccountsInsufficient", + "msg": "Insufficient remaining accounts" + }, + { + "code": 6050, + "name": "NoExtraAccountsForTransferHook", + "msg": "Unable to call transfer hook without extra accounts" + }, + { + "code": 6051, + "name": "IntermediateTokenAmountMismatch", + "msg": "Output and input amount mismatch" + }, + { + "code": 6052, + "name": "TransferFeeCalculationError", + "msg": "Transfer fee calculation failed" + }, + { + "code": 6053, + "name": "RemainingAccountsDuplicatedAccountsType", + "msg": "Same accounts type is provided more than once" + } + ] +}; + +export const IDL: Whirlpool = { + "version": "0.3.0", + "name": "whirlpool", + "instructions": [ + { + "name": "initializeConfig", + "docs": [ + "Initializes a WhirlpoolsConfig account that hosts info & authorities", + "required to govern a set of Whirlpools.", + "", + "### Parameters", + "- `fee_authority` - Authority authorized to initialize fee-tiers and set customs fees.", + "- `collect_protocol_fees_authority` - Authority authorized to collect protocol fees.", + "- `reward_emissions_super_authority` - Authority authorized to set reward authorities in pools." + ], + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": true + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "feeAuthority", + "type": "publicKey" + }, + { + "name": "collectProtocolFeesAuthority", + "type": "publicKey" + }, + { + "name": "rewardEmissionsSuperAuthority", + "type": "publicKey" + }, + { + "name": "defaultProtocolFeeRate", + "type": "u16" + } + ] + }, + { + "name": "initializePool", + "docs": [ + "Initializes a Whirlpool account.", + "Fee rate is set to the default values on the config and supplied fee_tier.", + "", + "### Parameters", + "- `bumps` - The bump value when deriving the PDA of the Whirlpool address.", + "- `tick_spacing` - The desired tick spacing for this pool.", + "- `initial_sqrt_price` - The desired initial sqrt-price for this pool", + "", + "#### Special Errors", + "`InvalidTokenMintOrder` - The order of mints have to be ordered by", + "`SqrtPriceOutOfBounds` - provided initial_sqrt_price is not between 2^-64 to 2^64", + "" + ], + "accounts": [ + { + "name": "whirlpoolsConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintB", + "isMut": false, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": true + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": true + }, + { + "name": "feeTier", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "bumps", + "type": { + "defined": "WhirlpoolBumps" + } + }, + { + "name": "tickSpacing", + "type": "u16" + }, + { + "name": "initialSqrtPrice", + "type": "u128" + } + ] }, { - "code": 6034, - "name": "InvalidSqrtPriceLimitDirection", - "msg": "Provided SqrtPriceLimit not in the same direction as the swap." + "name": "initializeTickArray", + "docs": [ + "Initializes a tick_array account to represent a tick-range in a Whirlpool.", + "", + "### Parameters", + "- `start_tick_index` - The starting tick index for this tick-array.", + "Has to be a multiple of TickArray size & the tick spacing of this pool.", + "", + "#### Special Errors", + "- `InvalidStartTick` - if the provided start tick is out of bounds or is not a multiple of", + "TICK_ARRAY_SIZE * tick spacing." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": false, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "tickArray", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "startTickIndex", + "type": "i32" + } + ] }, { - "code": 6035, - "name": "ZeroTradableAmount", - "msg": "There are no tradable amount to swap." + "name": "initializeFeeTier", + "docs": [ + "Initializes a fee_tier account usable by Whirlpools in a WhirlpoolConfig space.", + "", + "### Authority", + "- \"fee_authority\" - Set authority in the WhirlpoolConfig", + "", + "### Parameters", + "- `tick_spacing` - The tick-spacing that this fee-tier suggests the default_fee_rate for.", + "- `default_fee_rate` - The default fee rate that a pool will use if the pool uses this", + "fee tier during initialization.", + "", + "#### Special Errors", + "- `FeeRateMaxExceeded` - If the provided default_fee_rate exceeds MAX_FEE_RATE." + ], + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "feeTier", + "isMut": true, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "feeAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "tickSpacing", + "type": "u16" + }, + { + "name": "defaultFeeRate", + "type": "u16" + } + ] }, { - "code": 6036, - "name": "AmountOutBelowMinimum", - "msg": "Amount out below minimum threshold" + "name": "initializeReward", + "docs": [ + "Initialize reward for a Whirlpool. A pool can only support up to a set number of rewards.", + "", + "### Authority", + "- \"reward_authority\" - assigned authority by the reward_super_authority for the specified", + "reward-index in this Whirlpool", + "", + "### Parameters", + "- `reward_index` - The reward index that we'd like to initialize. (0 <= index <= NUM_REWARDS)", + "", + "#### Special Errors", + "- `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized", + "index in this pool, or exceeds NUM_REWARDS, or", + "all reward slots for this pool has been initialized." + ], + "accounts": [ + { + "name": "rewardAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "rewardMint", + "isMut": false, + "isSigner": false + }, + { + "name": "rewardVault", + "isMut": true, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "rewardIndex", + "type": "u8" + } + ] }, { - "code": 6037, - "name": "AmountInAboveMaximum", - "msg": "Amount in above maximum threshold" + "name": "setRewardEmissions", + "docs": [ + "Set the reward emissions for a reward in a Whirlpool.", + "", + "### Authority", + "- \"reward_authority\" - assigned authority by the reward_super_authority for the specified", + "reward-index in this Whirlpool", + "", + "### Parameters", + "- `reward_index` - The reward index (0 <= index <= NUM_REWARDS) that we'd like to modify.", + "- `emissions_per_second_x64` - The amount of rewards emitted in this pool.", + "", + "#### Special Errors", + "- `RewardVaultAmountInsufficient` - The amount of rewards in the reward vault cannot emit", + "more than a day of desired emissions.", + "- `InvalidTimestamp` - Provided timestamp is not in order with the previous timestamp.", + "- `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized", + "index in this pool, or exceeds NUM_REWARDS, or", + "all reward slots for this pool has been initialized." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "rewardAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "rewardVault", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "rewardIndex", + "type": "u8" + }, + { + "name": "emissionsPerSecondX64", + "type": "u128" + } + ] }, { - "code": 6038, - "name": "TickArraySequenceInvalidIndex", - "msg": "Invalid index for tick array sequence" + "name": "openPosition", + "docs": [ + "Open a position in a Whirlpool. A unique token will be minted to represent the position", + "in the users wallet. The position will start off with 0 liquidity.", + "", + "### Parameters", + "- `tick_lower_index` - The tick specifying the lower end of the position range.", + "- `tick_upper_index` - The tick specifying the upper end of the position range.", + "", + "#### Special Errors", + "- `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of", + "the tick-spacing in this pool." + ], + "accounts": [ + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "owner", + "isMut": false, + "isSigner": false + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "positionMint", + "isMut": true, + "isSigner": true + }, + { + "name": "positionTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "whirlpool", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "bumps", + "type": { + "defined": "OpenPositionBumps" + } + }, + { + "name": "tickLowerIndex", + "type": "i32" + }, + { + "name": "tickUpperIndex", + "type": "i32" + } + ] }, { - "code": 6039, - "name": "AmountCalcOverflow", - "msg": "Amount calculated overflows" + "name": "openPositionWithMetadata", + "docs": [ + "Open a position in a Whirlpool. A unique token will be minted to represent the position", + "in the users wallet. Additional Metaplex metadata is appended to identify the token.", + "The position will start off with 0 liquidity.", + "", + "### Parameters", + "- `tick_lower_index` - The tick specifying the lower end of the position range.", + "- `tick_upper_index` - The tick specifying the upper end of the position range.", + "", + "#### Special Errors", + "- `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of", + "the tick-spacing in this pool." + ], + "accounts": [ + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "owner", + "isMut": false, + "isSigner": false + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "positionMint", + "isMut": true, + "isSigner": true + }, + { + "name": "positionMetadataAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "https://github.com/metaplex-foundation/metaplex-program-library/blob/master/token-metadata/program/src/utils.rs#L873" + ] + }, + { + "name": "positionTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "whirlpool", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "metadataProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "metadataUpdateAuth", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "bumps", + "type": { + "defined": "OpenPositionWithMetadataBumps" + } + }, + { + "name": "tickLowerIndex", + "type": "i32" + }, + { + "name": "tickUpperIndex", + "type": "i32" + } + ] }, { - "code": 6040, - "name": "AmountRemainingOverflow", - "msg": "Amount remaining overflows" + "name": "increaseLiquidity", + "docs": [ + "Add liquidity to a position in the Whirlpool. This call also updates the position's accrued fees and rewards.", + "", + "### Authority", + "- `position_authority` - authority that owns the token corresponding to this desired position.", + "", + "### Parameters", + "- `liquidity_amount` - The total amount of Liquidity the user is willing to deposit.", + "- `token_max_a` - The maximum amount of tokenA the user is willing to deposit.", + "- `token_max_b` - The maximum amount of tokenB the user is willing to deposit.", + "", + "#### Special Errors", + "- `LiquidityZero` - Provided liquidity amount is zero.", + "- `LiquidityTooHigh` - Provided liquidity exceeds u128::max.", + "- `TokenMaxExceeded` - The required token to perform this operation exceeds the user defined amount." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "positionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "positionTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenOwnerAccountA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenOwnerAccountB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayLower", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayUpper", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "liquidityAmount", + "type": "u128" + }, + { + "name": "tokenMaxA", + "type": "u64" + }, + { + "name": "tokenMaxB", + "type": "u64" + } + ] }, { - "code": 6041, - "name": "InvalidIntermediaryMint", - "msg": "Invalid intermediary mint" + "name": "decreaseLiquidity", + "docs": [ + "Withdraw liquidity from a position in the Whirlpool. This call also updates the position's accrued fees and rewards.", + "", + "### Authority", + "- `position_authority` - authority that owns the token corresponding to this desired position.", + "", + "### Parameters", + "- `liquidity_amount` - The total amount of Liquidity the user desires to withdraw.", + "- `token_min_a` - The minimum amount of tokenA the user is willing to withdraw.", + "- `token_min_b` - The minimum amount of tokenB the user is willing to withdraw.", + "", + "#### Special Errors", + "- `LiquidityZero` - Provided liquidity amount is zero.", + "- `LiquidityTooHigh` - Provided liquidity exceeds u128::max.", + "- `TokenMinSubceeded` - The required token to perform this operation subceeds the user defined amount." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "positionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "positionTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenOwnerAccountA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenOwnerAccountB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayLower", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayUpper", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "liquidityAmount", + "type": "u128" + }, + { + "name": "tokenMinA", + "type": "u64" + }, + { + "name": "tokenMinB", + "type": "u64" + } + ] }, { - "code": 6042, - "name": "DuplicateTwoHopPool", - "msg": "Duplicate two hop pool" + "name": "updateFeesAndRewards", + "docs": [ + "Update the accrued fees and rewards for a position.", + "", + "#### Special Errors", + "- `TickNotFound` - Provided tick array account does not contain the tick for this position.", + "- `LiquidityZero` - Position has zero liquidity and therefore already has the most updated fees and reward values." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayLower", + "isMut": false, + "isSigner": false + }, + { + "name": "tickArrayUpper", + "isMut": false, + "isSigner": false + } + ], + "args": [] }, { - "code": 6043, - "name": "InvalidBundleIndex", - "msg": "Bundle index is out of bounds" + "name": "collectFees", + "docs": [ + "Collect fees accrued for this position.", + "", + "### Authority", + "- `position_authority` - authority that owns the token corresponding to this desired position." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": false, + "isSigner": false + }, + { + "name": "positionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "positionTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenOwnerAccountA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenOwnerAccountB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] }, { - "code": 6044, - "name": "BundledPositionAlreadyOpened", - "msg": "Position has already been opened" + "name": "collectReward", + "docs": [ + "Collect rewards accrued for this position.", + "", + "### Authority", + "- `position_authority` - authority that owns the token corresponding to this desired position." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": false, + "isSigner": false + }, + { + "name": "positionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "positionTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "rewardOwnerAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "rewardVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "rewardIndex", + "type": "u8" + } + ] }, { - "code": 6045, - "name": "BundledPositionAlreadyClosed", - "msg": "Position has already been closed" + "name": "collectProtocolFees", + "docs": [ + "Collect the protocol fees accrued in this Whirlpool", + "", + "### Authority", + "- `collect_protocol_fees_authority` - assigned authority in the WhirlpoolConfig that can collect protocol fees" + ], + "accounts": [ + { + "name": "whirlpoolsConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "collectProtocolFeesAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenDestinationA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenDestinationB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] }, { - "code": 6046, - "name": "PositionBundleNotDeletable", - "msg": "Unable to delete PositionBundle with open positions" - } - ] -}; - -export const IDL: Whirlpool = { - "version": "0.2.0", - "name": "whirlpool", - "instructions": [ - { - "name": "initializeConfig", + "name": "swap", "docs": [ - "Initializes a WhirlpoolsConfig account that hosts info & authorities", - "required to govern a set of Whirlpools.", + "Perform a swap in this Whirlpool", + "", + "### Authority", + "- \"token_authority\" - The authority to withdraw tokens from the input token account.", "", "### Parameters", - "- `fee_authority` - Authority authorized to initialize fee-tiers and set customs fees.", - "- `collect_protocol_fees_authority` - Authority authorized to collect protocol fees.", - "- `reward_emissions_super_authority` - Authority authorized to set reward authorities in pools." + "- `amount` - The amount of input or output token to swap from (depending on amount_specified_is_input).", + "- `other_amount_threshold` - The maximum/minimum of input/output token to swap into (depending on amount_specified_is_input).", + "- `sqrt_price_limit` - The maximum/minimum price the swap will swap to.", + "- `amount_specified_is_input` - Specifies the token the parameter `amount`represents. If true, the amount represents the input token of the swap.", + "- `a_to_b` - The direction of the swap. True if swapping from A to B. False if swapping from B to A.", + "", + "#### Special Errors", + "- `ZeroTradableAmount` - User provided parameter `amount` is 0.", + "- `InvalidSqrtPriceLimitDirection` - User provided parameter `sqrt_price_limit` does not match the direction of the trade.", + "- `SqrtPriceOutOfBounds` - User provided parameter `sqrt_price_limit` is over Whirlppool's max/min bounds for sqrt-price.", + "- `InvalidTickArraySequence` - User provided tick-arrays are not in sequential order required to proceed in this trade direction.", + "- `TickArraySequenceInvalidIndex` - The swap loop attempted to access an invalid array index during the query of the next initialized tick.", + "- `TickArrayIndexOutofBounds` - The swap loop attempted to access an invalid array index during tick crossing.", + "- `LiquidityOverflow` - Liquidity value overflowed 128bits during tick crossing.", + "- `InvalidTickSpacing` - The swap pool was initialized with tick-spacing of 0." ], "accounts": [ { - "name": "config", - "isMut": true, + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, "isSigner": true }, { - "name": "funder", + "name": "whirlpool", "isMut": true, - "isSigner": true + "isSigner": false }, { - "name": "systemProgram", + "name": "tokenOwnerAccountA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenOwnerAccountB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultB", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArray0", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArray1", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArray2", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", "isMut": false, "isSigner": false } ], "args": [ { - "name": "feeAuthority", - "type": "publicKey" + "name": "amount", + "type": "u64" }, { - "name": "collectProtocolFeesAuthority", - "type": "publicKey" + "name": "otherAmountThreshold", + "type": "u64" }, { - "name": "rewardEmissionsSuperAuthority", - "type": "publicKey" + "name": "sqrtPriceLimit", + "type": "u128" }, { - "name": "defaultProtocolFeeRate", - "type": "u16" + "name": "amountSpecifiedIsInput", + "type": "bool" + }, + { + "name": "aToB", + "type": "bool" } ] }, { - "name": "initializePool", + "name": "closePosition", "docs": [ - "Initializes a Whirlpool account.", - "Fee rate is set to the default values on the config and supplied fee_tier.", + "Close a position in a Whirlpool. Burns the position token in the owner's wallet.", "", - "### Parameters", - "- `bumps` - The bump value when deriving the PDA of the Whirlpool address.", - "- `tick_spacing` - The desired tick spacing for this pool.", - "- `initial_sqrt_price` - The desired initial sqrt-price for this pool", + "### Authority", + "- \"position_authority\" - The authority that owns the position token.", "", "#### Special Errors", - "`InvalidTokenMintOrder` - The order of mints have to be ordered by", - "`SqrtPriceOutOfBounds` - provided initial_sqrt_price is not between 2^-64 to 2^64", - "" + "- `ClosePositionNotEmpty` - The provided position account is not empty." ], "accounts": [ { - "name": "whirlpoolsConfig", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenMintA", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenMintB", + "name": "positionAuthority", "isMut": false, - "isSigner": false + "isSigner": true }, { - "name": "funder", + "name": "receiver", "isMut": true, - "isSigner": true + "isSigner": false }, { - "name": "whirlpool", + "name": "position", "isMut": true, "isSigner": false }, { - "name": "tokenVaultA", + "name": "positionMint", "isMut": true, - "isSigner": true + "isSigner": false }, { - "name": "tokenVaultB", + "name": "positionTokenAccount", "isMut": true, - "isSigner": true + "isSigner": false }, { - "name": "feeTier", + "name": "tokenProgram", "isMut": false, "isSigner": false - }, + } + ], + "args": [] + }, + { + "name": "setDefaultFeeRate", + "docs": [ + "Set the default_fee_rate for a FeeTier", + "Only the current fee authority has permission to invoke this instruction.", + "", + "### Authority", + "- \"fee_authority\" - Set authority in the WhirlpoolConfig", + "", + "### Parameters", + "- `default_fee_rate` - The default fee rate that a pool will use if the pool uses this", + "fee tier during initialization.", + "", + "#### Special Errors", + "- `FeeRateMaxExceeded` - If the provided default_fee_rate exceeds MAX_FEE_RATE." + ], + "accounts": [ { - "name": "tokenProgram", + "name": "whirlpoolsConfig", "isMut": false, "isSigner": false }, { - "name": "systemProgram", - "isMut": false, + "name": "feeTier", + "isMut": true, "isSigner": false }, { - "name": "rent", + "name": "feeAuthority", "isMut": false, - "isSigner": false + "isSigner": true } ], "args": [ { - "name": "bumps", - "type": { - "defined": "WhirlpoolBumps" - } - }, - { - "name": "tickSpacing", + "name": "defaultFeeRate", "type": "u16" - }, - { - "name": "initialSqrtPrice", - "type": "u128" } ] }, { - "name": "initializeTickArray", + "name": "setDefaultProtocolFeeRate", "docs": [ - "Initializes a tick_array account to represent a tick-range in a Whirlpool.", + "Sets the default protocol fee rate for a WhirlpoolConfig", + "Protocol fee rate is represented as a basis point.", + "Only the current fee authority has permission to invoke this instruction.", + "", + "### Authority", + "- \"fee_authority\" - Set authority that can modify pool fees in the WhirlpoolConfig", "", "### Parameters", - "- `start_tick_index` - The starting tick index for this tick-array.", - "Has to be a multiple of TickArray size & the tick spacing of this pool.", + "- `default_protocol_fee_rate` - Rate that is referenced during the initialization of a Whirlpool using this config.", "", "#### Special Errors", - "- `InvalidStartTick` - if the provided start tick is out of bounds or is not a multiple of", - "TICK_ARRAY_SIZE * tick spacing." + "- `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE." ], "accounts": [ { - "name": "whirlpool", - "isMut": false, - "isSigner": false - }, - { - "name": "funder", - "isMut": true, - "isSigner": true - }, - { - "name": "tickArray", + "name": "whirlpoolsConfig", "isMut": true, "isSigner": false }, { - "name": "systemProgram", + "name": "feeAuthority", "isMut": false, - "isSigner": false + "isSigner": true } ], "args": [ { - "name": "startTickIndex", - "type": "i32" + "name": "defaultProtocolFeeRate", + "type": "u16" } ] }, { - "name": "initializeFeeTier", + "name": "setFeeRate", "docs": [ - "Initializes a fee_tier account usable by Whirlpools in a WhirlpoolConfig space.", + "Sets the fee rate for a Whirlpool.", + "Fee rate is represented as hundredths of a basis point.", + "Only the current fee authority has permission to invoke this instruction.", "", "### Authority", - "- \"fee_authority\" - Set authority in the WhirlpoolConfig", + "- \"fee_authority\" - Set authority that can modify pool fees in the WhirlpoolConfig", "", "### Parameters", - "- `tick_spacing` - The tick-spacing that this fee-tier suggests the default_fee_rate for.", - "- `default_fee_rate` - The default fee rate that a pool will use if the pool uses this", - "fee tier during initialization.", + "- `fee_rate` - The rate that the pool will use to calculate fees going onwards.", "", "#### Special Errors", - "- `FeeRateMaxExceeded` - If the provided default_fee_rate exceeds MAX_FEE_RATE." + "- `FeeRateMaxExceeded` - If the provided fee_rate exceeds MAX_FEE_RATE." ], "accounts": [ { - "name": "config", + "name": "whirlpoolsConfig", "isMut": false, "isSigner": false }, { - "name": "feeTier", + "name": "whirlpool", "isMut": true, "isSigner": false }, - { - "name": "funder", - "isMut": true, - "isSigner": true - }, { "name": "feeAuthority", "isMut": false, "isSigner": true - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false } ], "args": [ { - "name": "tickSpacing", - "type": "u16" - }, - { - "name": "defaultFeeRate", + "name": "feeRate", "type": "u16" } ] }, { - "name": "initializeReward", + "name": "setProtocolFeeRate", "docs": [ - "Initialize reward for a Whirlpool. A pool can only support up to a set number of rewards.", + "Sets the protocol fee rate for a Whirlpool.", + "Protocol fee rate is represented as a basis point.", + "Only the current fee authority has permission to invoke this instruction.", "", "### Authority", - "- \"reward_authority\" - assigned authority by the reward_super_authority for the specified", - "reward-index in this Whirlpool", + "- \"fee_authority\" - Set authority that can modify pool fees in the WhirlpoolConfig", "", "### Parameters", - "- `reward_index` - The reward index that we'd like to initialize. (0 <= index <= NUM_REWARDS)", + "- `protocol_fee_rate` - The rate that the pool will use to calculate protocol fees going onwards.", "", "#### Special Errors", - "- `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized", - "index in this pool, or exceeds NUM_REWARDS, or", - "all reward slots for this pool has been initialized." + "- `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE." ], "accounts": [ { - "name": "rewardAuthority", + "name": "whirlpoolsConfig", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "funder", + "name": "whirlpool", "isMut": true, - "isSigner": true + "isSigner": false }, { - "name": "whirlpool", + "name": "feeAuthority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "protocolFeeRate", + "type": "u16" + } + ] + }, + { + "name": "setFeeAuthority", + "docs": [ + "Sets the fee authority for a WhirlpoolConfig.", + "The fee authority can set the fee & protocol fee rate for individual pools or", + "set the default fee rate for newly minted pools.", + "Only the current fee authority has permission to invoke this instruction.", + "", + "### Authority", + "- \"fee_authority\" - Set authority that can modify pool fees in the WhirlpoolConfig" + ], + "accounts": [ + { + "name": "whirlpoolsConfig", "isMut": true, "isSigner": false }, { - "name": "rewardMint", + "name": "feeAuthority", "isMut": false, - "isSigner": false + "isSigner": true }, { - "name": "rewardVault", + "name": "newFeeAuthority", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setCollectProtocolFeesAuthority", + "docs": [ + "Sets the fee authority to collect protocol fees for a WhirlpoolConfig.", + "Only the current collect protocol fee authority has permission to invoke this instruction.", + "", + "### Authority", + "- \"fee_authority\" - Set authority that can collect protocol fees in the WhirlpoolConfig" + ], + "accounts": [ + { + "name": "whirlpoolsConfig", "isMut": true, + "isSigner": false + }, + { + "name": "collectProtocolFeesAuthority", + "isMut": false, "isSigner": true }, { - "name": "tokenProgram", + "name": "newCollectProtocolFeesAuthority", "isMut": false, "isSigner": false + } + ], + "args": [] + }, + { + "name": "setRewardAuthority", + "docs": [ + "Set the whirlpool reward authority at the provided `reward_index`.", + "Only the current reward authority for this reward index has permission to invoke this instruction.", + "", + "### Authority", + "- \"reward_authority\" - Set authority that can control reward emission for this particular reward.", + "", + "#### Special Errors", + "- `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized", + "index in this pool, or exceeds NUM_REWARDS, or", + "all reward slots for this pool has been initialized." + ], + "accounts": [ + { + "name": "whirlpool", + "isMut": true, + "isSigner": false }, { - "name": "systemProgram", + "name": "rewardAuthority", "isMut": false, - "isSigner": false + "isSigner": true }, { - "name": "rent", + "name": "newRewardAuthority", "isMut": false, "isSigner": false } @@ -2828,39 +5143,37 @@ export const IDL: Whirlpool = { ] }, { - "name": "setRewardEmissions", + "name": "setRewardAuthorityBySuperAuthority", "docs": [ - "Set the reward emissions for a reward in a Whirlpool.", + "Set the whirlpool reward authority at the provided `reward_index`.", + "Only the current reward super authority has permission to invoke this instruction.", "", "### Authority", - "- \"reward_authority\" - assigned authority by the reward_super_authority for the specified", - "reward-index in this Whirlpool", - "", - "### Parameters", - "- `reward_index` - The reward index (0 <= index <= NUM_REWARDS) that we'd like to modify.", - "- `emissions_per_second_x64` - The amount of rewards emitted in this pool.", + "- \"reward_authority\" - Set authority that can control reward emission for this particular reward.", "", "#### Special Errors", - "- `RewardVaultAmountInsufficient` - The amount of rewards in the reward vault cannot emit", - "more than a day of desired emissions.", - "- `InvalidTimestamp` - Provided timestamp is not in order with the previous timestamp.", "- `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized", "index in this pool, or exceeds NUM_REWARDS, or", "all reward slots for this pool has been initialized." ], "accounts": [ + { + "name": "whirlpoolsConfig", + "isMut": false, + "isSigner": false + }, { "name": "whirlpool", "isMut": true, "isSigner": false }, { - "name": "rewardAuthority", + "name": "rewardEmissionsSuperAuthority", "isMut": false, "isSigner": true }, { - "name": "rewardVault", + "name": "newRewardAuthority", "isMut": false, "isSigner": false } @@ -2869,147 +5182,298 @@ export const IDL: Whirlpool = { { "name": "rewardIndex", "type": "u8" + } + ] + }, + { + "name": "setRewardEmissionsSuperAuthority", + "docs": [ + "Set the whirlpool reward super authority for a WhirlpoolConfig", + "Only the current reward super authority has permission to invoke this instruction.", + "This instruction will not change the authority on any `WhirlpoolRewardInfo` whirlpool rewards.", + "", + "### Authority", + "- \"reward_emissions_super_authority\" - Set authority that can control reward authorities for all pools in this config space." + ], + "accounts": [ + { + "name": "whirlpoolsConfig", + "isMut": true, + "isSigner": false }, { - "name": "emissionsPerSecondX64", - "type": "u128" + "name": "rewardEmissionsSuperAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "newRewardEmissionsSuperAuthority", + "isMut": false, + "isSigner": false } - ] + ], + "args": [] }, { - "name": "openPosition", + "name": "twoHopSwap", "docs": [ - "Open a position in a Whirlpool. A unique token will be minted to represent the position", - "in the users wallet. The position will start off with 0 liquidity.", + "Perform a two-hop swap in this Whirlpool", + "", + "### Authority", + "- \"token_authority\" - The authority to withdraw tokens from the input token account.", "", "### Parameters", - "- `tick_lower_index` - The tick specifying the lower end of the position range.", - "- `tick_upper_index` - The tick specifying the upper end of the position range.", + "- `amount` - The amount of input or output token to swap from (depending on amount_specified_is_input).", + "- `other_amount_threshold` - The maximum/minimum of input/output token to swap into (depending on amount_specified_is_input).", + "- `amount_specified_is_input` - Specifies the token the parameter `amount`represents. If true, the amount represents the input token of the swap.", + "- `a_to_b_one` - The direction of the swap of hop one. True if swapping from A to B. False if swapping from B to A.", + "- `a_to_b_two` - The direction of the swap of hop two. True if swapping from A to B. False if swapping from B to A.", + "- `sqrt_price_limit_one` - The maximum/minimum price the swap will swap to in the first hop.", + "- `sqrt_price_limit_two` - The maximum/minimum price the swap will swap to in the second hop.", "", "#### Special Errors", - "- `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of", - "the tick-spacing in this pool." + "- `ZeroTradableAmount` - User provided parameter `amount` is 0.", + "- `InvalidSqrtPriceLimitDirection` - User provided parameter `sqrt_price_limit` does not match the direction of the trade.", + "- `SqrtPriceOutOfBounds` - User provided parameter `sqrt_price_limit` is over Whirlppool's max/min bounds for sqrt-price.", + "- `InvalidTickArraySequence` - User provided tick-arrays are not in sequential order required to proceed in this trade direction.", + "- `TickArraySequenceInvalidIndex` - The swap loop attempted to access an invalid array index during the query of the next initialized tick.", + "- `TickArrayIndexOutofBounds` - The swap loop attempted to access an invalid array index during tick crossing.", + "- `LiquidityOverflow` - Liquidity value overflowed 128bits during tick crossing.", + "- `InvalidTickSpacing` - The swap pool was initialized with tick-spacing of 0.", + "- `InvalidIntermediaryMint` - Error if the intermediary mint between hop one and two do not equal.", + "- `DuplicateTwoHopPool` - Error if whirlpool one & two are the same pool." ], "accounts": [ { - "name": "funder", - "isMut": true, - "isSigner": true + "name": "tokenProgram", + "isMut": false, + "isSigner": false }, { - "name": "owner", + "name": "tokenAuthority", "isMut": false, + "isSigner": true + }, + { + "name": "whirlpoolOne", + "isMut": true, "isSigner": false }, { - "name": "position", + "name": "whirlpoolTwo", "isMut": true, "isSigner": false }, { - "name": "positionMint", + "name": "tokenOwnerAccountOneA", "isMut": true, - "isSigner": true + "isSigner": false }, { - "name": "positionTokenAccount", + "name": "tokenVaultOneA", "isMut": true, "isSigner": false }, { - "name": "whirlpool", - "isMut": false, + "name": "tokenOwnerAccountOneB", + "isMut": true, "isSigner": false }, { - "name": "tokenProgram", - "isMut": false, + "name": "tokenVaultOneB", + "isMut": true, "isSigner": false }, { - "name": "systemProgram", - "isMut": false, + "name": "tokenOwnerAccountTwoA", + "isMut": true, "isSigner": false }, { - "name": "rent", + "name": "tokenVaultTwoA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenOwnerAccountTwoB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultTwoB", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayOne0", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayOne1", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayOne2", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayTwo0", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayTwo1", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArrayTwo2", + "isMut": true, + "isSigner": false + }, + { + "name": "oracleOne", "isMut": false, "isSigner": false }, { - "name": "associatedTokenProgram", + "name": "oracleTwo", "isMut": false, "isSigner": false } ], "args": [ { - "name": "bumps", - "type": { - "defined": "OpenPositionBumps" - } + "name": "amount", + "type": "u64" }, { - "name": "tickLowerIndex", - "type": "i32" + "name": "otherAmountThreshold", + "type": "u64" }, { - "name": "tickUpperIndex", - "type": "i32" + "name": "amountSpecifiedIsInput", + "type": "bool" + }, + { + "name": "aToBOne", + "type": "bool" + }, + { + "name": "aToBTwo", + "type": "bool" + }, + { + "name": "sqrtPriceLimitOne", + "type": "u128" + }, + { + "name": "sqrtPriceLimitTwo", + "type": "u128" } ] }, { - "name": "openPositionWithMetadata", + "name": "initializePositionBundle", "docs": [ - "Open a position in a Whirlpool. A unique token will be minted to represent the position", - "in the users wallet. Additional Metaplex metadata is appended to identify the token.", - "The position will start off with 0 liquidity.", - "", - "### Parameters", - "- `tick_lower_index` - The tick specifying the lower end of the position range.", - "- `tick_upper_index` - The tick specifying the upper end of the position range.", - "", - "#### Special Errors", - "- `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of", - "the tick-spacing in this pool." + "Initializes a PositionBundle account that bundles several positions.", + "A unique token will be minted to represent the position bundle in the users wallet." ], "accounts": [ + { + "name": "positionBundle", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleMint", + "isMut": true, + "isSigner": true + }, + { + "name": "positionBundleTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleOwner", + "isMut": false, + "isSigner": false + }, { "name": "funder", "isMut": true, "isSigner": true }, { - "name": "owner", + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", "isMut": false, "isSigner": false - }, + } + ], + "args": [] + }, + { + "name": "initializePositionBundleWithMetadata", + "docs": [ + "Initializes a PositionBundle account that bundles several positions.", + "A unique token will be minted to represent the position bundle in the users wallet.", + "Additional Metaplex metadata is appended to identify the token." + ], + "accounts": [ { - "name": "position", + "name": "positionBundle", "isMut": true, "isSigner": false }, { - "name": "positionMint", + "name": "positionBundleMint", "isMut": true, "isSigner": true }, { - "name": "positionMetadataAccount", + "name": "positionBundleMetadata", "isMut": true, "isSigner": false, "docs": [ - "https://github.com/metaplex-foundation/metaplex-program-library/blob/master/token-metadata/program/src/utils.rs#L873" + "https://github.com/metaplex-foundation/metaplex-program-library/blob/773a574c4b34e5b9f248a81306ec24db064e255f/token-metadata/program/src/utils/metadata.rs#L100" ] }, { - "name": "positionTokenAccount", + "name": "positionBundleTokenAccount", "isMut": true, "isSigner": false }, { - "name": "whirlpool", + "name": "positionBundleOwner", + "isMut": false, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "metadataUpdateAuth", "isMut": false, "isSigner": false }, @@ -3037,302 +5501,350 @@ export const IDL: Whirlpool = { "name": "metadataProgram", "isMut": false, "isSigner": false - }, - { - "name": "metadataUpdateAuth", - "isMut": false, - "isSigner": false } ], - "args": [ - { - "name": "bumps", - "type": { - "defined": "OpenPositionWithMetadataBumps" - } - }, - { - "name": "tickLowerIndex", - "type": "i32" - }, - { - "name": "tickUpperIndex", - "type": "i32" - } - ] + "args": [] }, { - "name": "increaseLiquidity", + "name": "deletePositionBundle", "docs": [ - "Add liquidity to a position in the Whirlpool. This call also updates the position's accrued fees and rewards.", + "Delete a PositionBundle account. Burns the position bundle token in the owner's wallet.", "", "### Authority", - "- `position_authority` - authority that owns the token corresponding to this desired position.", - "", - "### Parameters", - "- `liquidity_amount` - The total amount of Liquidity the user is willing to deposit.", - "- `token_max_a` - The maximum amount of tokenA the user is willing to deposit.", - "- `token_max_b` - The maximum amount of tokenB the user is willing to deposit.", + "- `position_bundle_owner` - The owner that owns the position bundle token.", "", - "#### Special Errors", - "- `LiquidityZero` - Provided liquidity amount is zero.", - "- `LiquidityTooHigh` - Provided liquidity exceeds u128::max.", - "- `TokenMaxExceeded` - The required token to perform this operation exceeds the user defined amount." + "### Special Errors", + "- `PositionBundleNotDeletable` - The provided position bundle has open positions." ], "accounts": [ { - "name": "whirlpool", + "name": "positionBundle", "isMut": true, "isSigner": false }, { - "name": "tokenProgram", - "isMut": false, + "name": "positionBundleMint", + "isMut": true, "isSigner": false }, { - "name": "positionAuthority", + "name": "positionBundleTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleOwner", "isMut": false, "isSigner": true }, { - "name": "position", + "name": "receiver", "isMut": true, "isSigner": false }, { - "name": "positionTokenAccount", + "name": "tokenProgram", "isMut": false, "isSigner": false - }, + } + ], + "args": [] + }, + { + "name": "openBundledPosition", + "docs": [ + "Open a bundled position in a Whirlpool. No new tokens are issued", + "because the owner of the position bundle becomes the owner of the position.", + "The position will start off with 0 liquidity.", + "", + "### Authority", + "- `position_bundle_authority` - authority that owns the token corresponding to this desired position bundle.", + "", + "### Parameters", + "- `bundle_index` - The bundle index that we'd like to open.", + "- `tick_lower_index` - The tick specifying the lower end of the position range.", + "- `tick_upper_index` - The tick specifying the upper end of the position range.", + "", + "#### Special Errors", + "- `InvalidBundleIndex` - If the provided bundle index is out of bounds.", + "- `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of", + "the tick-spacing in this pool." + ], + "accounts": [ { - "name": "tokenOwnerAccountA", + "name": "bundledPosition", "isMut": true, "isSigner": false }, { - "name": "tokenOwnerAccountB", + "name": "positionBundle", "isMut": true, "isSigner": false }, { - "name": "tokenVaultA", - "isMut": true, + "name": "positionBundleTokenAccount", + "isMut": false, "isSigner": false }, { - "name": "tokenVaultB", - "isMut": true, + "name": "positionBundleAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "whirlpool", + "isMut": false, "isSigner": false }, { - "name": "tickArrayLower", + "name": "funder", "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, "isSigner": false }, { - "name": "tickArrayUpper", - "isMut": true, + "name": "rent", + "isMut": false, "isSigner": false } ], "args": [ { - "name": "liquidityAmount", - "type": "u128" + "name": "bundleIndex", + "type": "u16" }, { - "name": "tokenMaxA", - "type": "u64" + "name": "tickLowerIndex", + "type": "i32" }, { - "name": "tokenMaxB", - "type": "u64" + "name": "tickUpperIndex", + "type": "i32" } ] }, { - "name": "decreaseLiquidity", + "name": "closeBundledPosition", "docs": [ - "Withdraw liquidity from a position in the Whirlpool. This call also updates the position's accrued fees and rewards.", + "Close a bundled position in a Whirlpool.", "", "### Authority", - "- `position_authority` - authority that owns the token corresponding to this desired position.", + "- `position_bundle_authority` - authority that owns the token corresponding to this desired position bundle.", "", "### Parameters", - "- `liquidity_amount` - The total amount of Liquidity the user desires to withdraw.", - "- `token_min_a` - The minimum amount of tokenA the user is willing to withdraw.", - "- `token_min_b` - The minimum amount of tokenB the user is willing to withdraw.", + "- `bundle_index` - The bundle index that we'd like to close.", "", "#### Special Errors", - "- `LiquidityZero` - Provided liquidity amount is zero.", - "- `LiquidityTooHigh` - Provided liquidity exceeds u128::max.", - "- `TokenMinSubceeded` - The required token to perform this operation subceeds the user defined amount." + "- `InvalidBundleIndex` - If the provided bundle index is out of bounds.", + "- `ClosePositionNotEmpty` - The provided position account is not empty." ], "accounts": [ { - "name": "whirlpool", + "name": "bundledPosition", "isMut": true, "isSigner": false }, { - "name": "tokenProgram", + "name": "positionBundle", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleTokenAccount", "isMut": false, "isSigner": false }, { - "name": "positionAuthority", + "name": "positionBundleAuthority", "isMut": false, "isSigner": true }, { - "name": "position", + "name": "receiver", "isMut": true, "isSigner": false - }, + } + ], + "args": [ { - "name": "positionTokenAccount", + "name": "bundleIndex", + "type": "u16" + } + ] + }, + { + "name": "collectFeesV2", + "docs": [ + "Collect fees accrued for this position.", + "", + "### Authority", + "- `position_authority` - authority that owns the token corresponding to this desired position." + ], + "accounts": [ + { + "name": "whirlpool", "isMut": false, "isSigner": false }, { - "name": "tokenOwnerAccountA", - "isMut": true, - "isSigner": false + "name": "positionAuthority", + "isMut": false, + "isSigner": true }, { - "name": "tokenOwnerAccountB", + "name": "position", "isMut": true, "isSigner": false }, { - "name": "tokenVaultA", - "isMut": true, + "name": "positionTokenAccount", + "isMut": false, "isSigner": false }, { - "name": "tokenVaultB", - "isMut": true, + "name": "tokenMintA", + "isMut": false, "isSigner": false }, { - "name": "tickArrayLower", - "isMut": true, + "name": "tokenMintB", + "isMut": false, "isSigner": false }, { - "name": "tickArrayUpper", + "name": "tokenOwnerAccountA", "isMut": true, "isSigner": false - } - ], - "args": [ - { - "name": "liquidityAmount", - "type": "u128" }, { - "name": "tokenMinA", - "type": "u64" + "name": "tokenVaultA", + "isMut": true, + "isSigner": false }, { - "name": "tokenMinB", - "type": "u64" - } - ] - }, - { - "name": "updateFeesAndRewards", - "docs": [ - "Update the accrued fees and rewards for a position.", - "", - "#### Special Errors", - "- `TickNotFound` - Provided tick array account does not contain the tick for this position.", - "- `LiquidityZero` - Position has zero liquidity and therefore already has the most updated fees and reward values." - ], - "accounts": [ - { - "name": "whirlpool", + "name": "tokenOwnerAccountB", "isMut": true, "isSigner": false }, { - "name": "position", + "name": "tokenVaultB", "isMut": true, "isSigner": false }, { - "name": "tickArrayLower", + "name": "tokenProgramA", "isMut": false, "isSigner": false }, { - "name": "tickArrayUpper", + "name": "tokenProgramB", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", "isMut": false, "isSigner": false } ], - "args": [] + "args": [ + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } + } + ] }, { - "name": "collectFees", + "name": "collectProtocolFeesV2", "docs": [ - "Collect fees accrued for this position.", + "Collect the protocol fees accrued in this Whirlpool", "", "### Authority", - "- `position_authority` - authority that owns the token corresponding to this desired position." + "- `collect_protocol_fees_authority` - assigned authority in the WhirlpoolConfig that can collect protocol fees" ], "accounts": [ { - "name": "whirlpool", + "name": "whirlpoolsConfig", "isMut": false, "isSigner": false }, { - "name": "positionAuthority", + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "collectProtocolFeesAuthority", "isMut": false, "isSigner": true }, { - "name": "position", - "isMut": true, + "name": "tokenMintA", + "isMut": false, "isSigner": false }, { - "name": "positionTokenAccount", + "name": "tokenMintB", "isMut": false, "isSigner": false }, { - "name": "tokenOwnerAccountA", + "name": "tokenVaultA", "isMut": true, "isSigner": false }, { - "name": "tokenVaultA", + "name": "tokenVaultB", "isMut": true, "isSigner": false }, { - "name": "tokenOwnerAccountB", + "name": "tokenDestinationA", "isMut": true, "isSigner": false }, { - "name": "tokenVaultB", + "name": "tokenDestinationB", "isMut": true, "isSigner": false }, { - "name": "tokenProgram", + "name": "tokenProgramA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramB", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", "isMut": false, "isSigner": false } ], - "args": [] + "args": [ + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } + } + ] }, { - "name": "collectReward", + "name": "collectRewardV2", "docs": [ "Collect rewards accrued for this position.", "", @@ -3365,13 +5877,23 @@ export const IDL: Whirlpool = { "isMut": true, "isSigner": false }, + { + "name": "rewardMint", + "isMut": false, + "isSigner": false + }, { "name": "rewardVault", "isMut": true, "isSigner": false }, { - "name": "tokenProgram", + "name": "rewardTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", "isMut": false, "isSigner": false } @@ -3380,100 +5902,79 @@ export const IDL: Whirlpool = { { "name": "rewardIndex", "type": "u8" + }, + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } } ] }, { - "name": "collectProtocolFees", + "name": "decreaseLiquidityV2", "docs": [ - "Collect the protocol fees accrued in this Whirlpool", + "Withdraw liquidity from a position in the Whirlpool. This call also updates the position's accrued fees and rewards.", "", "### Authority", - "- `collect_protocol_fees_authority` - assigned authority in the WhirlpoolConfig that can collect protocol fees" + "- `position_authority` - authority that owns the token corresponding to this desired position.", + "", + "### Parameters", + "- `liquidity_amount` - The total amount of Liquidity the user desires to withdraw.", + "- `token_min_a` - The minimum amount of tokenA the user is willing to withdraw.", + "- `token_min_b` - The minimum amount of tokenB the user is willing to withdraw.", + "", + "#### Special Errors", + "- `LiquidityZero` - Provided liquidity amount is zero.", + "- `LiquidityTooHigh` - Provided liquidity exceeds u128::max.", + "- `TokenMinSubceeded` - The required token to perform this operation subceeds the user defined amount." ], "accounts": [ - { - "name": "whirlpoolsConfig", - "isMut": false, - "isSigner": false - }, { "name": "whirlpool", "isMut": true, "isSigner": false }, { - "name": "collectProtocolFeesAuthority", + "name": "tokenProgramA", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "tokenVaultA", - "isMut": true, + "name": "tokenProgramB", + "isMut": false, "isSigner": false }, { - "name": "tokenVaultB", - "isMut": true, + "name": "memoProgram", + "isMut": false, "isSigner": false }, { - "name": "tokenDestinationA", - "isMut": true, - "isSigner": false + "name": "positionAuthority", + "isMut": false, + "isSigner": true }, { - "name": "tokenDestinationB", + "name": "position", "isMut": true, "isSigner": false }, { - "name": "tokenProgram", + "name": "positionTokenAccount", "isMut": false, "isSigner": false - } - ], - "args": [] - }, - { - "name": "swap", - "docs": [ - "Perform a swap in this Whirlpool", - "", - "### Authority", - "- \"token_authority\" - The authority to withdraw tokens from the input token account.", - "", - "### Parameters", - "- `amount` - The amount of input or output token to swap from (depending on amount_specified_is_input).", - "- `other_amount_threshold` - The maximum/minimum of input/output token to swap into (depending on amount_specified_is_input).", - "- `sqrt_price_limit` - The maximum/minimum price the swap will swap to.", - "- `amount_specified_is_input` - Specifies the token the parameter `amount`represents. If true, the amount represents the input token of the swap.", - "- `a_to_b` - The direction of the swap. True if swapping from A to B. False if swapping from B to A.", - "", - "#### Special Errors", - "- `ZeroTradableAmount` - User provided parameter `amount` is 0.", - "- `InvalidSqrtPriceLimitDirection` - User provided parameter `sqrt_price_limit` does not match the direction of the trade.", - "- `SqrtPriceOutOfBounds` - User provided parameter `sqrt_price_limit` is over Whirlppool's max/min bounds for sqrt-price.", - "- `InvalidTickArraySequence` - User provided tick-arrays are not in sequential order required to proceed in this trade direction.", - "- `TickArraySequenceInvalidIndex` - The swap loop attempted to access an invalid array index during the query of the next initialized tick.", - "- `TickArrayIndexOutofBounds` - The swap loop attempted to access an invalid array index during tick crossing.", - "- `LiquidityOverflow` - Liquidity value overflowed 128bits during tick crossing.", - "- `InvalidTickSpacing` - The swap pool was initialized with tick-spacing of 0." - ], - "accounts": [ + }, { - "name": "tokenProgram", + "name": "tokenMintA", "isMut": false, "isSigner": false }, { - "name": "tokenAuthority", + "name": "tokenMintB", "isMut": false, - "isSigner": true - }, - { - "name": "whirlpool", - "isMut": true, "isSigner": false }, { @@ -3481,125 +5982,188 @@ export const IDL: Whirlpool = { "isMut": true, "isSigner": false }, - { - "name": "tokenVaultA", - "isMut": true, - "isSigner": false - }, { "name": "tokenOwnerAccountB", "isMut": true, "isSigner": false }, { - "name": "tokenVaultB", + "name": "tokenVaultA", "isMut": true, "isSigner": false }, { - "name": "tickArray0", + "name": "tokenVaultB", "isMut": true, "isSigner": false }, { - "name": "tickArray1", + "name": "tickArrayLower", "isMut": true, "isSigner": false }, { - "name": "tickArray2", + "name": "tickArrayUpper", "isMut": true, "isSigner": false - }, - { - "name": "oracle", - "isMut": false, - "isSigner": false } ], "args": [ { - "name": "amount", - "type": "u64" + "name": "liquidityAmount", + "type": "u128" }, { - "name": "otherAmountThreshold", + "name": "tokenMinA", "type": "u64" }, { - "name": "sqrtPriceLimit", - "type": "u128" - }, - { - "name": "amountSpecifiedIsInput", - "type": "bool" + "name": "tokenMinB", + "type": "u64" }, { - "name": "aToB", - "type": "bool" + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } } ] }, { - "name": "closePosition", + "name": "increaseLiquidityV2", "docs": [ - "Close a position in a Whirlpool. Burns the position token in the owner's wallet.", + "Add liquidity to a position in the Whirlpool. This call also updates the position's accrued fees and rewards.", "", "### Authority", - "- \"position_authority\" - The authority that owns the position token.", + "- `position_authority` - authority that owns the token corresponding to this desired position.", + "", + "### Parameters", + "- `liquidity_amount` - The total amount of Liquidity the user is willing to deposit.", + "- `token_max_a` - The maximum amount of tokenA the user is willing to deposit.", + "- `token_max_b` - The maximum amount of tokenB the user is willing to deposit.", "", "#### Special Errors", - "- `ClosePositionNotEmpty` - The provided position account is not empty." + "- `LiquidityZero` - Provided liquidity amount is zero.", + "- `LiquidityTooHigh` - Provided liquidity exceeds u128::max.", + "- `TokenMaxExceeded` - The required token to perform this operation exceeds the user defined amount." ], "accounts": [ + { + "name": "whirlpool", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgramA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramB", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", + "isMut": false, + "isSigner": false + }, { "name": "positionAuthority", "isMut": false, "isSigner": true }, { - "name": "receiver", + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "positionTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMintB", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenOwnerAccountA", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenOwnerAccountB", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenVaultA", "isMut": true, "isSigner": false }, { - "name": "position", + "name": "tokenVaultB", "isMut": true, "isSigner": false }, { - "name": "positionMint", + "name": "tickArrayLower", "isMut": true, "isSigner": false }, { - "name": "positionTokenAccount", + "name": "tickArrayUpper", "isMut": true, "isSigner": false + } + ], + "args": [ + { + "name": "liquidityAmount", + "type": "u128" }, { - "name": "tokenProgram", - "isMut": false, - "isSigner": false + "name": "tokenMaxA", + "type": "u64" + }, + { + "name": "tokenMaxB", + "type": "u64" + }, + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } } - ], - "args": [] + ] }, { - "name": "setDefaultFeeRate", + "name": "initializePoolV2", "docs": [ - "Set the default_fee_rate for a FeeTier", - "Only the current fee authority has permission to invoke this instruction.", - "", - "### Authority", - "- \"fee_authority\" - Set authority in the WhirlpoolConfig", + "Initializes a Whirlpool account.", + "Fee rate is set to the default values on the config and supplied fee_tier.", "", "### Parameters", - "- `default_fee_rate` - The default fee rate that a pool will use if the pool uses this", - "fee tier during initialization.", + "- `bumps` - The bump value when deriving the PDA of the Whirlpool address.", + "- `tick_spacing` - The desired tick spacing for this pool.", + "- `initial_sqrt_price` - The desired initial sqrt-price for this pool", "", "#### Special Errors", - "- `FeeRateMaxExceeded` - If the provided default_fee_rate exceeds MAX_FEE_RATE." + "`InvalidTokenMintOrder` - The order of mints have to be ordered by", + "`SqrtPriceOutOfBounds` - provided initial_sqrt_price is not between 2^-64 to 2^64", + "" ], "accounts": [ { @@ -3608,206 +6172,170 @@ export const IDL: Whirlpool = { "isSigner": false }, { - "name": "feeTier", - "isMut": true, + "name": "tokenMintA", + "isMut": false, "isSigner": false }, { - "name": "feeAuthority", + "name": "tokenMintB", "isMut": false, - "isSigner": true - } - ], - "args": [ + "isSigner": false + }, { - "name": "defaultFeeRate", - "type": "u16" - } - ] - }, - { - "name": "setDefaultProtocolFeeRate", - "docs": [ - "Sets the default protocol fee rate for a WhirlpoolConfig", - "Protocol fee rate is represented as a basis point.", - "Only the current fee authority has permission to invoke this instruction.", - "", - "### Authority", - "- \"fee_authority\" - Set authority that can modify pool fees in the WhirlpoolConfig", - "", - "### Parameters", - "- `default_protocol_fee_rate` - Rate that is referenced during the initialization of a Whirlpool using this config.", - "", - "#### Special Errors", - "- `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE." - ], - "accounts": [ + "name": "tokenBadgeA", + "isMut": false, + "isSigner": false + }, { - "name": "whirlpoolsConfig", + "name": "tokenBadgeB", + "isMut": false, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "whirlpool", "isMut": true, "isSigner": false }, { - "name": "feeAuthority", - "isMut": false, + "name": "tokenVaultA", + "isMut": true, "isSigner": true - } - ], - "args": [ + }, { - "name": "defaultProtocolFeeRate", - "type": "u16" - } - ] - }, - { - "name": "setFeeRate", - "docs": [ - "Sets the fee rate for a Whirlpool.", - "Fee rate is represented as hundredths of a basis point.", - "Only the current fee authority has permission to invoke this instruction.", - "", - "### Authority", - "- \"fee_authority\" - Set authority that can modify pool fees in the WhirlpoolConfig", - "", - "### Parameters", - "- `fee_rate` - The rate that the pool will use to calculate fees going onwards.", - "", - "#### Special Errors", - "- `FeeRateMaxExceeded` - If the provided fee_rate exceeds MAX_FEE_RATE." - ], - "accounts": [ + "name": "tokenVaultB", + "isMut": true, + "isSigner": true + }, { - "name": "whirlpoolsConfig", + "name": "feeTier", "isMut": false, "isSigner": false }, { - "name": "whirlpool", - "isMut": true, + "name": "tokenProgramA", + "isMut": false, "isSigner": false }, { - "name": "feeAuthority", + "name": "tokenProgramB", "isMut": false, - "isSigner": true + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false } ], "args": [ { - "name": "feeRate", + "name": "tickSpacing", "type": "u16" + }, + { + "name": "initialSqrtPrice", + "type": "u128" } ] }, { - "name": "setProtocolFeeRate", + "name": "initializeRewardV2", "docs": [ - "Sets the protocol fee rate for a Whirlpool.", - "Protocol fee rate is represented as a basis point.", - "Only the current fee authority has permission to invoke this instruction.", + "Initialize reward for a Whirlpool. A pool can only support up to a set number of rewards.", "", "### Authority", - "- \"fee_authority\" - Set authority that can modify pool fees in the WhirlpoolConfig", + "- \"reward_authority\" - assigned authority by the reward_super_authority for the specified", + "reward-index in this Whirlpool", "", "### Parameters", - "- `protocol_fee_rate` - The rate that the pool will use to calculate protocol fees going onwards.", + "- `reward_index` - The reward index that we'd like to initialize. (0 <= index <= NUM_REWARDS)", "", "#### Special Errors", - "- `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE." + "- `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized", + "index in this pool, or exceeds NUM_REWARDS, or", + "all reward slots for this pool has been initialized." ], "accounts": [ { - "name": "whirlpoolsConfig", + "name": "rewardAuthority", "isMut": false, - "isSigner": false + "isSigner": true }, { - "name": "whirlpool", + "name": "funder", "isMut": true, - "isSigner": false - }, - { - "name": "feeAuthority", - "isMut": false, "isSigner": true - } - ], - "args": [ - { - "name": "protocolFeeRate", - "type": "u16" - } - ] - }, - { - "name": "setFeeAuthority", - "docs": [ - "Sets the fee authority for a WhirlpoolConfig.", - "The fee authority can set the fee & protocol fee rate for individual pools or", - "set the default fee rate for newly minted pools.", - "Only the current fee authority has permission to invoke this instruction.", - "", - "### Authority", - "- \"fee_authority\" - Set authority that can modify pool fees in the WhirlpoolConfig" - ], - "accounts": [ + }, { - "name": "whirlpoolsConfig", + "name": "whirlpool", "isMut": true, "isSigner": false }, { - "name": "feeAuthority", + "name": "rewardMint", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "newFeeAuthority", + "name": "rewardTokenBadge", "isMut": false, "isSigner": false - } - ], - "args": [] - }, - { - "name": "setCollectProtocolFeesAuthority", - "docs": [ - "Sets the fee authority to collect protocol fees for a WhirlpoolConfig.", - "Only the current collect protocol fee authority has permission to invoke this instruction.", - "", - "### Authority", - "- \"fee_authority\" - Set authority that can collect protocol fees in the WhirlpoolConfig" - ], - "accounts": [ + }, { - "name": "whirlpoolsConfig", + "name": "rewardVault", "isMut": true, + "isSigner": true + }, + { + "name": "rewardTokenProgram", + "isMut": false, "isSigner": false }, { - "name": "collectProtocolFeesAuthority", + "name": "systemProgram", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "newCollectProtocolFeesAuthority", + "name": "rent", "isMut": false, "isSigner": false } ], - "args": [] + "args": [ + { + "name": "rewardIndex", + "type": "u8" + } + ] }, { - "name": "setRewardAuthority", + "name": "setRewardEmissionsV2", "docs": [ - "Set the whirlpool reward authority at the provided `reward_index`.", - "Only the current reward authority for this reward index has permission to invoke this instruction.", + "Set the reward emissions for a reward in a Whirlpool.", "", "### Authority", - "- \"reward_authority\" - Set authority that can control reward emission for this particular reward.", + "- \"reward_authority\" - assigned authority by the reward_super_authority for the specified", + "reward-index in this Whirlpool", + "", + "### Parameters", + "- `reward_index` - The reward index (0 <= index <= NUM_REWARDS) that we'd like to modify.", + "- `emissions_per_second_x64` - The amount of rewards emitted in this pool.", "", "#### Special Errors", + "- `RewardVaultAmountInsufficient` - The amount of rewards in the reward vault cannot emit", + "more than a day of desired emissions.", + "- `InvalidTimestamp` - Provided timestamp is not in order with the previous timestamp.", "- `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized", "index in this pool, or exceeds NUM_REWARDS, or", "all reward slots for this pool has been initialized." @@ -3824,7 +6352,7 @@ export const IDL: Whirlpool = { "isSigner": true }, { - "name": "newRewardAuthority", + "name": "rewardVault", "isMut": false, "isSigner": false } @@ -3833,83 +6361,148 @@ export const IDL: Whirlpool = { { "name": "rewardIndex", "type": "u8" + }, + { + "name": "emissionsPerSecondX64", + "type": "u128" } ] }, { - "name": "setRewardAuthorityBySuperAuthority", + "name": "swapV2", "docs": [ - "Set the whirlpool reward authority at the provided `reward_index`.", - "Only the current reward super authority has permission to invoke this instruction.", + "Perform a swap in this Whirlpool", "", "### Authority", - "- \"reward_authority\" - Set authority that can control reward emission for this particular reward.", + "- \"token_authority\" - The authority to withdraw tokens from the input token account.", + "", + "### Parameters", + "- `amount` - The amount of input or output token to swap from (depending on amount_specified_is_input).", + "- `other_amount_threshold` - The maximum/minimum of input/output token to swap into (depending on amount_specified_is_input).", + "- `sqrt_price_limit` - The maximum/minimum price the swap will swap to.", + "- `amount_specified_is_input` - Specifies the token the parameter `amount`represents. If true, the amount represents the input token of the swap.", + "- `a_to_b` - The direction of the swap. True if swapping from A to B. False if swapping from B to A.", "", "#### Special Errors", - "- `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized", - "index in this pool, or exceeds NUM_REWARDS, or", - "all reward slots for this pool has been initialized." + "- `ZeroTradableAmount` - User provided parameter `amount` is 0.", + "- `InvalidSqrtPriceLimitDirection` - User provided parameter `sqrt_price_limit` does not match the direction of the trade.", + "- `SqrtPriceOutOfBounds` - User provided parameter `sqrt_price_limit` is over Whirlppool's max/min bounds for sqrt-price.", + "- `InvalidTickArraySequence` - User provided tick-arrays are not in sequential order required to proceed in this trade direction.", + "- `TickArraySequenceInvalidIndex` - The swap loop attempted to access an invalid array index during the query of the next initialized tick.", + "- `TickArrayIndexOutofBounds` - The swap loop attempted to access an invalid array index during tick crossing.", + "- `LiquidityOverflow` - Liquidity value overflowed 128bits during tick crossing.", + "- `InvalidTickSpacing` - The swap pool was initialized with tick-spacing of 0." ], "accounts": [ { - "name": "whirlpoolsConfig", + "name": "tokenProgramA", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgramB", + "isMut": false, + "isSigner": false + }, + { + "name": "memoProgram", "isMut": false, "isSigner": false }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": true + }, { "name": "whirlpool", "isMut": true, "isSigner": false }, { - "name": "rewardEmissionsSuperAuthority", + "name": "tokenMintA", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "newRewardAuthority", + "name": "tokenMintB", "isMut": false, "isSigner": false - } - ], - "args": [ + }, { - "name": "rewardIndex", - "type": "u8" - } - ] - }, - { - "name": "setRewardEmissionsSuperAuthority", - "docs": [ - "Set the whirlpool reward super authority for a WhirlpoolConfig", - "Only the current reward super authority has permission to invoke this instruction.", - "This instruction will not change the authority on any `WhirlpoolRewardInfo` whirlpool rewards.", - "", - "### Authority", - "- \"reward_emissions_super_authority\" - Set authority that can control reward authorities for all pools in this config space." - ], - "accounts": [ + "name": "tokenOwnerAccountA", + "isMut": true, + "isSigner": false + }, { - "name": "whirlpoolsConfig", + "name": "tokenVaultA", "isMut": true, "isSigner": false }, { - "name": "rewardEmissionsSuperAuthority", - "isMut": false, - "isSigner": true + "name": "tokenOwnerAccountB", + "isMut": true, + "isSigner": false }, { - "name": "newRewardEmissionsSuperAuthority", - "isMut": false, + "name": "tokenVaultB", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArray0", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArray1", + "isMut": true, + "isSigner": false + }, + { + "name": "tickArray2", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": true, "isSigner": false } ], - "args": [] + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "otherAmountThreshold", + "type": "u64" + }, + { + "name": "sqrtPriceLimit", + "type": "u128" + }, + { + "name": "amountSpecifiedIsInput", + "type": "bool" + }, + { + "name": "aToB", + "type": "bool" + }, + { + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } + } + ] }, { - "name": "twoHopSwap", + "name": "twoHopSwapV2", "docs": [ "Perform a two-hop swap in this Whirlpool", "", @@ -3939,65 +6532,80 @@ export const IDL: Whirlpool = { ], "accounts": [ { - "name": "tokenProgram", + "name": "whirlpoolOne", + "isMut": true, + "isSigner": false + }, + { + "name": "whirlpoolTwo", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenMintInput", "isMut": false, "isSigner": false }, { - "name": "tokenAuthority", + "name": "tokenMintIntermediate", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "whirlpoolOne", - "isMut": true, + "name": "tokenMintOutput", + "isMut": false, "isSigner": false }, { - "name": "whirlpoolTwo", - "isMut": true, + "name": "tokenProgramInput", + "isMut": false, "isSigner": false }, { - "name": "tokenOwnerAccountOneA", - "isMut": true, + "name": "tokenProgramIntermediate", + "isMut": false, "isSigner": false }, { - "name": "tokenVaultOneA", - "isMut": true, + "name": "tokenProgramOutput", + "isMut": false, "isSigner": false }, { - "name": "tokenOwnerAccountOneB", + "name": "tokenOwnerAccountInput", "isMut": true, "isSigner": false }, { - "name": "tokenVaultOneB", + "name": "tokenVaultOneInput", "isMut": true, "isSigner": false }, { - "name": "tokenOwnerAccountTwoA", + "name": "tokenVaultOneIntermediate", "isMut": true, "isSigner": false }, { - "name": "tokenVaultTwoA", + "name": "tokenVaultTwoIntermediate", "isMut": true, "isSigner": false }, { - "name": "tokenOwnerAccountTwoB", + "name": "tokenVaultTwoOutput", "isMut": true, "isSigner": false }, { - "name": "tokenVaultTwoB", + "name": "tokenOwnerAccountOutput", "isMut": true, "isSigner": false }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": true + }, { "name": "tickArrayOne0", "isMut": true, @@ -4030,11 +6638,16 @@ export const IDL: Whirlpool = { }, { "name": "oracleOne", - "isMut": false, + "isMut": true, "isSigner": false }, { "name": "oracleTwo", + "isMut": true, + "isSigner": false + }, + { + "name": "memoProgram", "isMut": false, "isSigner": false } @@ -4062,103 +6675,33 @@ export const IDL: Whirlpool = { }, { "name": "sqrtPriceLimitOne", - "type": "u128" - }, - { - "name": "sqrtPriceLimitTwo", - "type": "u128" - } - ] - }, - { - "name": "initializePositionBundle", - "docs": [ - "Initializes a PositionBundle account that bundles several positions.", - "A unique token will be minted to represent the position bundle in the users wallet." - ], - "accounts": [ - { - "name": "positionBundle", - "isMut": true, - "isSigner": false - }, - { - "name": "positionBundleMint", - "isMut": true, - "isSigner": true - }, - { - "name": "positionBundleTokenAccount", - "isMut": true, - "isSigner": false - }, - { - "name": "positionBundleOwner", - "isMut": false, - "isSigner": false - }, - { - "name": "funder", - "isMut": true, - "isSigner": true - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false - }, - { - "name": "associatedTokenProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, - { - "name": "initializePositionBundleWithMetadata", - "docs": [ - "Initializes a PositionBundle account that bundles several positions.", - "A unique token will be minted to represent the position bundle in the users wallet.", - "Additional Metaplex metadata is appended to identify the token." - ], - "accounts": [ - { - "name": "positionBundle", - "isMut": true, - "isSigner": false - }, - { - "name": "positionBundleMint", - "isMut": true, - "isSigner": true + "type": "u128" }, { - "name": "positionBundleMetadata", - "isMut": true, - "isSigner": false, - "docs": [ - "https://github.com/metaplex-foundation/metaplex-program-library/blob/773a574c4b34e5b9f248a81306ec24db064e255f/token-metadata/program/src/utils/metadata.rs#L100" - ] + "name": "sqrtPriceLimitTwo", + "type": "u128" }, { - "name": "positionBundleTokenAccount", - "isMut": true, + "name": "remainingAccountsInfo", + "type": { + "option": { + "defined": "RemainingAccountsInfo" + } + } + } + ] + }, + { + "name": "initializeConfigExtension", + "accounts": [ + { + "name": "config", + "isMut": false, "isSigner": false }, { - "name": "positionBundleOwner", - "isMut": false, + "name": "configExtension", + "isMut": true, "isSigner": false }, { @@ -4167,32 +6710,38 @@ export const IDL: Whirlpool = { "isSigner": true }, { - "name": "metadataUpdateAuth", + "name": "feeAuthority", "isMut": false, - "isSigner": false + "isSigner": true }, { - "name": "tokenProgram", + "name": "systemProgram", "isMut": false, "isSigner": false - }, + } + ], + "args": [] + }, + { + "name": "setConfigExtensionAuthority", + "accounts": [ { - "name": "systemProgram", + "name": "whirlpoolsConfig", "isMut": false, "isSigner": false }, { - "name": "rent", - "isMut": false, + "name": "whirlpoolsConfigExtension", + "isMut": true, "isSigner": false }, { - "name": "associatedTokenProgram", + "name": "configExtensionAuthority", "isMut": false, - "isSigner": false + "isSigner": true }, { - "name": "metadataProgram", + "name": "newConfigExtensionAuthority", "isMut": false, "isSigner": false } @@ -4200,44 +6749,25 @@ export const IDL: Whirlpool = { "args": [] }, { - "name": "deletePositionBundle", - "docs": [ - "Delete a PositionBundle account. Burns the position bundle token in the owner's wallet.", - "", - "### Authority", - "- `position_bundle_owner` - The owner that owns the position bundle token.", - "", - "### Special Errors", - "- `PositionBundleNotDeletable` - The provided position bundle has open positions." - ], + "name": "setTokenBadgeAuthority", "accounts": [ { - "name": "positionBundle", - "isMut": true, - "isSigner": false - }, - { - "name": "positionBundleMint", - "isMut": true, + "name": "whirlpoolsConfig", + "isMut": false, "isSigner": false }, { - "name": "positionBundleTokenAccount", + "name": "whirlpoolsConfigExtension", "isMut": true, "isSigner": false }, { - "name": "positionBundleOwner", + "name": "configExtensionAuthority", "isMut": false, "isSigner": true }, { - "name": "receiver", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenProgram", + "name": "newTokenBadgeAuthority", "isMut": false, "isSigner": false } @@ -4245,51 +6775,33 @@ export const IDL: Whirlpool = { "args": [] }, { - "name": "openBundledPosition", - "docs": [ - "Open a bundled position in a Whirlpool. No new tokens are issued", - "because the owner of the position bundle becomes the owner of the position.", - "The position will start off with 0 liquidity.", - "", - "### Authority", - "- `position_bundle_authority` - authority that owns the token corresponding to this desired position bundle.", - "", - "### Parameters", - "- `bundle_index` - The bundle index that we'd like to open.", - "- `tick_lower_index` - The tick specifying the lower end of the position range.", - "- `tick_upper_index` - The tick specifying the upper end of the position range.", - "", - "#### Special Errors", - "- `InvalidBundleIndex` - If the provided bundle index is out of bounds.", - "- `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of", - "the tick-spacing in this pool." - ], + "name": "initializeTokenBadge", "accounts": [ { - "name": "bundledPosition", - "isMut": true, - "isSigner": false - }, - { - "name": "positionBundle", - "isMut": true, + "name": "whirlpoolsConfig", + "isMut": false, "isSigner": false }, { - "name": "positionBundleTokenAccount", + "name": "whirlpoolsConfigExtension", "isMut": false, "isSigner": false }, { - "name": "positionBundleAuthority", + "name": "tokenBadgeAuthority", "isMut": false, "isSigner": true }, { - "name": "whirlpool", + "name": "tokenMint", "isMut": false, "isSigner": false }, + { + "name": "tokenBadge", + "isMut": true, + "isSigner": false + }, { "name": "funder", "isMut": true, @@ -4299,63 +6811,37 @@ export const IDL: Whirlpool = { "name": "systemProgram", "isMut": false, "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false } ], - "args": [ - { - "name": "bundleIndex", - "type": "u16" - }, - { - "name": "tickLowerIndex", - "type": "i32" - }, - { - "name": "tickUpperIndex", - "type": "i32" - } - ] + "args": [] }, { - "name": "closeBundledPosition", - "docs": [ - "Close a bundled position in a Whirlpool.", - "", - "### Authority", - "- `position_bundle_authority` - authority that owns the token corresponding to this desired position bundle.", - "", - "### Parameters", - "- `bundle_index` - The bundle index that we'd like to close.", - "", - "#### Special Errors", - "- `InvalidBundleIndex` - If the provided bundle index is out of bounds.", - "- `ClosePositionNotEmpty` - The provided position account is not empty." - ], + "name": "deleteTokenBadge", "accounts": [ { - "name": "bundledPosition", - "isMut": true, + "name": "whirlpoolsConfig", + "isMut": false, "isSigner": false }, { - "name": "positionBundle", - "isMut": true, + "name": "whirlpoolsConfigExtension", + "isMut": false, "isSigner": false }, { - "name": "positionBundleTokenAccount", + "name": "tokenBadgeAuthority", "isMut": false, - "isSigner": false + "isSigner": true }, { - "name": "positionBundleAuthority", + "name": "tokenMint", "isMut": false, - "isSigner": true + "isSigner": false + }, + { + "name": "tokenBadge", + "isMut": true, + "isSigner": false }, { "name": "receiver", @@ -4363,15 +6849,30 @@ export const IDL: Whirlpool = { "isSigner": false } ], - "args": [ - { - "name": "bundleIndex", - "type": "u16" - } - ] + "args": [] } ], "accounts": [ + { + "name": "whirlpoolsConfigExtension", + "type": { + "kind": "struct", + "fields": [ + { + "name": "whirlpoolsConfig", + "type": "publicKey" + }, + { + "name": "configExtensionAuthority", + "type": "publicKey" + }, + { + "name": "tokenBadgeAuthority", + "type": "publicKey" + } + ] + } + }, { "name": "whirlpoolsConfig", "type": { @@ -4519,6 +7020,22 @@ export const IDL: Whirlpool = { ] } }, + { + "name": "tokenBadge", + "type": { + "kind": "struct", + "fields": [ + { + "name": "whirlpoolsConfig", + "type": "publicKey" + }, + { + "name": "tokenMint", + "type": "publicKey" + } + ] + } + }, { "name": "whirlpool", "type": { @@ -4765,6 +7282,40 @@ export const IDL: Whirlpool = { ] } }, + { + "name": "RemainingAccountsSlice", + "type": { + "kind": "struct", + "fields": [ + { + "name": "accountsType", + "type": { + "defined": "AccountsType" + } + }, + { + "name": "length", + "type": "u8" + } + ] + } + }, + { + "name": "RemainingAccountsInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "slices", + "type": { + "vec": { + "defined": "RemainingAccountsSlice" + } + } + } + ] + } + }, { "name": "CurrIndex", "type": { @@ -4809,6 +7360,32 @@ export const IDL: Whirlpool = { } ] } + }, + { + "name": "AccountsType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "TransferHookA" + }, + { + "name": "TransferHookB" + }, + { + "name": "TransferHookReward" + }, + { + "name": "TransferHookInput" + }, + { + "name": "TransferHookIntermediate" + }, + { + "name": "TransferHookOutput" + } + ] + } } ], "errors": [ @@ -5046,6 +7623,41 @@ export const IDL: Whirlpool = { "code": 6046, "name": "PositionBundleNotDeletable", "msg": "Unable to delete PositionBundle with open positions" + }, + { + "code": 6047, + "name": "UnsupportedTokenMint", + "msg": "Token mint has unsupported attributes" + }, + { + "code": 6048, + "name": "RemainingAccountsInvalidSlice", + "msg": "Invalid remaining accounts" + }, + { + "code": 6049, + "name": "RemainingAccountsInsufficient", + "msg": "Insufficient remaining accounts" + }, + { + "code": 6050, + "name": "NoExtraAccountsForTransferHook", + "msg": "Unable to call transfer hook without extra accounts" + }, + { + "code": 6051, + "name": "IntermediateTokenAmountMismatch", + "msg": "Output and input amount mismatch" + }, + { + "code": 6052, + "name": "TransferFeeCalculationError", + "msg": "Transfer fee calculation failed" + }, + { + "code": 6053, + "name": "RemainingAccountsDuplicatedAccountsType", + "msg": "Same accounts type is provided more than once" } ] }; diff --git a/sdk/src/impl/position-impl.ts b/sdk/src/impl/position-impl.ts index 2f4a3451e..1a41cbbae 100644 --- a/sdk/src/impl/position-impl.ts +++ b/sdk/src/impl/position-impl.ts @@ -2,6 +2,7 @@ import { Address } from "@coral-xyz/anchor"; import { AddressUtil, Instruction, + ResolvedTokenAddressInstruction, TokenUtil, TransactionBuilder, ZERO, @@ -15,9 +16,13 @@ import { DecreaseLiquidityInput, IncreaseLiquidityInput, collectFeesIx, + collectFeesV2Ix, collectRewardIx, + collectRewardV2Ix, decreaseLiquidityIx, + decreaseLiquidityV2Ix, increaseLiquidityIx, + increaseLiquidityV2Ix, updateFeesAndRewardsIx, } from "../instructions"; import { @@ -34,6 +39,8 @@ import { resolveAtaForMints, } from "../utils/whirlpool-ata-utils"; import { Position } from "../whirlpool-client"; +import { TokenExtensionUtil } from "../utils/public/token-extension-util"; +import { MultipleTransactionBuilderFactoryWithAccountResolver, convertListToMap } from "../utils/txn-utils"; export class PositionImpl implements Position { private data: PositionData; @@ -107,6 +114,8 @@ export class PositionImpl implements Position { throw new Error("Unable to fetch whirlpool for this position."); } + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(this.ctx.fetcher, whirlpool, IGNORE_CACHE); + const txBuilder = new TransactionBuilder( this.ctx.provider.connection, this.ctx.provider.wallet, @@ -140,12 +149,14 @@ export class PositionImpl implements Position { tokenOwnerAccountA = getAssociatedTokenAddressSync( whirlpool.tokenMintA, sourceWalletKey, - this.ctx.accountResolverOpts.allowPDAOwnerAddress + this.ctx.accountResolverOpts.allowPDAOwnerAddress, + tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, ); tokenOwnerAccountB = getAssociatedTokenAddressSync( whirlpool.tokenMintB, sourceWalletKey, - this.ctx.accountResolverOpts.allowPDAOwnerAddress + this.ctx.accountResolverOpts.allowPDAOwnerAddress, + tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, ); } const positionTokenAccount = getAssociatedTokenAddressSync( @@ -154,7 +165,7 @@ export class PositionImpl implements Position { this.ctx.accountResolverOpts.allowPDAOwnerAddress ); - const increaseIx = increaseLiquidityIx(this.ctx.program, { + const baseParams = { ...liquidityInput, whirlpool: this.data.whirlpool, position: this.address, @@ -174,7 +185,27 @@ export class PositionImpl implements Position { TickUtil.getStartTickIndex(this.data.tickUpperIndex, whirlpool.tickSpacing) ).publicKey, positionAuthority: positionWalletKey, - }); + }; + // V2 can handle TokenProgram/TokenProgram pool, but it increases the size of transaction, so V1 is prefer if possible. + const increaseIx = !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx) + ? increaseLiquidityIx(this.ctx.program, baseParams) + : increaseLiquidityV2Ix(this.ctx.program, { + ...baseParams, + tokenMintA: whirlpool.tokenMintA, + tokenMintB: whirlpool.tokenMintB, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + ...await TokenExtensionUtil.getExtraAccountMetasForTransferHookForPool( + this.ctx.connection, + tokenExtensionCtx, + baseParams.tokenOwnerAccountA, + baseParams.tokenVaultA, + baseParams.positionAuthority, + baseParams.tokenOwnerAccountB, + baseParams.tokenVaultB, + baseParams.positionAuthority, + ), + }); txBuilder.addInstruction(increaseIx); return txBuilder; } @@ -199,6 +230,8 @@ export class PositionImpl implements Position { throw new Error("Unable to fetch whirlpool for this position."); } + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(this.ctx.fetcher, whirlpool, IGNORE_CACHE); + const txBuilder = new TransactionBuilder( this.ctx.provider.connection, this.ctx.provider.wallet, @@ -228,16 +261,18 @@ export class PositionImpl implements Position { tokenOwnerAccountA = getAssociatedTokenAddressSync( whirlpool.tokenMintA, sourceWalletKey, - this.ctx.accountResolverOpts.allowPDAOwnerAddress + this.ctx.accountResolverOpts.allowPDAOwnerAddress, + tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, ); tokenOwnerAccountB = getAssociatedTokenAddressSync( whirlpool.tokenMintB, sourceWalletKey, - this.ctx.accountResolverOpts.allowPDAOwnerAddress + this.ctx.accountResolverOpts.allowPDAOwnerAddress, + tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, ); } - const decreaseIx = decreaseLiquidityIx(this.ctx.program, { + const baseParams = { ...liquidityInput, whirlpool: this.data.whirlpool, position: this.address, @@ -261,7 +296,27 @@ export class PositionImpl implements Position { TickUtil.getStartTickIndex(this.data.tickUpperIndex, whirlpool.tickSpacing) ).publicKey, positionAuthority: positionWalletKey, - }); + }; + // V2 can handle TokenProgram/TokenProgram pool, but it increases the size of transaction, so V1 is prefer if possible. + const decreaseIx = !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx) + ? decreaseLiquidityIx(this.ctx.program, baseParams) + : decreaseLiquidityV2Ix(this.ctx.program, { + ...baseParams, + tokenMintA: whirlpool.tokenMintA, + tokenMintB: whirlpool.tokenMintB, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + ...await TokenExtensionUtil.getExtraAccountMetasForTransferHookForPool( + this.ctx.connection, + tokenExtensionCtx, + baseParams.tokenVaultA, + baseParams.tokenOwnerAccountA, + baseParams.whirlpool, // vault to owner, so pool is authority + baseParams.tokenVaultB, + baseParams.tokenOwnerAccountB, + baseParams.whirlpool, // vault to owner, so pool is authority + ), + }); txBuilder.addInstruction(decreaseIx); return txBuilder; } @@ -287,6 +342,8 @@ export class PositionImpl implements Position { ); } + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(this.ctx.fetcher, whirlpool, IGNORE_CACHE); + let txBuilder = new TransactionBuilder( this.ctx.provider.connection, this.ctx.provider.wallet, @@ -350,7 +407,7 @@ export class PositionImpl implements Position { txBuilder.addInstruction(updateIx); } - const ix = collectFeesIx(this.ctx.program, { + const baseParams = { whirlpool: this.data.whirlpool, position: this.address, positionTokenAccount, @@ -359,8 +416,27 @@ export class PositionImpl implements Position { tokenVaultA: whirlpool.tokenVaultA, tokenVaultB: whirlpool.tokenVaultB, positionAuthority: positionWalletKey, - }); - + }; + // V2 can handle TokenProgram/TokenProgram pool, but it increases the size of transaction, so V1 is prefer if possible. + const ix = !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx) + ? collectFeesIx(this.ctx.program, baseParams) + : collectFeesV2Ix(this.ctx.program, { + ...baseParams, + tokenMintA: whirlpool.tokenMintA, + tokenMintB: whirlpool.tokenMintB, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + ...await TokenExtensionUtil.getExtraAccountMetasForTransferHookForPool( + this.ctx.connection, + tokenExtensionCtx, + baseParams.tokenVaultA, + baseParams.tokenOwnerAccountA, + baseParams.whirlpool, // vault to owner, so pool is authority + baseParams.tokenVaultB, + baseParams.tokenOwnerAccountB, + baseParams.whirlpool, // vault to owner, so pool is authority + ), + }); txBuilder.addInstruction(ix); return txBuilder; @@ -374,7 +450,7 @@ export class PositionImpl implements Position { positionWallet?: Address, ataPayer?: Address, opts: WhirlpoolAccountFetchOptions = IGNORE_CACHE - ): Promise { + ): Promise { const [destinationWalletKey, positionWalletKey, ataPayerKey] = AddressUtil.toPubKeys([ destinationWallet ?? this.ctx.wallet.publicKey, positionWallet ?? this.ctx.wallet.publicKey, @@ -392,86 +468,107 @@ export class PositionImpl implements Position { PoolUtil.isRewardInitialized(info) ); - const txBuilder = new TransactionBuilder( - this.ctx.provider.connection, - this.ctx.provider.wallet, - this.ctx.txBuilderOpts - ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(this.ctx.fetcher, whirlpool, IGNORE_CACHE); - const accountExemption = await this.ctx.fetcher.getAccountRentExempt(); + let resolvedAtas: Record; + if (ownerTokenAccountMap) { + resolvedAtas = {}; + Object.entries(ownerTokenAccountMap).forEach(([mint, address]) => { + if (!address) return; - let ataMap = { ...ownerTokenAccountMap }; - if (!ownerTokenAccountMap) { - const rewardMints = getTokenMintsFromWhirlpools([whirlpool], TokenMintTypes.REWARD_ONLY); - const { ataTokenAddresses: affliatedTokenAtaMap, resolveAtaIxs } = await resolveAtaForMints( - this.ctx, - { - mints: rewardMints.mintMap, - accountExemption, - receiver: destinationWalletKey, - payer: ataPayerKey, + resolvedAtas[mint] = { + address: AddressUtil.toPubKey(address), + instructions: [], + cleanupInstructions: [], + signers: [], + tokenProgram: PublicKey.default, // unused (dummy) } + }); + } else { + const accountExemption = await this.ctx.fetcher.getAccountRentExempt(); + const rewardMints = getTokenMintsFromWhirlpools([whirlpool], TokenMintTypes.REWARD_ONLY); + resolvedAtas = convertListToMap( + await resolveOrCreateATAs( + this.ctx.connection, + destinationWalletKey, + rewardMints.mintMap.map((tokenMint) => ({ tokenMint })), + async () => accountExemption, + ataPayerKey, + true, // CreateIdempotent + this.ctx.accountResolverOpts.allowPDAOwnerAddress, + ), + rewardMints.mintMap.map((mint) => mint.toBase58()) ); - - if (rewardMints.hasNativeMint) { - let { address: wSOLAta, ...resolveWSolIx } = - TokenUtil.createWrappedNativeAccountInstruction( - destinationWalletKey, - ZERO, - accountExemption, - ataPayerKey, - destinationWalletKey, - this.ctx.accountResolverOpts.createWrappedSolAccountMethod - ); - affliatedTokenAtaMap[NATIVE_MINT.toBase58()] = wSOLAta; - txBuilder.addInstruction(resolveWSolIx); - } - - txBuilder.addInstructions(resolveAtaIxs); - - ataMap = { ...affliatedTokenAtaMap }; } + const builder = new MultipleTransactionBuilderFactoryWithAccountResolver( + this.ctx, + resolvedAtas, + destinationWalletKey, + ataPayerKey + ); + const positionTokenAccount = getAssociatedTokenAddressSync( this.data.positionMint, positionWalletKey, this.ctx.accountResolverOpts.allowPDAOwnerAddress ); + if (updateFeesAndRewards && !this.data.liquidity.isZero()) { - const updateIx = await this.updateFeesAndRewards(); - txBuilder.addInstruction(updateIx); + await builder.addInstructions(async () => { + const updateIx = await this.updateFeesAndRewards(); + return [updateIx]; + }); } - initializedRewards.forEach((info, index) => { + for (let index = 0; index < initializedRewards.length; index++) { + const info = initializedRewards[index]; if ( rewardsToCollect && !rewardsToCollect.some((r) => r.toString() === info.mint.toBase58()) ) { // If rewardsToCollect is specified and this reward is not in it, // don't include collectIX for that in TX - return; + break; } - const rewardOwnerAccount = ataMap[info.mint.toBase58()]; - invariant( - !!rewardOwnerAccount, - `No owner token account provided for wallet ${destinationWalletKey.toBase58()} for reward ${index} token ${info.mint.toBase58()} ` - ); - - const ix = collectRewardIx(this.ctx.program, { - whirlpool: this.data.whirlpool, - position: this.address, - positionTokenAccount, - rewardIndex: index, - rewardOwnerAccount: AddressUtil.toPubKey(rewardOwnerAccount), - rewardVault: info.vault, - positionAuthority: positionWalletKey, + await builder.addInstructions(async (resolve) => { + const rewardOwnerAccount = resolve(info.mint.toBase58()); + invariant( + !!rewardOwnerAccount, + `No owner token account provided for wallet ${destinationWalletKey.toBase58()} for reward ${index} token ${info.mint.toBase58()} ` + ); + + const baseParams = { + whirlpool: this.data.whirlpool, + position: this.address, + positionTokenAccount, + rewardIndex: index, + rewardOwnerAccount: AddressUtil.toPubKey(rewardOwnerAccount), + rewardVault: info.vault, + positionAuthority: positionWalletKey, + }; + // V2 can handle TokenProgram/TokenProgram pool, but it increases the size of transaction, so V1 is prefer if possible. + const ix = !TokenExtensionUtil.isV2IxRequiredReward(tokenExtensionCtx, index) + ? collectRewardIx(this.ctx.program, baseParams) + : collectRewardV2Ix(this.ctx.program, { + ...baseParams, + rewardMint: info.mint, + rewardTokenProgram: tokenExtensionCtx.rewardTokenMintsWithProgram[index]!.tokenProgram, + rewardTransferHookAccounts: await TokenExtensionUtil.getExtraAccountMetasForTransferHook( + this.ctx.connection, + tokenExtensionCtx.rewardTokenMintsWithProgram[index]!, + baseParams.rewardVault, + baseParams.rewardOwnerAccount, + baseParams.whirlpool, // vault to owner, so pool is authority + ), + }); + + return [ix]; }); + } - txBuilder.addInstruction(ix); - }); - - return txBuilder; + return builder.build(); } private async refresh() { diff --git a/sdk/src/impl/whirlpool-client-impl.ts b/sdk/src/impl/whirlpool-client-impl.ts index 866abdae2..2db1e4c7d 100644 --- a/sdk/src/impl/whirlpool-client-impl.ts +++ b/sdk/src/impl/whirlpool-client-impl.ts @@ -23,6 +23,7 @@ import { Position, Whirlpool, WhirlpoolClient } from "../whirlpool-client"; import { PositionImpl } from "./position-impl"; import { getRewardInfos, getTokenMintInfos, getTokenVaultAccountInfos } from "./util"; import { WhirlpoolImpl } from "./whirlpool-impl"; +import { NO_TOKEN_EXTENSION_CONTEXT, TokenExtensionContextForPool, TokenExtensionUtil } from "../utils/public/token-extension-util"; export class WhirlpoolClientImpl implements WhirlpoolClient { constructor(readonly ctx: WhirlpoolContext) {} @@ -210,6 +211,13 @@ export class WhirlpoolClientImpl implements WhirlpoolClient { "Token order needs to be flipped to match the canonical ordering (i.e. sorted on the byte repr. of the mint pubkeys)" ); + const mintInfos = await this.ctx.fetcher.getMintInfos([tokenMintA, tokenMintB], opts); + const tokenExtensionCtx: TokenExtensionContextForPool = { + ...NO_TOKEN_EXTENSION_CONTEXT, + tokenMintWithProgramA: mintInfos.get(tokenMintA.toString())!, + tokenMintWithProgramB: mintInfos.get(tokenMintB.toString())!, + }; + whirlpoolsConfig = AddressUtil.toPubKey(whirlpoolsConfig); const feeTierKey = PDAUtil.getFeeTier( @@ -239,7 +247,10 @@ export class WhirlpoolClientImpl implements WhirlpoolClient { this.ctx.txBuilderOpts ); - const initPoolIx = WhirlpoolIx.initializePoolIx(this.ctx.program, { + const tokenBadgeA = PDAUtil.getTokenBadge(this.ctx.program.programId, whirlpoolsConfig, AddressUtil.toPubKey(tokenMintA)).publicKey; + const tokenBadgeB = PDAUtil.getTokenBadge(this.ctx.program.programId, whirlpoolsConfig, AddressUtil.toPubKey(tokenMintB)).publicKey; + + const baseParams = { initSqrtPrice, whirlpoolsConfig, whirlpoolPda, @@ -250,7 +261,16 @@ export class WhirlpoolClientImpl implements WhirlpoolClient { feeTierKey, tickSpacing, funder: new PublicKey(funder), - }); + }; + const initPoolIx = !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx) + ? WhirlpoolIx.initializePoolIx(this.ctx.program, baseParams) + : WhirlpoolIx.initializePoolV2Ix(this.ctx.program, { + ...baseParams, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + tokenBadgeA, + tokenBadgeB, + }); const initialTickArrayStartTick = TickUtil.getStartTickIndex(initialTick, tickSpacing); const initialTickArrayPda = PDAUtil.getTickArray( diff --git a/sdk/src/impl/whirlpool-impl.ts b/sdk/src/impl/whirlpool-impl.ts index 6a1c9d0c2..2719c5fe9 100644 --- a/sdk/src/impl/whirlpool-impl.ts +++ b/sdk/src/impl/whirlpool-impl.ts @@ -18,7 +18,9 @@ import { SwapInput, closePositionIx, decreaseLiquidityIx, + decreaseLiquidityV2Ix, increaseLiquidityIx, + increaseLiquidityV2Ix, initTickArrayIx, openPositionIx, openPositionWithMetadataIx, @@ -32,7 +34,7 @@ import { } from "../quotes/public"; import { TokenAccountInfo, TokenInfo, WhirlpoolData, WhirlpoolRewardInfo } from "../types/public"; import { getTickArrayDataForPosition } from "../utils/builder/position-builder-util"; -import { PDAUtil, TickArrayUtil, TickUtil } from "../utils/public"; +import { PDAUtil, PoolUtil, TickArrayUtil, TickUtil } from "../utils/public"; import { TokenMintTypes, getTokenMintsFromWhirlpools, @@ -41,7 +43,9 @@ import { import { Whirlpool } from "../whirlpool-client"; import { PositionImpl } from "./position-impl"; import { getRewardInfos, getTokenVaultAccountInfos } from "./util"; -import { checkMergedTransactionSizeIsValid } from "../utils/txn-utils"; +import { MultipleTransactionBuilderFactoryWithAccountResolver, convertListToMap } from "../utils/txn-utils"; +import { TokenExtensionUtil } from "../utils/public/token-extension-util"; +import { WhirlpoolIx } from "../ix"; export class WhirlpoolImpl implements Whirlpool { private data: WhirlpoolData; @@ -274,6 +278,8 @@ export class WhirlpoolImpl implements Whirlpool { throw new Error(`Whirlpool not found: ${translateAddress(this.address).toBase58()}`); } + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(this.ctx.fetcher, whirlpool, IGNORE_CACHE); + invariant( TickUtil.isTickInitializable(tickLower, whirlpool.tickSpacing), `lower tick ${tickLower} is not an initializable tick for tick-spacing ${whirlpool.tickSpacing}` @@ -353,7 +359,7 @@ export class WhirlpoolImpl implements Whirlpool { this.ctx.program.programId ); - const liquidityIx = increaseLiquidityIx(this.ctx.program, { + const baseParams = { liquidityAmount: liquidity, tokenMaxA, tokenMaxB, @@ -367,7 +373,27 @@ export class WhirlpoolImpl implements Whirlpool { tokenVaultB: whirlpool.tokenVaultB, tickArrayLower: tickArrayLowerPda.publicKey, tickArrayUpper: tickArrayUpperPda.publicKey, - }); + }; + // V2 can handle TokenProgram/TokenProgram pool, but it increases the size of transaction, so V1 is prefer if possible. + const liquidityIx = !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx) + ? increaseLiquidityIx(this.ctx.program, baseParams) + : increaseLiquidityV2Ix(this.ctx.program, { + ...baseParams, + tokenMintA: whirlpool.tokenMintA, + tokenMintB: whirlpool.tokenMintB, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + ...await TokenExtensionUtil.getExtraAccountMetasForTransferHookForPool( + this.ctx.connection, + tokenExtensionCtx, + baseParams.tokenOwnerAccountA, + baseParams.tokenVaultA, + baseParams.positionAuthority, + baseParams.tokenOwnerAccountB, + baseParams.tokenVaultB, + baseParams.positionAuthority, + ), + }); txBuilder.addInstruction(liquidityIx); return { @@ -401,12 +427,6 @@ export class WhirlpoolImpl implements Whirlpool { this.ctx.accountResolverOpts.allowPDAOwnerAddress ); - const tokenAccountsTxBuilder = new TransactionBuilder( - this.ctx.provider.connection, - this.ctx.provider.wallet, - this.ctx.txBuilderOpts - ); - const accountExemption = await this.ctx.fetcher.getAccountRentExempt(); const txBuilder = new TransactionBuilder( @@ -446,6 +466,8 @@ export class WhirlpoolImpl implements Whirlpool { `Tick array ${tickArrayUpper} expected to be initialized for whirlpool ${this.address}` ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(this.ctx.fetcher, whirlpool, IGNORE_CACHE); + const position = new PositionImpl( this.ctx, positionAddress, @@ -463,6 +485,7 @@ export class WhirlpoolImpl implements Whirlpool { whirlpool, tickLower, tickUpper, + tokenExtensionCtx, }); const rewardsQuote = collectRewardsQuote({ @@ -470,18 +493,25 @@ export class WhirlpoolImpl implements Whirlpool { whirlpool, tickLower, tickUpper, + tokenExtensionCtx, }); const shouldCollectFees = feesQuote.feeOwedA.gtn(0) || feesQuote.feeOwedB.gtn(0); invariant( - this.data.rewardInfos.length === rewardsQuote.length, + this.data.rewardInfos.length === rewardsQuote.rewardOwed.length, "Rewards quote does not match reward infos length" ); const shouldDecreaseLiquidity = positionData.liquidity.gtn(0); const rewardsToCollect = this.data.rewardInfos - .filter((_, i) => (rewardsQuote[i] ?? ZERO).gtn(0)) + .filter((_, i) => { + return ( + (rewardsQuote.rewardOwed[i] ?? ZERO).gtn(0) || + // we need to collect reward even if all reward will be deducted as transfer fee + (rewardsQuote.transferFee.deductedFromRewardOwed[i] ?? ZERO).gtn(0) + ); + }) .map((info) => info.mint); const shouldCollectRewards = rewardsToCollect.length > 0; @@ -493,117 +523,172 @@ export class WhirlpoolImpl implements Whirlpool { mintType = TokenMintTypes.REWARD_ONLY; } - const affiliatedMints = getTokenMintsFromWhirlpools([whirlpool], mintType); - const { ataTokenAddresses: walletTokenAccountsByMint, resolveAtaIxs } = - await resolveAtaForMints(this.ctx, { - mints: affiliatedMints.mintMap, - accountExemption, - receiver: destinationWallet, - payer: payerKey, - }); - - tokenAccountsTxBuilder.addInstructions(resolveAtaIxs); - - // Handle native mint - if (affiliatedMints.hasNativeMint) { - let { address: wSOLAta, ...resolveWSolIx } = TokenUtil.createWrappedNativeAccountInstruction( + const allMints = getTokenMintsFromWhirlpools([whirlpool], mintType); + const resolvedAtas = convertListToMap( + await resolveOrCreateATAs( + this.ctx.connection, destinationWallet, - ZERO, - accountExemption, + allMints.mintMap.map((tokenMint) => ({ tokenMint })), + async () => accountExemption, payerKey, - destinationWallet, + true, // CreateIdempotent + this.ctx.accountResolverOpts.allowPDAOwnerAddress, this.ctx.accountResolverOpts.createWrappedSolAccountMethod - ); - walletTokenAccountsByMint[NATIVE_MINT.toBase58()] = wSOLAta; - txBuilder.addInstruction(resolveWSolIx); - } + ), + allMints.mintMap.map((mint) => mint.toBase58()) + ); - if (shouldDecreaseLiquidity) { - /* Remove all liquidity remaining in the position */ - const tokenOwnerAccountA = walletTokenAccountsByMint[whirlpool.tokenMintA.toBase58()]; - const tokenOwnerAccountB = walletTokenAccountsByMint[whirlpool.tokenMintB.toBase58()]; - - const decreaseLiqQuote = decreaseLiquidityQuoteByLiquidityWithParams({ - liquidity: positionData.liquidity, - slippageTolerance, - sqrtPrice: whirlpool.sqrtPrice, - tickCurrentIndex: whirlpool.tickCurrentIndex, - tickLowerIndex: positionData.tickLowerIndex, - tickUpperIndex: positionData.tickUpperIndex, - }); + const builder = new MultipleTransactionBuilderFactoryWithAccountResolver( + this.ctx, + resolvedAtas, + destinationWallet, + payerKey + ); - const liquidityIx = decreaseLiquidityIx(this.ctx.program, { - ...decreaseLiqQuote, - whirlpool: positionData.whirlpool, - positionAuthority: positionWallet, - position: positionAddress, - positionTokenAccount, - tokenOwnerAccountA, - tokenOwnerAccountB, - tokenVaultA: whirlpool.tokenVaultA, - tokenVaultB: whirlpool.tokenVaultB, - tickArrayLower, - tickArrayUpper, + if (shouldDecreaseLiquidity) { + await builder.addInstructions(async (resolveTokenAccount) => { + const tokenOwnerAccountA = resolveTokenAccount(whirlpool.tokenMintA.toBase58()); + const tokenOwnerAccountB = resolveTokenAccount(whirlpool.tokenMintB.toBase58()); + + const decreaseLiqQuote = decreaseLiquidityQuoteByLiquidityWithParams({ + liquidity: positionData.liquidity, + slippageTolerance, + sqrtPrice: whirlpool.sqrtPrice, + tickCurrentIndex: whirlpool.tickCurrentIndex, + tickLowerIndex: positionData.tickLowerIndex, + tickUpperIndex: positionData.tickUpperIndex, + tokenExtensionCtx, + }); + + const baseParams = { + ...decreaseLiqQuote, + whirlpool: positionData.whirlpool, + positionAuthority: positionWallet, + position: positionAddress, + positionTokenAccount, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA: whirlpool.tokenVaultA, + tokenVaultB: whirlpool.tokenVaultB, + tickArrayLower, + tickArrayUpper, + }; + + // V2 can handle TokenProgram/TokenProgram pool, but it increases the size of transaction, so V1 is prefer if possible. + const ix = !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx) + ? WhirlpoolIx.decreaseLiquidityIx(this.ctx.program, baseParams) + : WhirlpoolIx.decreaseLiquidityV2Ix(this.ctx.program, { + ...baseParams, + tokenMintA: whirlpool.tokenMintA, + tokenMintB: whirlpool.tokenMintB, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + ...await TokenExtensionUtil.getExtraAccountMetasForTransferHookForPool( + this.ctx.connection, + tokenExtensionCtx, + baseParams.tokenVaultA, + baseParams.tokenOwnerAccountA, + baseParams.whirlpool, // vault to owner, so pool is authority + baseParams.tokenVaultB, + baseParams.tokenOwnerAccountB, + baseParams.whirlpool, // vault to owner, so pool is authority + ), + }); + + return [ix]; }); - - txBuilder.addInstruction(liquidityIx); } if (shouldCollectFees) { - const collectFeexTx = await position.collectFees( - false, - walletTokenAccountsByMint, - destinationWallet, - positionWallet, - payerKey, - IGNORE_CACHE - ); - - txBuilder.addInstruction(collectFeexTx.compressIx(false)); + await builder.addInstructions(async (resolveTokenAccount) => { + const tokenOwnerAccountA = resolveTokenAccount(whirlpool.tokenMintA.toBase58()); + const tokenOwnerAccountB = resolveTokenAccount(whirlpool.tokenMintB.toBase58()); + + const collectFeesBaseParams = { + whirlpool: positionData.whirlpool, + position: positionAddress, + positionAuthority: positionWallet, + positionTokenAccount, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA: whirlpool.tokenVaultA, + tokenVaultB: whirlpool.tokenVaultB, + }; + + const ix = !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx) + ? WhirlpoolIx.collectFeesIx(this.ctx.program, collectFeesBaseParams) + : WhirlpoolIx.collectFeesV2Ix(this.ctx.program, { + ...collectFeesBaseParams, + tokenMintA: tokenExtensionCtx.tokenMintWithProgramA.address, + tokenMintB: tokenExtensionCtx.tokenMintWithProgramB.address, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + ...await TokenExtensionUtil.getExtraAccountMetasForTransferHookForPool( + this.ctx.connection, + tokenExtensionCtx, + collectFeesBaseParams.tokenVaultA, + collectFeesBaseParams.tokenOwnerAccountA, + collectFeesBaseParams.whirlpool, // vault to owner, so pool is authority + collectFeesBaseParams.tokenVaultB, + collectFeesBaseParams.tokenOwnerAccountB, + collectFeesBaseParams.whirlpool, // vault to owner, so pool is authority + ), + }); + + return [ix]; + }); } if (shouldCollectRewards) { - const collectRewardsTx = await position.collectRewards( - rewardsToCollect, - false, - walletTokenAccountsByMint, - destinationWallet, - positionWallet, - payerKey - ); - - txBuilder.addInstruction(collectRewardsTx.compressIx(false)); + for (let rewardIndex = 0; rewardIndex < rewardsToCollect.length; rewardIndex++) { + await builder.addInstructions(async (resolveTokenAccount) => { + const rewardOwnerAccount = resolveTokenAccount(rewardsToCollect[rewardIndex].toBase58()); + + const collectRewardBaseParams = { + whirlpool: positionData.whirlpool, + position: positionAddress, + positionAuthority: positionWallet, + positionTokenAccount, + rewardIndex, + rewardOwnerAccount, + rewardVault: whirlpool.rewardInfos[rewardIndex].vault, + }; + + + const ix = !TokenExtensionUtil.isV2IxRequiredReward(tokenExtensionCtx, rewardIndex) + ? WhirlpoolIx.collectRewardIx(this.ctx.program, collectRewardBaseParams) + : WhirlpoolIx.collectRewardV2Ix(this.ctx.program, { + ...collectRewardBaseParams, + rewardMint: tokenExtensionCtx.rewardTokenMintsWithProgram[rewardIndex]!.address, + rewardTokenProgram: tokenExtensionCtx.rewardTokenMintsWithProgram[rewardIndex]!.tokenProgram, + rewardTransferHookAccounts: await TokenExtensionUtil.getExtraAccountMetasForTransferHook( + this.ctx.connection, + tokenExtensionCtx.rewardTokenMintsWithProgram[rewardIndex]!, + collectRewardBaseParams.rewardVault, + collectRewardBaseParams.rewardOwnerAccount, + collectRewardBaseParams.whirlpool, // vault to owner, so pool is authority + ), + }); + + return [ix]; + }); + } } /* Close position */ - const positionIx = closePositionIx(this.ctx.program, { - positionAuthority: positionWallet, - receiver: destinationWallet, - positionTokenAccount, - position: positionAddress, - positionMint: positionData.positionMint, + await builder.addInstructions(async () => { + const ix = closePositionIx(this.ctx.program, { + positionAuthority: positionWallet, + receiver: destinationWallet, + positionTokenAccount, + position: positionAddress, + positionMint: positionData.positionMint, + }); + + return [ix]; }); - txBuilder.addInstruction(positionIx); - - if (tokenAccountsTxBuilder.isEmpty()) { - return [txBuilder] - } - - // This handles an edge case where the instructions are too - // large to fit in a single transaction and we need to split the - // instructions into two transactions. - const canFitInOneTransaction = await checkMergedTransactionSizeIsValid( - this.ctx, - [tokenAccountsTxBuilder, txBuilder], - MEASUREMENT_BLOCKHASH - ) - if (!canFitInOneTransaction) { - return [tokenAccountsTxBuilder, txBuilder] - } - - tokenAccountsTxBuilder.addInstruction(txBuilder.compressIx(false)); - return [tokenAccountsTxBuilder] + return builder.build(); } private async refresh() { diff --git a/sdk/src/instructions/composites/collect-all-txn.ts b/sdk/src/instructions/composites/collect-all-txn.ts index bee6b0abf..9da2931cb 100644 --- a/sdk/src/instructions/composites/collect-all-txn.ts +++ b/sdk/src/instructions/composites/collect-all-txn.ts @@ -17,6 +17,7 @@ import { PDAUtil, PoolUtil, TickUtil } from "../../utils/public"; import { checkMergedTransactionSizeIsValid, convertListToMap } from "../../utils/txn-utils"; import { getTokenMintsFromWhirlpools } from "../../utils/whirlpool-ata-utils"; import { updateFeesAndRewardsIx } from "../update-fees-and-rewards-ix"; +import { TokenExtensionUtil } from "../../utils/public/token-extension-util"; /** * Parameters to collect all fees and rewards from a list of positions. @@ -119,6 +120,9 @@ export async function collectAllForPositionsTxns( const allMints = getTokenMintsFromWhirlpools(Array.from(whirlpools.values())); const accountExemption = await ctx.fetcher.getAccountRentExempt(); + // make cache + await ctx.fetcher.getMintInfos(allMints.mintMap); + // resolvedAtas[mint] => Instruction & { address } // if already ATA exists, Instruction will be EMPTY_INSTRUCTION const resolvedAtas = convertListToMap( @@ -138,11 +142,47 @@ export async function collectAllForPositionsTxns( const latestBlockhash = await ctx.connection.getLatestBlockhash(); const txBuilders: TransactionBuilder[] = []; - let posIndex = 0; + // build tasks + // For TokenProgram-TokenProgram pair pool, collectFees and 3 collectReward instructions can be packed into one transaction. + // But if pool has TokenExtension, especially TransferHook, we can no longer pack all instructions into one transaction. + // So transactions need to be broken up at a finer granularity. + const collectionTasks: CollectionTask[] = []; + positionList.forEach(([positionAddr, position]) => { + const whirlpool = whirlpools.get(position.whirlpool.toBase58()); + if (!whirlpool) { + throw new Error( + `Unable to process positionMint ${position.positionMint.toBase58()} - unable to derive whirlpool ${position.whirlpool.toBase58()}` + ); + } + + // add fee collection task + collectionTasks.push({ + collectionType: "fee", + positionAddr, + position, + whirlpool, + }); + + // add reward collection task + whirlpool.rewardInfos.forEach((rewardInfo, index) => { + if (PoolUtil.isRewardInitialized(rewardInfo)) { + collectionTasks.push({ + collectionType: "reward", + rewardIndex: index, + positionAddr, + position, + whirlpool, + }); + } + }) + }); + + let cursor = 0; let pendingTxBuilder = null; let touchedMints = null; + let lastUpdatedPosition = null; let reattempt = false; - while (posIndex < positionList.length) { + while (cursor < collectionTasks.length) { if (!pendingTxBuilder || !touchedMints) { pendingTxBuilder = new TransactionBuilder(ctx.connection, ctx.wallet, ctx.txBuilderOpts); touchedMints = new Set(); @@ -157,12 +197,12 @@ export async function collectAllForPositionsTxns( } // Build collect instructions - const [positionAddr, position] = positionList[posIndex]; - const collectIxForPosition = constructCollectIxForPosition( + const task = collectionTasks[cursor]; + const alreadyUpdated = lastUpdatedPosition === task.positionAddr; + const collectIxForPosition = await constructCollectIxForPosition( ctx, - new PublicKey(positionAddr), - position, - whirlpools, + task, + alreadyUpdated, positionOwnerKey, positionAuthorityKey, resolvedAtas, @@ -172,7 +212,7 @@ export async function collectAllForPositionsTxns( positionTxBuilder.addInstructions(collectIxForPosition); // Attempt to push the new instructions into the pending builder - // Iterate to the next position if possible + // Iterate to the next task if possible // Create a builder and reattempt if the current one is full. const mergeable = await checkMergedTransactionSizeIsValid( ctx, @@ -181,18 +221,20 @@ export async function collectAllForPositionsTxns( ); if (mergeable) { pendingTxBuilder.addInstruction(positionTxBuilder.compressIx(false)); - posIndex += 1; + cursor += 1; + lastUpdatedPosition = task.positionAddr; reattempt = false; } else { if (reattempt) { throw new Error( - `Unable to fit collection ix for ${position.positionMint.toBase58()} in a Transaction.` + `Unable to fit collection ix for ${task.position.positionMint.toBase58()} in a Transaction.` ); } txBuilders.push(pendingTxBuilder); pendingTxBuilder = null; touchedMints = null; + lastUpdatedPosition = null; reattempt = true; } } @@ -203,16 +245,29 @@ export async function collectAllForPositionsTxns( return txBuilders; } +type CollectionTask = FeeCollectionTask | RewardCollectionTask; +type FeeCollectionTask = { + collectionType: "fee"; +} & CollectionTaskBase; +type RewardCollectionTask = { + collectionType: "reward"; + rewardIndex: number; +} & CollectionTaskBase; +type CollectionTaskBase = { + positionAddr: string; + position: PositionData; + whirlpool: WhirlpoolData; +}; + // TODO: Once individual collect ix for positions is implemented, maybe migrate over if it can take custom ATA? -const constructCollectIxForPosition = ( +const constructCollectIxForPosition = async ( ctx: WhirlpoolContext, - positionKey: PublicKey, - position: PositionData, - whirlpools: ReadonlyMap, + task: CollectionTask, + alreadyUpdated: boolean, positionOwner: PublicKey, positionAuthority: PublicKey, resolvedAtas: Record, - touchedMints: Set + touchedMints: Set, ) => { const ixForPosition: Instruction[] = []; const { @@ -222,18 +277,19 @@ const constructCollectIxForPosition = ( tickUpperIndex, positionMint, rewardInfos: positionRewardInfos, - } = position; + } = task.position; - const whirlpool = whirlpools.get(whirlpoolKey.toBase58()); - if (!whirlpool) { - throw new Error( - `Unable to process positionMint ${positionMint} - unable to derive whirlpool ${whirlpoolKey.toBase58()}` - ); - } + const whirlpool = task.whirlpool; const { tickSpacing } = whirlpool; const mintA = whirlpool.tokenMintA.toBase58(); const mintB = whirlpool.tokenMintB.toBase58(); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext( + ctx.fetcher, + whirlpool, + PREFER_CACHE, + ); + const positionTokenAccount = getAssociatedTokenAddressSync( positionMint, positionOwner, @@ -241,10 +297,10 @@ const constructCollectIxForPosition = ( ); // Update fee and reward values if necessary - if (!liquidity.eq(ZERO)) { + if (!liquidity.eq(ZERO) && !alreadyUpdated) { ixForPosition.push( updateFeesAndRewardsIx(ctx.program, { - position: positionKey, + position: new PublicKey(task.positionAddr), whirlpool: whirlpoolKey, tickArrayLower: PDAUtil.getTickArray( ctx.program.programId, @@ -260,51 +316,86 @@ const constructCollectIxForPosition = ( ); } - // Collect Fee - if (!touchedMints.has(mintA)) { - ixForPosition.push(resolvedAtas[mintA]); - touchedMints.add(mintA); - } - if (!touchedMints.has(mintB)) { - ixForPosition.push(resolvedAtas[mintB]); - touchedMints.add(mintB); - } - ixForPosition.push( - WhirlpoolIx.collectFeesIx(ctx.program, { + if (task.collectionType === "fee") { + // Collect Fee + + if (!touchedMints.has(mintA)) { + ixForPosition.push(resolvedAtas[mintA]); + touchedMints.add(mintA); + } + if (!touchedMints.has(mintB)) { + ixForPosition.push(resolvedAtas[mintB]); + touchedMints.add(mintB); + } + const collectFeesBaseParams = { whirlpool: whirlpoolKey, - position: positionKey, + position: new PublicKey(task.positionAddr), positionAuthority, positionTokenAccount, tokenOwnerAccountA: resolvedAtas[mintA].address, tokenOwnerAccountB: resolvedAtas[mintB].address, tokenVaultA: whirlpool.tokenVaultA, tokenVaultB: whirlpool.tokenVaultB, - }) - ); + }; + ixForPosition.push( + !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx) + ? WhirlpoolIx.collectFeesIx(ctx.program, collectFeesBaseParams) + : WhirlpoolIx.collectFeesV2Ix(ctx.program, { + ...collectFeesBaseParams, + tokenMintA: tokenExtensionCtx.tokenMintWithProgramA.address, + tokenMintB: tokenExtensionCtx.tokenMintWithProgramB.address, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + ...await TokenExtensionUtil.getExtraAccountMetasForTransferHookForPool( + ctx.connection, + tokenExtensionCtx, + collectFeesBaseParams.tokenVaultA, + collectFeesBaseParams.tokenOwnerAccountA, + collectFeesBaseParams.whirlpool, // vault to owner, so pool is authority + collectFeesBaseParams.tokenVaultB, + collectFeesBaseParams.tokenOwnerAccountB, + collectFeesBaseParams.whirlpool, // vault to owner, so pool is authority + ), + }) + ); + } else { + // Collect Rewards - // Collect Rewards - // TODO: handle empty vault values? - positionRewardInfos.forEach((_, index) => { + // TODO: handle empty vault values? + const index = task.rewardIndex; const rewardInfo = whirlpool.rewardInfos[index]; - if (PoolUtil.isRewardInitialized(rewardInfo)) { - const mintReward = rewardInfo.mint.toBase58(); - if (!touchedMints.has(mintReward)) { - ixForPosition.push(resolvedAtas[mintReward]); - touchedMints.add(mintReward); - } - ixForPosition.push( - WhirlpoolIx.collectRewardIx(ctx.program, { - whirlpool: whirlpoolKey, - position: positionKey, - positionAuthority, - positionTokenAccount, - rewardIndex: index, - rewardOwnerAccount: resolvedAtas[mintReward].address, - rewardVault: rewardInfo.vault, - }) - ); + + const mintReward = rewardInfo.mint.toBase58(); + if (!touchedMints.has(mintReward)) { + ixForPosition.push(resolvedAtas[mintReward]); + touchedMints.add(mintReward); } - }); + const collectRewardBaseParams = { + whirlpool: whirlpoolKey, + position: new PublicKey(task.positionAddr), + positionAuthority, + positionTokenAccount, + rewardIndex: index, + rewardOwnerAccount: resolvedAtas[mintReward].address, + rewardVault: rewardInfo.vault, + }; + ixForPosition.push( + !TokenExtensionUtil.isV2IxRequiredReward(tokenExtensionCtx, index) + ? WhirlpoolIx.collectRewardIx(ctx.program, collectRewardBaseParams) + : WhirlpoolIx.collectRewardV2Ix(ctx.program, { + ...collectRewardBaseParams, + rewardMint: tokenExtensionCtx.rewardTokenMintsWithProgram[index]!.address, + rewardTokenProgram: tokenExtensionCtx.rewardTokenMintsWithProgram[index]!.tokenProgram, + rewardTransferHookAccounts: await TokenExtensionUtil.getExtraAccountMetasForTransferHook( + ctx.connection, + tokenExtensionCtx.rewardTokenMintsWithProgram[index]!, + collectRewardBaseParams.rewardVault, + collectRewardBaseParams.rewardOwnerAccount, + collectRewardBaseParams.whirlpool, // vault to owner, so pool is authority + ), + }) + ); + } return ixForPosition; }; diff --git a/sdk/src/instructions/composites/collect-protocol-fees.ts b/sdk/src/instructions/composites/collect-protocol-fees.ts index 6a7aa5233..b4aa72b0b 100644 --- a/sdk/src/instructions/composites/collect-protocol-fees.ts +++ b/sdk/src/instructions/composites/collect-protocol-fees.ts @@ -11,6 +11,8 @@ import { resolveAtaForMints, } from "../../utils/whirlpool-ata-utils"; import { collectProtocolFeesIx } from "../collect-protocol-fees-ix"; +import { TokenExtensionUtil } from "../../utils/public/token-extension-util"; +import { collectProtocolFeesV2Ix } from "../v2"; export async function collectProtocolFees( ctx: WhirlpoolContext, @@ -23,9 +25,13 @@ export async function collectProtocolFees( (await ctx.fetcher.getPools(poolAddresses, PREFER_CACHE)).values() ); + // make cache + const mints = getTokenMintsFromWhirlpools(whirlpoolDatas, TokenMintTypes.POOL_ONLY).mintMap; + await ctx.fetcher.getMintInfos(mints); + const accountExemption = await ctx.fetcher.getAccountRentExempt(); const { ataTokenAddresses, resolveAtaIxs } = await resolveAtaForMints(ctx, { - mints: getTokenMintsFromWhirlpools(whirlpoolDatas, TokenMintTypes.POOL_ONLY).mintMap, + mints: mints, accountExemption, receiver: receiverKey, payer: payerKey, @@ -69,17 +75,43 @@ export async function collectProtocolFees( ); } + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext( + ctx.fetcher, + pool, + PREFER_CACHE, + ); + + const baseParams = { + whirlpoolsConfig: pool.whirlpoolsConfig, + whirlpool: AddressUtil.toPubKey(poolAddress), + tokenVaultA: pool.tokenVaultA, + tokenVaultB: pool.tokenVaultB, + tokenOwnerAccountA: ataTokenAddresses[pool.tokenMintA.toBase58()], + tokenOwnerAccountB: ataTokenAddresses[pool.tokenMintB.toBase58()], + collectProtocolFeesAuthority: poolConfig.collectProtocolFeesAuthority, + }; + // add collect ixn instructions.push( - collectProtocolFeesIx(ctx.program, { - whirlpoolsConfig: pool.whirlpoolsConfig, - whirlpool: AddressUtil.toPubKey(poolAddress), - tokenVaultA: pool.tokenVaultA, - tokenVaultB: pool.tokenVaultB, - tokenOwnerAccountA: ataTokenAddresses[pool.tokenMintA.toBase58()], - tokenOwnerAccountB: ataTokenAddresses[pool.tokenMintB.toBase58()], - collectProtocolFeesAuthority: poolConfig.collectProtocolFeesAuthority, - }) + !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx) + ? collectProtocolFeesIx(ctx.program, baseParams) + : collectProtocolFeesV2Ix(ctx.program, { + ...baseParams, + tokenMintA: tokenExtensionCtx.tokenMintWithProgramA.address, + tokenMintB: tokenExtensionCtx.tokenMintWithProgramB.address, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + ...await TokenExtensionUtil.getExtraAccountMetasForTransferHookForPool( + ctx.connection, + tokenExtensionCtx, + baseParams.tokenVaultA, + baseParams.tokenOwnerAccountA, + baseParams.whirlpool, // vault to protocol, so pool is authority + baseParams.tokenVaultB, + baseParams.tokenOwnerAccountB, + baseParams.whirlpool, // vault to protocol, so pool is authority + ), + }) ); } diff --git a/sdk/src/instructions/composites/swap-async.ts b/sdk/src/instructions/composites/swap-async.ts index 19385654f..4bfc2d8ac 100644 --- a/sdk/src/instructions/composites/swap-async.ts +++ b/sdk/src/instructions/composites/swap-async.ts @@ -3,6 +3,8 @@ import { PublicKey } from "@solana/web3.js"; import { SwapUtils, TickArrayUtil, Whirlpool, WhirlpoolContext } from "../.."; import { WhirlpoolAccountFetchOptions } from "../../network/public/fetcher"; import { SwapInput, swapIx } from "../swap-ix"; +import { TokenExtensionUtil } from "../../utils/public/token-extension-util"; +import { swapV2Ix } from "../v2"; export type SwapAsyncParams = { swapInput: SwapInput; @@ -56,17 +58,38 @@ export async function swapAsync( const inputTokenAccount = aToB ? ataAKey : ataBKey; const outputTokenAccount = aToB ? ataBKey : ataAKey; + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext( + ctx.fetcher, + data, + ); + + const baseParams = SwapUtils.getSwapParamsFromQuote( + swapInput, + ctx, + whirlpool, + inputTokenAccount, + outputTokenAccount, + wallet + ); return txBuilder.addInstruction( - swapIx( - ctx.program, - SwapUtils.getSwapParamsFromQuote( - swapInput, - ctx, - whirlpool, - inputTokenAccount, - outputTokenAccount, - wallet - ) - ) + !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx) + ? swapIx(ctx.program, baseParams) + : swapV2Ix(ctx.program, { + ...baseParams, + tokenMintA: tokenExtensionCtx.tokenMintWithProgramA.address, + tokenMintB: tokenExtensionCtx.tokenMintWithProgramB.address, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + ...await TokenExtensionUtil.getExtraAccountMetasForTransferHookForPool( + ctx.connection, + tokenExtensionCtx, + baseParams.aToB ? baseParams.tokenOwnerAccountA : baseParams.tokenVaultA, + baseParams.aToB ? baseParams.tokenVaultA : baseParams.tokenOwnerAccountA, + baseParams.aToB ? baseParams.tokenAuthority : baseParams.whirlpool, + baseParams.aToB ? baseParams.tokenVaultB : baseParams.tokenOwnerAccountB, + baseParams.aToB ? baseParams.tokenOwnerAccountB : baseParams.tokenVaultB, + baseParams.aToB ? baseParams.whirlpool : baseParams.tokenAuthority, + ), + }) ); } diff --git a/sdk/src/instructions/composites/swap-with-route.ts b/sdk/src/instructions/composites/swap-with-route.ts index 53118bb18..b932d4adc 100644 --- a/sdk/src/instructions/composites/swap-with-route.ts +++ b/sdk/src/instructions/composites/swap-with-route.ts @@ -10,6 +10,7 @@ import { import { Account, NATIVE_MINT, + TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, createCloseAccountInstruction, getAssociatedTokenAddressSync, @@ -377,7 +378,8 @@ async function cachedResolveOrCreateNonNativeATAs( signers: [], }; } - instructionMap[tokenMintArray[index].toBase58()] = resolvedInstruction; + // WhirlpoolRouter does not handle TokenExtension, so token program is always standard TokenProgram. + instructionMap[tokenMintArray[index].toBase58()] = { tokenProgram: TOKEN_PROGRAM_ID, ...resolvedInstruction }; }); return instructionMap; diff --git a/sdk/src/instructions/index.ts b/sdk/src/instructions/index.ts index b22aee16c..388a00a3b 100644 --- a/sdk/src/instructions/index.ts +++ b/sdk/src/instructions/index.ts @@ -28,3 +28,4 @@ export * from "./set-reward-emissions-super-authority-ix"; export * from "./swap-ix"; export * from "./two-hop-swap-ix"; export * from "./update-fees-and-rewards-ix"; +export * from "./v2"; diff --git a/sdk/src/instructions/v2/collect-fees-ix.ts b/sdk/src/instructions/v2/collect-fees-ix.ts new file mode 100644 index 000000000..ebd2a2c96 --- /dev/null +++ b/sdk/src/instructions/v2/collect-fees-ix.ts @@ -0,0 +1,101 @@ +import { Program } from "@coral-xyz/anchor"; +import { AccountMeta, PublicKey } from "@solana/web3.js"; +import { Whirlpool } from "../../artifacts/whirlpool"; +import { MEMO_PROGRAM_ADDRESS } from "../.."; + +import { Instruction } from "@orca-so/common-sdk"; +import { RemainingAccountsBuilder, RemainingAccountsType } from "../../utils/remaining-accounts-util"; + +/** + * Parameters to collect fees from a position. + * + * @category Instruction Types + * @param whirlpool - PublicKey for the whirlpool that the position will be opened for. + * @param position - PublicKey for the position will be opened for. + * @param positionTokenAccount - PublicKey for the position token's associated token address. + * @param positionAuthority - authority that owns the token corresponding to this desired position. + * @param tokenMintA - PublicKey for the token A mint. + * @param tokenMintB - PublicKey for the token B mint. + * @param tokenOwnerAccountA - PublicKey for the token A account that will be withdrawed from. + * @param tokenOwnerAccountB - PublicKey for the token B account that will be withdrawed from. + * @param tokenVaultA - PublicKey for the tokenA vault for this whirlpool. + * @param tokenVaultB - PublicKey for the tokenB vault for this whirlpool. + * @param tokenTransferHookAccountsA - Optional array of token transfer hook accounts for token A. + * @param tokenTransferHookAccountsB - Optional array of token transfer hook accounts for token B. + * @param tokenProgramA - PublicKey for the token program for token A. + * @param tokenProgramB - PublicKey for the token program for token B. + */ +export type CollectFeesV2Params = { + whirlpool: PublicKey; + position: PublicKey; + positionTokenAccount: PublicKey; + positionAuthority: PublicKey; + tokenMintA: PublicKey; + tokenMintB: PublicKey; + tokenOwnerAccountA: PublicKey; + tokenOwnerAccountB: PublicKey; + tokenVaultA: PublicKey; + tokenVaultB: PublicKey; + tokenTransferHookAccountsA?: AccountMeta[]; + tokenTransferHookAccountsB?: AccountMeta[]; + tokenProgramA: PublicKey; + tokenProgramB: PublicKey; +}; + +/** + * Collect fees accrued for this position. + * Call updateFeesAndRewards before this to update the position to the newest accrued values. + * + * @category Instructions + * @param context - Context object containing services required to generate the instruction + * @param params - CollectFeesV2Params object + * @returns - Instruction to perform the action. + */ +export function collectFeesV2Ix(program: Program, params: CollectFeesV2Params): Instruction { + const { + whirlpool, + positionAuthority, + position, + positionTokenAccount, + tokenMintA, + tokenMintB, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA, + tokenVaultB, + tokenTransferHookAccountsA, + tokenTransferHookAccountsB, + tokenProgramA, + tokenProgramB, + } = params; + + const [remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder() + .addSlice(RemainingAccountsType.TransferHookA, tokenTransferHookAccountsA) + .addSlice(RemainingAccountsType.TransferHookB, tokenTransferHookAccountsB) + .build(); + + const ix = program.instruction.collectFeesV2(remainingAccountsInfo, { + accounts: { + whirlpool, + positionAuthority, + position, + positionTokenAccount, + tokenMintA, + tokenMintB, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA, + tokenVaultB, + tokenProgramA, + tokenProgramB, + memoProgram: MEMO_PROGRAM_ADDRESS, + }, + remainingAccounts, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }; +} diff --git a/sdk/src/instructions/v2/collect-protocol-fees-ix.ts b/sdk/src/instructions/v2/collect-protocol-fees-ix.ts new file mode 100644 index 000000000..501ca04f3 --- /dev/null +++ b/sdk/src/instructions/v2/collect-protocol-fees-ix.ts @@ -0,0 +1,98 @@ +import { Program } from "@coral-xyz/anchor"; +import { Instruction } from "@orca-so/common-sdk"; +import { AccountMeta, PublicKey } from "@solana/web3.js"; +import { Whirlpool } from "../../artifacts/whirlpool"; +import { MEMO_PROGRAM_ADDRESS } from "../.."; +import { RemainingAccountsBuilder, RemainingAccountsType } from "../../utils/remaining-accounts-util"; + +/** + * Parameters to collect protocol fees for a Whirlpool + * + * @category Instruction Types + * @param whirlpoolsConfig - The public key for the WhirlpoolsConfig this pool is initialized in + * @param whirlpool - PublicKey for the whirlpool that the position will be opened for. + * @param collectProtocolFeesAuthority - assigned authority in the WhirlpoolsConfig that can collect protocol fees + * @param tokenMintA - PublicKey for the token A mint. + * @param tokenMintB - PublicKey for the token B mint. + * @param tokenVaultA - PublicKey for the tokenA vault for this whirlpool. + * @param tokenVaultB - PublicKey for the tokenB vault for this whirlpool. + * @param tokenOwnerAccountA - PublicKey for the associated token account for tokenA in the collection wallet + * @param tokenOwnerAccountB - PublicKey for the associated token account for tokenA in the collection wallet + * @param tokenTransferHookAccountsA - Optional array of token transfer hook accounts for token A. + * @param tokenTransferHookAccountsB - Optional array of token transfer hook accounts for token B. + * @param tokenProgramA - PublicKey for the token program for token A. + * @param tokenProgramB - PublicKey for the token program for token B. + */ +export type CollectProtocolFeesV2Params = { + whirlpoolsConfig: PublicKey; + whirlpool: PublicKey; + collectProtocolFeesAuthority: PublicKey; + tokenMintA: PublicKey; + tokenMintB: PublicKey; + tokenVaultA: PublicKey; + tokenVaultB: PublicKey; + tokenOwnerAccountA: PublicKey; + tokenOwnerAccountB: PublicKey; + tokenTransferHookAccountsA?: AccountMeta[]; + tokenTransferHookAccountsB?: AccountMeta[]; + tokenProgramA: PublicKey; + tokenProgramB: PublicKey; +}; + +/** + * Collect protocol fees accrued in this Whirlpool. + * + * @category Instructions + * @param context - Context object containing services required to generate the instruction + * @param params - CollectProtocolFeesV2Params object + * @returns - Instruction to perform the action. + */ +export function collectProtocolFeesV2Ix( + program: Program, + params: CollectProtocolFeesV2Params +): Instruction { + const { + whirlpoolsConfig, + whirlpool, + collectProtocolFeesAuthority, + tokenMintA, + tokenMintB, + tokenVaultA, + tokenVaultB, + tokenTransferHookAccountsA, + tokenTransferHookAccountsB, + tokenOwnerAccountA: tokenDestinationA, + tokenOwnerAccountB: tokenDestinationB, + tokenProgramA, + tokenProgramB, + } = params; + + const [remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder() + .addSlice(RemainingAccountsType.TransferHookA, tokenTransferHookAccountsA) + .addSlice(RemainingAccountsType.TransferHookB, tokenTransferHookAccountsB) + .build(); + + const ix = program.instruction.collectProtocolFeesV2(remainingAccountsInfo, { + accounts: { + whirlpoolsConfig, + whirlpool, + collectProtocolFeesAuthority, + tokenMintA, + tokenMintB, + tokenVaultA, + tokenVaultB, + tokenDestinationA, + tokenDestinationB, + tokenProgramA, + tokenProgramB, + memoProgram: MEMO_PROGRAM_ADDRESS, + }, + remainingAccounts, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }; +} diff --git a/sdk/src/instructions/v2/collect-reward-ix.ts b/sdk/src/instructions/v2/collect-reward-ix.ts new file mode 100644 index 000000000..52c06fcab --- /dev/null +++ b/sdk/src/instructions/v2/collect-reward-ix.ts @@ -0,0 +1,86 @@ +import { Program } from "@coral-xyz/anchor"; +import { Instruction } from "@orca-so/common-sdk"; +import { AccountMeta, PublicKey } from "@solana/web3.js"; +import { Whirlpool } from "../../artifacts/whirlpool"; +import { MEMO_PROGRAM_ADDRESS } from "../.."; +import { RemainingAccountsBuilder, RemainingAccountsType } from "../../utils/remaining-accounts-util"; + +/** + * Parameters to collect rewards from a reward index in a position. + * + * @category Instruction Types + * @param whirlpool - PublicKey for the whirlpool that the position will be opened for. + * @param position - PublicKey for the position will be opened for. + * @param positionTokenAccount - PublicKey for the position token's associated token address. + * @param positionAuthority - authority that owns the token corresponding to this desired position. + * @param rewardIndex - The reward index that we'd like to initialize. (0 <= index <= NUM_REWARDS). + * @param rewardMint - PublicKey for the reward token mint. + * @param rewardOwnerAccount - PublicKey for the reward token account that the reward will deposit into. + * @param rewardVault - PublicKey of the vault account that reward will be withdrawn from. + * @param rewardTransferHookAccounts - Optional array of token transfer hook accounts for the reward token. + * @param rewardTokenProgram - PublicKey for the token program. + */ +export type CollectRewardV2Params = { + whirlpool: PublicKey; + position: PublicKey; + positionTokenAccount: PublicKey; + positionAuthority: PublicKey; + rewardIndex: number; + rewardMint: PublicKey; + rewardOwnerAccount: PublicKey; + rewardVault: PublicKey; + rewardTransferHookAccounts?: AccountMeta[]; + rewardTokenProgram: PublicKey; +}; + +/** + * Collect rewards accrued for this reward index in a position. + * Call updateFeesAndRewards before this to update the position to the newest accrued values. + * + * @category Instructions + * @param context - Context object containing services required to generate the instruction + * @param params - CollectRewardV2Params object + * @returns - Instruction to perform the action. + */ +export function collectRewardV2Ix( + program: Program, + params: CollectRewardV2Params +): Instruction { + const { + whirlpool, + positionAuthority, + position, + positionTokenAccount, + rewardMint, + rewardOwnerAccount, + rewardVault, + rewardTransferHookAccounts, + rewardIndex, + rewardTokenProgram, + } = params; + + const [remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder() + .addSlice(RemainingAccountsType.TransferHookReward, rewardTransferHookAccounts) + .build(); + + const ix = program.instruction.collectRewardV2(rewardIndex, remainingAccountsInfo, { + accounts: { + whirlpool, + positionAuthority, + position, + positionTokenAccount, + rewardMint, + rewardOwnerAccount, + rewardVault, + rewardTokenProgram, + memoProgram: MEMO_PROGRAM_ADDRESS, + }, + remainingAccounts, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }; +} diff --git a/sdk/src/instructions/v2/decrease-liquidity-ix.ts b/sdk/src/instructions/v2/decrease-liquidity-ix.ts new file mode 100644 index 000000000..19d7a427a --- /dev/null +++ b/sdk/src/instructions/v2/decrease-liquidity-ix.ts @@ -0,0 +1,121 @@ +import { Program } from "@coral-xyz/anchor"; +import { Instruction } from "@orca-so/common-sdk"; +import { AccountMeta, PublicKey } from "@solana/web3.js"; +import { Whirlpool } from "../../artifacts/whirlpool"; +import { DecreaseLiquidityInput, MEMO_PROGRAM_ADDRESS } from "../.."; +import { RemainingAccountsBuilder, RemainingAccountsType } from "../../utils/remaining-accounts-util"; + +/** + * Parameters to remove liquidity from a position. + * + * @category Instruction Types + * @param liquidityAmount - The total amount of Liquidity the user is withdrawing + * @param tokenMinA - The minimum amount of token A to remove from the position. + * @param tokenMinB - The minimum amount of token B to remove from the position. + * @param whirlpool - PublicKey for the whirlpool that the position will be opened for. + * @param position - PublicKey for the position will be opened for. + * @param positionTokenAccount - PublicKey for the position token's associated token address. + * @param positionAuthority - authority that owns the token corresponding to this desired position. + * @param tokenMintA - PublicKey for the token A mint. + * @param tokenMintB - PublicKey for the token B mint. + * @param tokenOwnerAccountA - PublicKey for the token A account that will be withdrawed from. + * @param tokenOwnerAccountB - PublicKey for the token B account that will be withdrawed from. + * @param tokenVaultA - PublicKey for the tokenA vault for this whirlpool. + * @param tokenVaultB - PublicKey for the tokenB vault for this whirlpool. + * @param tokenTransferHookAccountsA - Optional array of token transfer hook accounts for token A. + * @param tokenTransferHookAccountsB - Optional array of token transfer hook accounts for token B. + * @param tokenProgramA - PublicKey for the token program for token A. + * @param tokenProgramB - PublicKey for the token program for token B. + * @param tickArrayLower - PublicKey for the tick-array account that hosts the tick at the lower tick index. + * @param tickArrayUpper - PublicKey for the tick-array account that hosts the tick at the upper tick index. + */ +export type DecreaseLiquidityV2Params = { + whirlpool: PublicKey; + position: PublicKey; + positionTokenAccount: PublicKey; + positionAuthority: PublicKey; + tokenMintA: PublicKey; + tokenMintB: PublicKey; + tokenOwnerAccountA: PublicKey; + tokenOwnerAccountB: PublicKey; + tokenVaultA: PublicKey; + tokenVaultB: PublicKey; + tokenTransferHookAccountsA?: AccountMeta[]; + tokenTransferHookAccountsB?: AccountMeta[]; + tokenProgramA: PublicKey; + tokenProgramB: PublicKey; + tickArrayLower: PublicKey; + tickArrayUpper: PublicKey; +} & DecreaseLiquidityInput; + +/** + * Remove liquidity to a position in the Whirlpool. + * + * #### Special Errors + * - `LiquidityZero` - Provided liquidity amount is zero. + * - `LiquidityTooHigh` - Provided liquidity exceeds u128::max. + * - `TokenMinSubceeded` - The required token to perform this operation subceeds the user defined amount. + * + * @category Instructions + * @param context - Context object containing services required to generate the instruction + * @param params - DecreaseLiquidityV2Params object + * @returns - Instruction to perform the action. + */ +export function decreaseLiquidityV2Ix( + program: Program, + params: DecreaseLiquidityV2Params +): Instruction { + const { + liquidityAmount, + tokenMinA, + tokenMinB, + whirlpool, + positionAuthority, + position, + positionTokenAccount, + tokenMintA, + tokenMintB, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA, + tokenVaultB, + tokenTransferHookAccountsA, + tokenTransferHookAccountsB, + tokenProgramA, + tokenProgramB, + tickArrayLower, + tickArrayUpper, + } = params; + + const [remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder() + .addSlice(RemainingAccountsType.TransferHookA, tokenTransferHookAccountsA) + .addSlice(RemainingAccountsType.TransferHookB, tokenTransferHookAccountsB) + .build(); + + const ix = program.instruction.decreaseLiquidityV2(liquidityAmount, tokenMinA, tokenMinB, remainingAccountsInfo, { + accounts: { + whirlpool, + positionAuthority, + position, + positionTokenAccount, + tokenMintA, + tokenMintB, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA, + tokenVaultB, + tokenProgramA, + tokenProgramB, + tickArrayLower, + tickArrayUpper, + memoProgram: MEMO_PROGRAM_ADDRESS, + }, + remainingAccounts, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }; +} diff --git a/sdk/src/instructions/v2/delete-token-badge-ix.ts b/sdk/src/instructions/v2/delete-token-badge-ix.ts new file mode 100644 index 000000000..89b0cf097 --- /dev/null +++ b/sdk/src/instructions/v2/delete-token-badge-ix.ts @@ -0,0 +1,63 @@ +import { Program } from "@coral-xyz/anchor"; +import { Instruction, PDA } from "@orca-so/common-sdk"; +import { PublicKey } from "@solana/web3.js"; +import { Whirlpool } from "../../artifacts/whirlpool"; + +/** + * Parameters to delete a TokenBadge account. + * + * @category Instruction Types + * @param whirlpoolsConfig - PublicKey for the whirlpools config account + * @param whirlpoolsConfigExtension - PublicKey for the whirlpools config extension account + * @param tokenBadgeAuthority - PublicKey for the token badge authority + * @param tokenMint - Publickey for the mint for which the TokenBadge have been initialized + * @param tokenBadge - PublicKey for the token badge account to be deleted + * @param receiver - PublicKey for the account that will receive the rent + */ +export type DeleteTokenBadgeParams = { + whirlpoolsConfig: PublicKey; + whirlpoolsConfigExtension: PublicKey; + tokenBadgeAuthority: PublicKey; + tokenMint: PublicKey; + tokenBadge: PublicKey; + receiver: PublicKey; +}; + +/** + * Deletes a TokenBadge account. + * + * @category Instructions + * @param program - program object containing services required to generate the instruction + * @param params - DeleteTokenBadgeParams object + * @returns - Instruction to perform the action. + */ +export function deleteTokenBadgeIx( + program: Program, + params: DeleteTokenBadgeParams +): Instruction { + const { + whirlpoolsConfig, + whirlpoolsConfigExtension, + tokenBadgeAuthority, + tokenMint, + tokenBadge, + receiver, + } = params; + + const ix = program.instruction.deleteTokenBadge({ + accounts: { + whirlpoolsConfig, + whirlpoolsConfigExtension, + tokenBadgeAuthority, + tokenMint, + tokenBadge, + receiver, + }, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }; +} diff --git a/sdk/src/instructions/v2/increase-liquidity-ix.ts b/sdk/src/instructions/v2/increase-liquidity-ix.ts new file mode 100644 index 000000000..747a37fb1 --- /dev/null +++ b/sdk/src/instructions/v2/increase-liquidity-ix.ts @@ -0,0 +1,118 @@ +import { Program } from "@coral-xyz/anchor"; +import { AccountMeta, PublicKey } from "@solana/web3.js"; +import { Whirlpool } from "../../artifacts/whirlpool"; +import { IncreaseLiquidityInput, MEMO_PROGRAM_ADDRESS } from "../.."; + +import { Instruction } from "@orca-so/common-sdk"; +import { RemainingAccountsBuilder, RemainingAccountsType } from "../../utils/remaining-accounts-util"; + +/** + * Parameters to increase liquidity for a position. + * + * @category Instruction Types + * @param liquidityAmount - The total amount of Liquidity the user is willing to deposit. + * @param tokenMaxA - The maximum amount of token A to add to the position. + * @param tokenMaxB - The maximum amount of token B to add to the position. + * @param whirlpool - PublicKey for the whirlpool that the position will be opened for. + * @param position - PublicKey for the position will be opened for. + * @param positionTokenAccount - PublicKey for the position token's associated token address. + * @param positionAuthority - authority that owns the token corresponding to this desired position. + * @param tokenOwnerAccountA - PublicKey for the token A account that will be withdrawed from. + * @param tokenOwnerAccountB - PublicKey for the token B account that will be withdrawed from. + * @param tokenVaultA - PublicKey for the tokenA vault for this whirlpool. + * @param tokenVaultB - PublicKey for the tokenB vault for this whirlpool. + * @param tokenTransferHookAccountsA - Optional array of token transfer hook accounts for token A. + * @param tokenTransferHookAccountsB - Optional array of token transfer hook accounts for token B. + * @param tickArrayLower - PublicKey for the tick-array account that hosts the tick at the lower tick index. + * @param tickArrayUpper - PublicKey for the tick-array account that hosts the tick at the upper tick index. + */ +export type IncreaseLiquidityV2Params = { + whirlpool: PublicKey; + position: PublicKey; + positionTokenAccount: PublicKey; + positionAuthority: PublicKey; + tokenMintA: PublicKey; + tokenMintB: PublicKey; + tokenOwnerAccountA: PublicKey; + tokenOwnerAccountB: PublicKey; + tokenVaultA: PublicKey; + tokenVaultB: PublicKey; + tokenTransferHookAccountsA?: AccountMeta[]; + tokenTransferHookAccountsB?: AccountMeta[]; + tokenProgramA: PublicKey; + tokenProgramB: PublicKey; + tickArrayLower: PublicKey; + tickArrayUpper: PublicKey; +} & IncreaseLiquidityInput; + +/** + * Add liquidity to a position in the Whirlpool. + * + * #### Special Errors + * `LiquidityZero` - Provided liquidity amount is zero. + * `LiquidityTooHigh` - Provided liquidity exceeds u128::max. + * `TokenMaxExceeded` - The required token to perform this operation exceeds the user defined amount. + * + * @category Instructions + * @param context - Context object containing services required to generate the instruction + * @param params - IncreaseLiquidityV2Params object + * @returns - Instruction to perform the action. + */ +export function increaseLiquidityV2Ix( + program: Program, + params: IncreaseLiquidityV2Params +): Instruction { + const { + liquidityAmount, + tokenMaxA, + tokenMaxB, + whirlpool, + positionAuthority, + position, + positionTokenAccount, + tokenMintA, + tokenMintB, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA, + tokenVaultB, + tokenTransferHookAccountsA, + tokenTransferHookAccountsB, + tokenProgramA, + tokenProgramB, + tickArrayLower, + tickArrayUpper, + } = params; + + const [remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder() + .addSlice(RemainingAccountsType.TransferHookA, tokenTransferHookAccountsA) + .addSlice(RemainingAccountsType.TransferHookB, tokenTransferHookAccountsB) + .build(); + + const ix = program.instruction.increaseLiquidityV2(liquidityAmount, tokenMaxA, tokenMaxB, remainingAccountsInfo, { + accounts: { + whirlpool, + positionAuthority, + position, + positionTokenAccount, + tokenMintA, + tokenMintB, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA, + tokenVaultB, + tokenProgramA, + tokenProgramB, + tickArrayLower, + tickArrayUpper, + memoProgram: MEMO_PROGRAM_ADDRESS, + }, + remainingAccounts, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }; +} diff --git a/sdk/src/instructions/v2/index.ts b/sdk/src/instructions/v2/index.ts new file mode 100644 index 000000000..fe9867b79 --- /dev/null +++ b/sdk/src/instructions/v2/index.ts @@ -0,0 +1,16 @@ +export * from "./collect-fees-ix"; +export * from "./collect-protocol-fees-ix"; +export * from "./collect-reward-ix"; +export * from "./decrease-liquidity-ix"; +export * from "./increase-liquidity-ix"; +export * from "./initialize-pool-ix"; +export * from "./initialize-reward-ix"; +export * from "./set-reward-emissions-ix"; +export * from "./swap-ix"; +export * from "./two-hop-swap-ix"; + +export * from "./initialize-config-extension-ix"; +export * from "./set-config-extension-authority-ix"; +export * from "./set-token-badge-authority-ix"; +export * from "./initialize-token-badge-ix"; +export * from "./delete-token-badge-ix"; diff --git a/sdk/src/instructions/v2/initialize-config-extension-ix.ts b/sdk/src/instructions/v2/initialize-config-extension-ix.ts new file mode 100644 index 000000000..d71e3d0fb --- /dev/null +++ b/sdk/src/instructions/v2/initialize-config-extension-ix.ts @@ -0,0 +1,55 @@ +import { Program } from "@coral-xyz/anchor"; +import { Instruction, PDA } from "@orca-so/common-sdk"; +import { PublicKey, SystemProgram } from "@solana/web3.js"; +import { Whirlpool } from "../../artifacts/whirlpool"; + +/** + * Parameters to initialize a WhirlpoolsConfigExtension account. + * + * @category Instruction Types + * @ + */ +export type InitConfigExtensionParams = { + whirlpoolsConfig: PublicKey; + whirlpoolsConfigExtensionPda: PDA; + funder: PublicKey; + feeAuthority: PublicKey; +}; + +/** + * Initializes a WhirlpoolsConfigExtension account that hosts info & authorities + * + * @category Instructions + * @param context - Context object containing services required to generate the instruction + * @param params - InitConfigExtensionParams object + * @returns - Instruction to perform the action. + */ +export function initializeConfigExtensionIx( + program: Program, + params: InitConfigExtensionParams +): Instruction { + const { + whirlpoolsConfig, + whirlpoolsConfigExtensionPda, + funder, + feeAuthority, + } = params; + + const ix = program.instruction.initializeConfigExtension( + { + accounts: { + config: whirlpoolsConfig, + configExtension: whirlpoolsConfigExtensionPda.publicKey, + funder, + feeAuthority, + systemProgram: SystemProgram.programId, + }, + } + ); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }; +} diff --git a/sdk/src/instructions/v2/initialize-pool-ix.ts b/sdk/src/instructions/v2/initialize-pool-ix.ts new file mode 100644 index 000000000..2fab1b002 --- /dev/null +++ b/sdk/src/instructions/v2/initialize-pool-ix.ts @@ -0,0 +1,96 @@ +import { BN, Program } from "@coral-xyz/anchor"; +import { Instruction, PDA } from "@orca-so/common-sdk"; +import { Keypair, PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } from "@solana/web3.js"; +import { Whirlpool } from "../../artifacts/whirlpool"; + +/** + * Parameters to initialize a Whirlpool account. + * + * @category Instruction Types + * @param initSqrtPrice - The desired initial sqrt-price for this pool + * @param whirlpoolsConfig - The public key for the WhirlpoolsConfig this pool is initialized in + * @param whirlpoolPda - PDA for the whirlpool account that would be initialized + * @param tokenMintA - Mint public key for token A + * @param tokenMintB - Mint public key for token B + * @param tokenBadgeA - TokenBadge public key for token A + * @param tokenBadgeB - TokenBadge public key for token B + * @param tokenProgramA - Token program public key for token A + * @param tokenProgramB - Token program public key for token B + * @param tokenVaultAKeypair - Keypair of the token A vault for this pool + * @param tokenVaultBKeypair - Keypair of the token B vault for this pool + * @param feeTierKey - PublicKey of the fee-tier account that this pool would use for the fee-rate + * @param tickSpacing - The desired tick spacing for this pool. + * @param funder - The account that would fund the creation of this account + */ +export type InitPoolV2Params = { + initSqrtPrice: BN; + whirlpoolsConfig: PublicKey; + whirlpoolPda: PDA; + tokenMintA: PublicKey; + tokenMintB: PublicKey; + tokenBadgeA: PublicKey; + tokenBadgeB: PublicKey; + tokenProgramA: PublicKey; + tokenProgramB: PublicKey; + tokenVaultAKeypair: Keypair; + tokenVaultBKeypair: Keypair; + feeTierKey: PublicKey; + tickSpacing: number; + funder: PublicKey; +}; + +/** + * Initializes a tick_array account to represent a tick-range in a Whirlpool. + * + * Special Errors + * `InvalidTokenMintOrder` - The order of mints have to be ordered by + * `SqrtPriceOutOfBounds` - provided initial_sqrt_price is not between 2^-64 to 2^64 + * + * @category Instructions + * @param context - Context object containing services required to generate the instruction + * @param params - InitPoolV2Params object + * @returns - Instruction to perform the action. + */ +export function initializePoolV2Ix(program: Program, params: InitPoolV2Params): Instruction { + const { + initSqrtPrice, + tokenMintA, + tokenMintB, + tokenBadgeA, + tokenBadgeB, + tokenProgramA, + tokenProgramB, + whirlpoolsConfig, + whirlpoolPda, + feeTierKey, + tokenVaultAKeypair, + tokenVaultBKeypair, + tickSpacing, + funder, + } = params; + + const ix = program.instruction.initializePoolV2(tickSpacing, initSqrtPrice, { + accounts: { + whirlpoolsConfig, + tokenMintA, + tokenMintB, + tokenBadgeA, + tokenBadgeB, + funder, + whirlpool: whirlpoolPda.publicKey, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + feeTier: feeTierKey, + systemProgram: SystemProgram.programId, + tokenProgramA, + tokenProgramB, + rent: SYSVAR_RENT_PUBKEY, + }, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [tokenVaultAKeypair, tokenVaultBKeypair], + }; +} diff --git a/sdk/src/instructions/v2/initialize-reward-ix.ts b/sdk/src/instructions/v2/initialize-reward-ix.ts new file mode 100644 index 000000000..9c84a12cf --- /dev/null +++ b/sdk/src/instructions/v2/initialize-reward-ix.ts @@ -0,0 +1,72 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { Keypair, PublicKey, SystemProgram } from "@solana/web3.js"; +import { Whirlpool } from "../../artifacts/whirlpool"; + +import { Instruction } from "@orca-so/common-sdk"; + +/** + * Parameters to initialize a rewards for a Whirlpool + * + * @category Instruction Types + * @param whirlpool - PublicKey for the whirlpool config space that the fee-tier will be initialized for. + * @param rewardIndex - The reward index that we'd like to initialize. (0 <= index <= NUM_REWARDS). + * @param rewardMint - PublicKey for the reward mint that we'd use for the reward index. + * @param rewardTokenBadge - PublicKey for the TokenBadge for this reward mint. + * @param rewardVaultKeypair - Keypair of the vault for this reward index. + * @param rewardAuthority - Assigned authority by the reward_super_authority for the specified reward-index in this Whirlpool + * @param funder - The account that would fund the creation of this account + * @param rewardTokenProgram - PublicKey for the token program. + */ +export type InitializeRewardV2Params = { + whirlpool: PublicKey; + rewardIndex: number; + rewardMint: PublicKey; + rewardTokenBadge: PublicKey; + rewardVaultKeypair: Keypair; + rewardAuthority: PublicKey; + funder: PublicKey; + rewardTokenProgram: PublicKey; +}; + +/** + * Initialize reward for a Whirlpool. A pool can only support up to a set number of rewards. + * The initial emissionsPerSecond is set to 0. + * + * #### Special Errors + * - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized index in this pool, + * or exceeds NUM_REWARDS, or all reward slots for this pool has been initialized. + * + * @category Instructions + * @param context - Context object containing services required to generate the instruction + * @param params - InitializeRewardV2Params object + * @returns - Instruction to perform the action. + */ +export function initializeRewardV2Ix( + program: Program, + params: InitializeRewardV2Params +): Instruction { + const { rewardAuthority, funder, whirlpool, rewardMint, rewardTokenBadge, rewardVaultKeypair, rewardIndex, rewardTokenProgram } = + params; + + const ix = program.instruction.initializeRewardV2(rewardIndex, { + accounts: { + rewardAuthority, + funder, + whirlpool, + rewardMint, + rewardTokenBadge, + rewardVault: rewardVaultKeypair.publicKey, + rewardTokenProgram, + systemProgram: SystemProgram.programId, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [rewardVaultKeypair], + }; +} diff --git a/sdk/src/instructions/v2/initialize-token-badge-ix.ts b/sdk/src/instructions/v2/initialize-token-badge-ix.ts new file mode 100644 index 000000000..0f4d805ba --- /dev/null +++ b/sdk/src/instructions/v2/initialize-token-badge-ix.ts @@ -0,0 +1,65 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { Instruction, PDA } from "@orca-so/common-sdk"; +import { PublicKey, SystemProgram } from "@solana/web3.js"; +import { Whirlpool } from "../../artifacts/whirlpool"; + +/** + * Parameters to initialize a TokenBadge account. + * + * @category Instruction Types + * @param whirlpoolsConfig - The public key for the WhirlpoolsConfig + * @param whirlpoolsConfigExtension - The public key for the WhirlpoolsConfigExtension + * @param tokenBadgeAuthority - The public key for the tokenBadgeAuthority + * @param tokenMint - The public key for the mint for which the TokenBadge is being initialized + * @param tokenBadgePda - The PDA for the TokenBadge account + * @param funder - The account that would fund the creation of this account + */ +export type InitializeTokenBadgeParams = { + whirlpoolsConfig: PublicKey; + whirlpoolsConfigExtension: PublicKey; + tokenBadgeAuthority: PublicKey; + tokenMint: PublicKey; + tokenBadgePda: PDA; + funder: PublicKey; +}; + +/** + * Initializes a TokenBadge account. + * + * @category Instructions + * @param program - program object containing services required to generate the instruction + * @param params - InitializeTokenBadgeParams object + * @returns - Instruction to perform the action. + */ +export function initializeTokenBadgeIx( + program: Program, + params: InitializeTokenBadgeParams +): Instruction { + const { + whirlpoolsConfig, + whirlpoolsConfigExtension, + tokenBadgeAuthority, + tokenMint, + tokenBadgePda, + funder, + } = params; + + const ix = program.instruction.initializeTokenBadge({ + accounts: { + whirlpoolsConfig, + whirlpoolsConfigExtension, + tokenBadgeAuthority, + tokenMint, + tokenBadge: tokenBadgePda.publicKey, + funder, + systemProgram: SystemProgram.programId, + }, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }; +} diff --git a/sdk/src/instructions/v2/set-config-extension-authority-ix.ts b/sdk/src/instructions/v2/set-config-extension-authority-ix.ts new file mode 100644 index 000000000..be44569b3 --- /dev/null +++ b/sdk/src/instructions/v2/set-config-extension-authority-ix.ts @@ -0,0 +1,51 @@ +import { Program } from "@coral-xyz/anchor"; +import { Instruction } from "@orca-so/common-sdk"; +import { PublicKey } from "@solana/web3.js"; +import { Whirlpool } from "../../artifacts/whirlpool"; + +/** + * Parameters to set the token badge authority in a WhirlpoolsConfigExtension + * + * @category Instruction Types + * @param whirlpoolsConfig - PublicKey for the whirlpools config account + * @param whirlpoolsConfigExtension - The public key for the WhirlpoolsConfigExtension + * @param configExtensionAuthority - The current configExtensionAuthority in the WhirlpoolsConfigExtension + * @param newConfigExtensionAuthority - The new configExtensionAuthority in the WhirlpoolsConfigExtension + */ +export type SetConfigExtensionAuthorityParams = { + whirlpoolsConfig: PublicKey; + whirlpoolsConfigExtension: PublicKey; + configExtensionAuthority: PublicKey; + newConfigExtensionAuthority: PublicKey; +}; + +/** + * Sets the config extension authority for a WhirlpoolsConfigExtension. + * Only the current config extension authority has permission to invoke this instruction. + * + * @category Instructions + * @param context - Context object containing services required to generate the instruction + * @param params - SetTokenBadgeAuthorityParams object + * @returns - Instruction to perform the action. + */ +export function setConfigExtensionAuthorityIx( + program: Program, + params: SetConfigExtensionAuthorityParams +): Instruction { + const { whirlpoolsConfig, whirlpoolsConfigExtension, configExtensionAuthority, newConfigExtensionAuthority } = params; + + const ix = program.instruction.setConfigExtensionAuthority({ + accounts: { + whirlpoolsConfig, + whirlpoolsConfigExtension, + configExtensionAuthority, + newConfigExtensionAuthority, + }, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }; +} diff --git a/sdk/src/instructions/v2/set-reward-emissions-ix.ts b/sdk/src/instructions/v2/set-reward-emissions-ix.ts new file mode 100644 index 000000000..3752bc55b --- /dev/null +++ b/sdk/src/instructions/v2/set-reward-emissions-ix.ts @@ -0,0 +1,63 @@ +import { BN, Program } from "@coral-xyz/anchor"; +import { Instruction } from "@orca-so/common-sdk"; +import { PublicKey } from "@solana/web3.js"; +import { Whirlpool } from "../../artifacts/whirlpool"; + +/** + * Parameters to set rewards emissions for a reward in a Whirlpool + * + * @category Instruction Types + * @param whirlpool - PublicKey for the whirlpool which the reward resides in. + * @param rewardIndex - The reward index that we'd like to initialize. (0 <= index <= NUM_REWARDS). + * @param rewardVaultKey - PublicKey of the vault for this reward index. + * @param rewardAuthority - Assigned authority by the reward_super_authority for the specified reward-index in this Whirlpool + * @param emissionsPerSecondX64 - The new emissions per second to set for this reward. + */ +export type SetRewardEmissionsV2Params = { + whirlpool: PublicKey; + rewardIndex: number; + rewardVaultKey: PublicKey; + rewardAuthority: PublicKey; + emissionsPerSecondX64: BN; +}; + +/** + * Set the reward emissions for a reward in a Whirlpool. + * + * #### Special Errors + * - `RewardVaultAmountInsufficient` - The amount of rewards in the reward vault cannot emit more than a day of desired emissions. + * - `InvalidTimestamp` - Provided timestamp is not in order with the previous timestamp. + * - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized index in this pool, + * or exceeds NUM_REWARDS. + * + * @category Instructions + * @param context - Context object containing services required to generate the instruction + * @param params - SetRewardEmissionsV2Params object + * @returns - Instruction to perform the action. + */ +export function setRewardEmissionsV2Ix( + program: Program, + params: SetRewardEmissionsV2Params +): Instruction { + const { + rewardAuthority, + whirlpool, + rewardIndex, + rewardVaultKey: rewardVault, + emissionsPerSecondX64, + } = params; + + const ix = program.instruction.setRewardEmissionsV2(rewardIndex, emissionsPerSecondX64, { + accounts: { + rewardAuthority, + whirlpool, + rewardVault, + }, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }; +} diff --git a/sdk/src/instructions/v2/set-token-badge-authority-ix.ts b/sdk/src/instructions/v2/set-token-badge-authority-ix.ts new file mode 100644 index 000000000..e652a44ae --- /dev/null +++ b/sdk/src/instructions/v2/set-token-badge-authority-ix.ts @@ -0,0 +1,52 @@ +import { Program } from "@coral-xyz/anchor"; +import { Instruction } from "@orca-so/common-sdk"; +import { PublicKey } from "@solana/web3.js"; +import { Whirlpool } from "../../artifacts/whirlpool"; + +/** + * Parameters to set the token badge authority in a WhirlpoolsConfigExtension + * + * @category Instruction Types + * @param whirlpoolsConfig - PublicKey for the whirlpools config account + * @param whirlpoolsConfigExtension - The public key for the WhirlpoolsConfigExtension + * @param configExtensionAuthority - The current configExtensionAuthority in the WhirlpoolsConfigExtension + * @param newTokenBadgeAuthority - The new tokenBadgeAuthority in the WhirlpoolsConfigExtension + */ +export type SetTokenBadgeAuthorityParams = { + whirlpoolsConfig: PublicKey; + whirlpoolsConfigExtension: PublicKey; + configExtensionAuthority: PublicKey; + newTokenBadgeAuthority: PublicKey; +}; + +/** + * Sets the token badge authority for a WhirlpoolsConfigExtension. + * The token badge authority can initialize TokenBadge. + * Only the config extension authority has permission to invoke this instruction. + * + * @category Instructions + * @param context - Context object containing services required to generate the instruction + * @param params - SetTokenBadgeAuthorityParams object + * @returns - Instruction to perform the action. + */ +export function setTokenBadgeAuthorityIx( + program: Program, + params: SetTokenBadgeAuthorityParams +): Instruction { + const { whirlpoolsConfig, whirlpoolsConfigExtension, configExtensionAuthority, newTokenBadgeAuthority } = params; + + const ix = program.instruction.setTokenBadgeAuthority({ + accounts: { + whirlpoolsConfig, + whirlpoolsConfigExtension, + configExtensionAuthority, + newTokenBadgeAuthority, + }, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }; +} diff --git a/sdk/src/instructions/v2/swap-ix.ts b/sdk/src/instructions/v2/swap-ix.ts new file mode 100644 index 000000000..88391a0fa --- /dev/null +++ b/sdk/src/instructions/v2/swap-ix.ts @@ -0,0 +1,127 @@ +import { Program } from "@coral-xyz/anchor"; +import { Instruction } from "@orca-so/common-sdk"; +import { AccountMeta, PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; +import { Whirlpool } from "../../artifacts/whirlpool"; +import { MEMO_PROGRAM_ADDRESS, SwapInput } from "../../types/public"; +import { RemainingAccountsBuilder, RemainingAccountsType } from "../../utils/remaining-accounts-util"; + +/** + * Raw parameters and accounts to swap on a Whirlpool + * + * @category Instruction Types + * @param swapInput - Parameters in {@link SwapInput} + * @param whirlpool - PublicKey for the whirlpool that the swap will occur on + * @param tokenMintA - PublicKey for the token A mint. + * @param tokenMintB - PublicKey for the token B mint. + * @param tokenOwnerAccountA - PublicKey for the associated token account for tokenA in the collection wallet + * @param tokenOwnerAccountB - PublicKey for the associated token account for tokenB in the collection wallet + * @param tokenVaultA - PublicKey for the tokenA vault for this whirlpool. + * @param tokenVaultB - PublicKey for the tokenB vault for this whirlpool. + * @param tokenTransferHookAccountsA - Optional array of token transfer hook accounts for token A. + * @param tokenTransferHookAccountsB - Optional array of token transfer hook accounts for token B. + * @param tokenProgramA - PublicKey for the token program for token A. + * @param tokenProgramB - PublicKey for the token program for token B. + * @param oracle - PublicKey for the oracle account for this Whirlpool. + * @param tokenAuthority - authority to withdraw tokens from the input token account + */ +export type SwapV2Params = SwapInput & { + whirlpool: PublicKey; + tokenMintA: PublicKey; + tokenMintB: PublicKey; + tokenOwnerAccountA: PublicKey; + tokenOwnerAccountB: PublicKey; + tokenVaultA: PublicKey; + tokenVaultB: PublicKey; + tokenTransferHookAccountsA?: AccountMeta[]; + tokenTransferHookAccountsB?: AccountMeta[]; + tokenProgramA: PublicKey; + tokenProgramB: PublicKey; + oracle: PublicKey; + tokenAuthority: PublicKey; +}; + +/** + * Perform a swap in this Whirlpool + * + * #### Special Errors + * - `ZeroTradableAmount` - User provided parameter `amount` is 0. + * - `InvalidSqrtPriceLimitDirection` - User provided parameter `sqrt_price_limit` does not match the direction of the trade. + * - `SqrtPriceOutOfBounds` - User provided parameter `sqrt_price_limit` is over Whirlppool's max/min bounds for sqrt-price. + * - `InvalidTickArraySequence` - User provided tick-arrays are not in sequential order required to proceed in this trade direction. + * - `TickArraySequenceInvalidIndex` - The swap loop attempted to access an invalid array index during the query of the next initialized tick. + * - `TickArrayIndexOutofBounds` - The swap loop attempted to access an invalid array index during tick crossing. + * - `LiquidityOverflow` - Liquidity value overflowed 128bits during tick crossing. + * - `InvalidTickSpacing` - The swap pool was initialized with tick-spacing of 0. + * + * ### Parameters + * @category Instructions + * @param context - Context object containing services required to generate the instruction + * @param params - {@link SwapV2Params} + * @returns - Instruction to perform the action. + */ +export function swapV2Ix(program: Program, params: SwapV2Params): Instruction { + const { + amount, + otherAmountThreshold, + sqrtPriceLimit, + amountSpecifiedIsInput, + aToB, + whirlpool, + tokenAuthority, + tokenMintA, + tokenMintB, + tokenOwnerAccountA, + tokenVaultA, + tokenOwnerAccountB, + tokenVaultB, + tokenTransferHookAccountsA, + tokenTransferHookAccountsB, + tokenProgramA, + tokenProgramB, + tickArray0, + tickArray1, + tickArray2, + oracle, + } = params; + + const [remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder() + .addSlice(RemainingAccountsType.TransferHookA, tokenTransferHookAccountsA) + .addSlice(RemainingAccountsType.TransferHookB, tokenTransferHookAccountsB) + .build(); + + const ix = program.instruction.swapV2( + amount, + otherAmountThreshold, + sqrtPriceLimit, + amountSpecifiedIsInput, + aToB, + remainingAccountsInfo, + { + accounts: { + tokenProgramA, + tokenProgramB, + memoProgram: MEMO_PROGRAM_ADDRESS, + tokenAuthority: tokenAuthority, + whirlpool, + tokenMintA, + tokenMintB, + tokenOwnerAccountA, + tokenVaultA, + tokenOwnerAccountB, + tokenVaultB, + tickArray0, + tickArray1, + tickArray2, + oracle, + }, + remainingAccounts, + } + ); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }; +} diff --git a/sdk/src/instructions/v2/two-hop-swap-ix.ts b/sdk/src/instructions/v2/two-hop-swap-ix.ts new file mode 100644 index 000000000..c158cc58a --- /dev/null +++ b/sdk/src/instructions/v2/two-hop-swap-ix.ts @@ -0,0 +1,165 @@ +import { Program } from "@coral-xyz/anchor"; +import { Instruction } from "@orca-so/common-sdk"; +import { AccountMeta, PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; +import { Whirlpool } from "../../artifacts/whirlpool"; +import { MEMO_PROGRAM_ADDRESS } from "../../types/public"; +import { RemainingAccountsBuilder, RemainingAccountsType } from "../../utils/remaining-accounts-util"; +import { TwoHopSwapInput } from "../two-hop-swap-ix"; + +/** + * Parameters to execute a two-hop swap on a Whirlpool. + * + * @category Instruction Types + * @param whirlpoolOne - PublicKey for the whirlpool that the swap-one will occur on + * @param whirlpoolTwo - PublicKey for the whirlpool that the swap-two will occur on + * @param tokenMintInput - PublicKey for the input token mint. + * @param tokenMintIntermediate - PublicKey for the intermediate token mint. + * @param tokenMintOutput - PublicKey for the output token mint. + * @param tokenOwnerAccountInput - PublicKey for the input token owner account. + * @param tokenOwnerAccountOutput - PublicKey for the output token owner account. + * @param tokenVaultOneInput - PublicKey for the input token vault of whirlpoolOne. + * @param tokenVaultOneIntermediate - PublicKey for the intermediate token vault of whirlpoolOne. + * @param tokenVaultTwoIntermediate - PublicKey for the intermediate token vault of whirlpoolTwo. + * @param tokenVaultTwoOutput - PublicKey for the output token vault of whirlpoolTwo. + * @param tokenTransferHookAccountsInput - AccountMeta[] for the input token transfer hook accounts. + * @param tokenTransferHookAccountsIntermediate - AccountMeta[] for the intermediate token transfer hook accounts. + * @param tokenTransferHookAccountsOutput - AccountMeta[] for the output token transfer hook accounts. + * @param oracleOne - PublicKey for the oracle account for this whirlpoolOne. + * @param oracleTwo - PublicKey for the oracle account for this whirlpoolTwo. + * @param tokenAuthority - authority to withdraw tokens from the input token account + * @param swapInput - Parameters in {@link TwoHopSwapInput} + */ +export type TwoHopSwapV2Params = TwoHopSwapInput & { + whirlpoolOne: PublicKey; + whirlpoolTwo: PublicKey; + tokenMintInput: PublicKey; + tokenMintIntermediate: PublicKey; + tokenMintOutput: PublicKey; + tokenOwnerAccountInput: PublicKey; + tokenOwnerAccountOutput: PublicKey; + tokenVaultOneInput: PublicKey; + tokenVaultOneIntermediate: PublicKey; + tokenVaultTwoIntermediate: PublicKey; + tokenVaultTwoOutput: PublicKey; + tokenTransferHookAccountsInput?: AccountMeta[]; + tokenTransferHookAccountsIntermediate?: AccountMeta[]; + tokenTransferHookAccountsOutput?: AccountMeta[]; + tokenProgramInput: PublicKey; + tokenProgramIntermediate: PublicKey; + tokenProgramOutput: PublicKey; + oracleOne: PublicKey; + oracleTwo: PublicKey; + tokenAuthority: PublicKey; +}; + +/** + * Perform a two-hop swap in this Whirlpool + * + * #### Special Errors + * - `ZeroTradableAmount` - User provided parameter `amount` is 0. + * - `InvalidSqrtPriceLimitDirection` - User provided parameter `sqrt_price_limit` does not match the direction of the trade. + * - `SqrtPriceOutOfBounds` - User provided parameter `sqrt_price_limit` is over Whirlppool's max/min bounds for sqrt-price. + * - `InvalidTickArraySequence` - User provided tick-arrays are not in sequential order required to proceed in this trade direction. + * - `TickArraySequenceInvalidIndex` - The swap loop attempted to access an invalid array index during the query of the next initialized tick. + * - `TickArrayIndexOutofBounds` - The swap loop attempted to access an invalid array index during tick crossing. + * - `LiquidityOverflow` - Liquidity value overflowed 128bits during tick crossing. + * - `InvalidTickSpacing` - The swap pool was initialized with tick-spacing of 0. + * - `InvalidIntermediaryMint` - Error if the intermediary mint between hop one and two do not equal. + * - `DuplicateTwoHopPool` - Error if whirlpool one & two are the same pool. + * + * ### Parameters + * @category Instructions + * @param context - Context object containing services required to generate the instruction + * @param params - {@link TwoHopSwapV2Params} object + * @returns - Instruction to perform the action. + */ +export function twoHopSwapV2Ix(program: Program, params: TwoHopSwapV2Params): Instruction { + const { + amount, + otherAmountThreshold, + amountSpecifiedIsInput, + aToBOne, + aToBTwo, + sqrtPriceLimitOne, + sqrtPriceLimitTwo, + whirlpoolOne, + whirlpoolTwo, + tokenMintInput, + tokenMintIntermediate, + tokenMintOutput, + tokenProgramInput, + tokenProgramIntermediate, + tokenProgramOutput, + tokenVaultOneInput, + tokenVaultOneIntermediate, + tokenVaultTwoIntermediate, + tokenVaultTwoOutput, + tokenAuthority, + tokenTransferHookAccountsInput, + tokenTransferHookAccountsIntermediate, + tokenTransferHookAccountsOutput, + tokenOwnerAccountInput, + tokenOwnerAccountOutput, + tickArrayOne0, + tickArrayOne1, + tickArrayOne2, + tickArrayTwo0, + tickArrayTwo1, + tickArrayTwo2, + oracleOne, + oracleTwo, + } = params; + + const [remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder() + .addSlice(RemainingAccountsType.TransferHookInput, tokenTransferHookAccountsInput) + .addSlice(RemainingAccountsType.TransferHookIntermediate, tokenTransferHookAccountsIntermediate) + .addSlice(RemainingAccountsType.TransferHookOutput, tokenTransferHookAccountsOutput) + .build(); + + const ix = program.instruction.twoHopSwapV2( + amount, + otherAmountThreshold, + amountSpecifiedIsInput, + aToBOne, + aToBTwo, + sqrtPriceLimitOne, + sqrtPriceLimitTwo, + remainingAccountsInfo, + { + accounts: { + whirlpoolOne, + whirlpoolTwo, + tokenMintInput, + tokenMintIntermediate, + tokenMintOutput, + tokenProgramInput, + tokenProgramIntermediate, + tokenProgramOutput, + tokenOwnerAccountInput, + tokenVaultOneInput, + tokenVaultOneIntermediate, + tokenVaultTwoIntermediate, + tokenVaultTwoOutput, + tokenOwnerAccountOutput, + tokenAuthority, + tickArrayOne0, + tickArrayOne1, + tickArrayOne2, + tickArrayTwo0, + tickArrayTwo1, + tickArrayTwo2, + oracleOne, + oracleTwo, + memoProgram: MEMO_PROGRAM_ADDRESS, + }, + remainingAccounts, + } + ); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }; +} diff --git a/sdk/src/ix.ts b/sdk/src/ix.ts index 1cb5ed765..897be69e8 100644 --- a/sdk/src/ix.ts +++ b/sdk/src/ix.ts @@ -519,4 +519,113 @@ export class WhirlpoolIx { ) { return ix.closeBundledPositionIx(program, params); } + + // V2 instructions + // TODO: comments + public static collectFeesV2Ix( + program: Program, + params: ix.CollectFeesV2Params + ) { + return ix.collectFeesV2Ix(program, params); + } + + public static collectProtocolFeesV2Ix( + program: Program, + params: ix.CollectProtocolFeesV2Params + ) { + return ix.collectProtocolFeesV2Ix(program, params); + } + + public static collectRewardV2Ix( + program: Program, + params: ix.CollectRewardV2Params + ) { + return ix.collectRewardV2Ix(program, params); + } + + public static decreaseLiquidityV2Ix( + program: Program, + params: ix.DecreaseLiquidityV2Params + ) { + return ix.decreaseLiquidityV2Ix(program, params); + } + + public static increaseLiquidityV2Ix( + program: Program, + params: ix.IncreaseLiquidityV2Params + ) { + return ix.increaseLiquidityV2Ix(program, params); + } + + public static initializePoolV2Ix( + program: Program, + params: ix.InitPoolV2Params + ) { + return ix.initializePoolV2Ix(program, params); + } + + public static initializeRewardV2Ix( + program: Program, + params: ix.InitializeRewardV2Params + ) { + return ix.initializeRewardV2Ix(program, params); + } + + public static setRewardEmissionsV2Ix( + program: Program, + params: ix.SetRewardEmissionsV2Params + ) { + return ix.setRewardEmissionsV2Ix(program, params); + } + + public static swapV2Ix( + program: Program, + params: ix.SwapV2Params + ) { + return ix.swapV2Ix(program, params); + } + + public static twoHopSwapV2Ix( + program: Program, + params: ix.TwoHopSwapV2Params + ) { + return ix.twoHopSwapV2Ix(program, params); + } + + // V2 instructions (TokenBadge related) + // TODO: comments + public static initializeConfigExtensionIx( + program: Program, + params: ix.InitConfigExtensionParams + ) { + return ix.initializeConfigExtensionIx(program, params); + } + + public static setConfigExtensionAuthorityIx( + program: Program, + params: ix.SetConfigExtensionAuthorityParams + ) { + return ix.setConfigExtensionAuthorityIx(program, params); + } + + public static setTokenBadgeAuthorityIx( + program: Program, + params: ix.SetTokenBadgeAuthorityParams + ) { + return ix.setTokenBadgeAuthorityIx(program, params); + } + + public static initializeTokenBadgeIx( + program: Program, + params: ix.InitializeTokenBadgeParams + ) { + return ix.initializeTokenBadgeIx(program, params); + } + + public static deleteTokenBadgeIx( + program: Program, + params: ix.DeleteTokenBadgeParams + ) { + return ix.deleteTokenBadgeIx(program, params); + } } diff --git a/sdk/src/network/public/fetcher/fetcher-impl.ts b/sdk/src/network/public/fetcher/fetcher-impl.ts index 29bfe7a1e..fbb867733 100644 --- a/sdk/src/network/public/fetcher/fetcher-impl.ts +++ b/sdk/src/network/public/fetcher/fetcher-impl.ts @@ -5,9 +5,11 @@ import { ParsableMintInfo, ParsableTokenAccountInfo, SimpleAccountFetcher, + MintWithTokenProgram, + AccountWithTokenProgram as TokenAccountWithTokenProgram, } from "@orca-so/common-sdk"; import { AccountLayout, Mint, Account as TokenAccount } from "@solana/spl-token"; -import { Connection } from "@solana/web3.js"; +import { Connection, EpochInfo } from "@solana/web3.js"; import { DEFAULT_WHIRLPOOL_RETENTION_POLICY, WhirlpoolAccountFetchOptions, @@ -19,8 +21,10 @@ import { PositionBundleData, PositionData, TickArrayData, + TokenBadgeData, WhirlpoolData, WhirlpoolsConfigData, + WhirlpoolsConfigExtensionData, } from "../../../types/public"; import { ParsableFeeTier, @@ -29,6 +33,8 @@ import { ParsableTickArray, ParsableWhirlpool, ParsableWhirlpoolsConfig, + ParsableWhirlpoolsConfigExtension, + ParsableTokenBadge, } from "../parsing"; /** @@ -51,6 +57,8 @@ export const buildDefaultAccountFetcher = (connection: Connection) => { */ export class WhirlpoolAccountFetcher implements WhirlpoolAccountFetcherInterface { private _accountRentExempt: number | undefined; + private _epochInfo: EpochInfo | undefined; + private _epochInfoNextFetchTime: number = 0; constructor( readonly connection: Connection, @@ -68,6 +76,21 @@ export class WhirlpoolAccountFetcher implements WhirlpoolAccountFetcherInterface return this._accountRentExempt; } + async getEpoch(refresh: boolean = false): Promise { + if (!this._epochInfo || Date.now() >= this._epochInfoNextFetchTime || refresh) { + const epochInfo = await this.connection.getEpochInfo(); + + // In theory, 1 slot per every 400ms. + // 320ms is 80% of 400ms. + const remainingSlotsInEpoch = Math.max(epochInfo.slotsInEpoch - epochInfo.slotIndex, 0); + const nextFetchTime = Date.now() + remainingSlotsInEpoch * 320; + + this._epochInfo = epochInfo; + this._epochInfoNextFetchTime = nextFetchTime; + } + return this._epochInfo.epoch; + } + getPool(address: Address, opts?: WhirlpoolAccountFetchOptions): Promise { return this.fetcher.getAccount(address, ParsableWhirlpool, opts); } @@ -110,22 +133,22 @@ export class WhirlpoolAccountFetcher implements WhirlpoolAccountFetcherInterface getTokenInfo( address: Address, opts?: WhirlpoolAccountFetchOptions - ): Promise { + ): Promise { return this.fetcher.getAccount(address, ParsableTokenAccountInfo, opts); } getTokenInfos( addresses: Address[], opts?: WhirlpoolAccountFetchOptions - ): Promise> { + ): Promise> { return this.fetcher.getAccounts(addresses, ParsableTokenAccountInfo, opts); } - getMintInfo(address: Address, opts?: WhirlpoolAccountFetchOptions): Promise { + getMintInfo(address: Address, opts?: WhirlpoolAccountFetchOptions): Promise { return this.fetcher.getAccount(address, ParsableMintInfo, opts); } getMintInfos( addresses: Address[], opts?: WhirlpoolAccountFetchOptions - ): Promise> { + ): Promise> { return this.fetcher.getAccounts(addresses, ParsableMintInfo, opts); } getConfig( @@ -152,6 +175,34 @@ export class WhirlpoolAccountFetcher implements WhirlpoolAccountFetcherInterface ): Promise> { return this.fetcher.getAccounts(addresses, ParsablePositionBundle, opts); } + + getConfigExtension( + address: Address, + opts?: WhirlpoolAccountFetchOptions + ): Promise { + return this.fetcher.getAccount(address, ParsableWhirlpoolsConfigExtension, opts); + } + getConfigExtensions( + addresses: Address[], + opts?: WhirlpoolAccountFetchOptions + ): Promise> { + return this.fetcher.getAccounts(addresses, ParsableWhirlpoolsConfigExtension, opts); + } + + getTokenBadge( + address: Address, + opts?: WhirlpoolAccountFetchOptions + ): Promise { + return this.fetcher.getAccount(address, ParsableTokenBadge, opts); + } + getTokenBadges( + addresses: Address[], + opts?: WhirlpoolAccountFetchOptions + ): Promise> { + return this.fetcher.getAccounts(addresses, ParsableTokenBadge, opts); + } + + populateCache( accounts: ReadonlyMap, parser: ParsableEntity, diff --git a/sdk/src/network/public/fetcher/fetcher-types.ts b/sdk/src/network/public/fetcher/fetcher-types.ts index a79df3671..5631d505d 100644 --- a/sdk/src/network/public/fetcher/fetcher-types.ts +++ b/sdk/src/network/public/fetcher/fetcher-types.ts @@ -3,15 +3,18 @@ import { BasicSupportedTypes, ParsableEntity, SimpleAccountFetchOptions, + MintWithTokenProgram, + AccountWithTokenProgram as TokenAccountWithTokenProgram } from "@orca-so/common-sdk"; -import { Mint, Account as TokenAccount } from "@solana/spl-token"; import { FeeTierData, PositionBundleData, PositionData, TickArrayData, + TokenBadgeData, WhirlpoolData, WhirlpoolsConfigData, + WhirlpoolsConfigExtensionData, } from "../../../types/public"; /** @@ -25,6 +28,8 @@ export type WhirlpoolSupportedTypes = | TickArrayData | FeeTierData | PositionBundleData + | WhirlpoolsConfigExtensionData + | TokenBadgeData | BasicSupportedTypes; /** @@ -65,6 +70,12 @@ export interface WhirlpoolAccountFetcherInterface { */ getAccountRentExempt(refresh?: boolean): Promise; + /** + * Fetch and cache the current epoch info + * @param refresh If true, will always fetch from the network + */ + getEpoch(refresh?: boolean): Promise; + /** * Fetch and cache the account for a given Whirlpool addresses * @param address The mint address @@ -141,7 +152,7 @@ export interface WhirlpoolAccountFetcherInterface { * @param address The address of the token account * @param opts {@link WhirlpoolAccountFetchOptions} instance to dictate fetch behavior */ - getTokenInfo(address: Address, opts?: WhirlpoolAccountFetchOptions): Promise; + getTokenInfo(address: Address, opts?: WhirlpoolAccountFetchOptions): Promise; /** * Fetch and cache the accounts for a given array of TokenAccount addresses @@ -151,14 +162,14 @@ export interface WhirlpoolAccountFetcherInterface { getTokenInfos( addresses: Address[], opts?: WhirlpoolAccountFetchOptions - ): Promise>; + ): Promise>; /** * Fetch and cache the account for a given Mint address * @param address The address of the mint account * @param opts {@link WhirlpoolAccountFetchOptions} instance to dictate fetch behavior */ - getMintInfo(address: Address, opts?: WhirlpoolAccountFetchOptions): Promise; + getMintInfo(address: Address, opts?: WhirlpoolAccountFetchOptions): Promise; /** * Fetch and cache the accounts for a given array of Mint addresses @@ -168,7 +179,7 @@ export interface WhirlpoolAccountFetcherInterface { getMintInfos( addresses: Address[], opts?: WhirlpoolAccountFetchOptions - ): Promise>; + ): Promise>; /** * Fetch and cache the account for a given WhirlpoolConfig address @@ -210,6 +221,46 @@ export interface WhirlpoolAccountFetcherInterface { opts?: WhirlpoolAccountFetchOptions ): Promise>; + /** + * Fetch and cache the account for a given WhirlpoolConfigExtension address + * @param address The address of the WhirlpoolConfigExtension account + * @param opts {@link WhirlpoolAccountFetchOptions} instance to dictate fetch behavior + */ + getConfigExtension( + address: Address, + opts?: WhirlpoolAccountFetchOptions + ): Promise; + + /** + * Fetch and cache the accounts for a given array of WhirlpoolConfigExtension addresses + * @param addresses The array of WhirlpoolConfigExtension account addresses + * @param opts {@link WhirlpoolAccountFetchOptions} instance to dictate fetch behavior + */ + getConfigExtensions( + addresses: Address[], + opts?: WhirlpoolAccountFetchOptions + ): Promise>; + + /** + * Fetch and cache the account for a given TokenBadge address + * @param address The address of the TokenBadge account + * @param opts {@link WhirlpoolAccountFetchOptions} instance to dictate fetch behavior + */ + getTokenBadge( + address: Address, + opts?: WhirlpoolAccountFetchOptions + ): Promise; + + /** + * Fetch and cache the accounts for a given array of TokenBadge addresses + * @param addresses The array of TokenBadge account addresses + * @param opts {@link WhirlpoolAccountFetchOptions} instance to dictate fetch behavior + */ + getTokenBadges( + addresses: Address[], + opts?: WhirlpoolAccountFetchOptions + ): Promise>; + /** * Populate the fetcher's cache with the given {@link WhirlpoolsData} accounts * @param accounts The map of addresses to on-chain account data diff --git a/sdk/src/network/public/parsing.ts b/sdk/src/network/public/parsing.ts index 1bfd946e6..6e512f6c2 100644 --- a/sdk/src/network/public/parsing.ts +++ b/sdk/src/network/public/parsing.ts @@ -8,8 +8,10 @@ import { PositionBundleData, PositionData, TickArrayData, + TokenBadgeData, WhirlpoolData, WhirlpoolsConfigData, + WhirlpoolsConfigExtensionData, } from "../../types/public"; /** @@ -156,6 +158,54 @@ export class ParsablePositionBundle { } } +/** + * @category Network + */ +@staticImplements>() +export class ParsableWhirlpoolsConfigExtension { + private constructor() {} + + public static parse( + address: PublicKey, + accountData: AccountInfo | undefined | null + ): WhirlpoolsConfigExtensionData | null { + if (!accountData?.data) { + return null; + } + + try { + return parseAnchorAccount(AccountName.WhirlpoolsConfigExtension, accountData); + } catch (e) { + console.error(`error while parsing WhirlpoolsConfigExtension: ${e}`); + return null; + } + } +} + +/** + * @category Network + */ +@staticImplements>() +export class ParsableTokenBadge { + private constructor() {} + + public static parse( + address: PublicKey, + accountData: AccountInfo | undefined | null + ): TokenBadgeData | null { + if (!accountData?.data) { + return null; + } + + try { + return parseAnchorAccount(AccountName.TokenBadge, accountData); + } catch (e) { + console.error(`error while parsing TokenBadge: ${e}`); + return null; + } + } +} + const WhirlpoolCoder = new BorshAccountsCoder(WhirlpoolIDL as Idl); function parseAnchorAccount(accountName: AccountName, accountData: AccountInfo) { diff --git a/sdk/src/prices/calculate-pool-prices.ts b/sdk/src/prices/calculate-pool-prices.ts index fca1e73de..67390e623 100644 --- a/sdk/src/prices/calculate-pool-prices.ts +++ b/sdk/src/prices/calculate-pool-prices.ts @@ -15,6 +15,7 @@ import { import { swapQuoteWithParams } from "../quotes/public/swap-quote"; import { TickArray, WhirlpoolData } from "../types/public"; import { PoolUtil, PriceMath, SwapUtils } from "../utils/public"; +import { NO_TOKEN_EXTENSION_CONTEXT } from "../utils/public/token-extension-util"; function checkLiquidity( pool: WhirlpoolData, @@ -37,6 +38,8 @@ function checkLiquidity( otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), tickArrays, + // To calculate token price, transfer fee is NOT taken into account. + tokenExtensionCtx: NO_TOKEN_EXTENSION_CONTEXT, }, Percentage.fromDecimal(new Decimal(0)) )); diff --git a/sdk/src/prices/price-module.ts b/sdk/src/prices/price-module.ts index 4bf1bf16f..06fea2cb0 100644 --- a/sdk/src/prices/price-module.ts +++ b/sdk/src/prices/price-module.ts @@ -25,6 +25,8 @@ import { calculatePricesForQuoteToken, convertAmount, isSubset } from "./calcula * token prices for a set of pools or mints. * * @category PriceModule + * + * @deprecated PriceModule will be removed in the future release. Please use endpoint which provides prices. */ export class PriceModule { /** @@ -38,6 +40,8 @@ export class PriceModule { * @param opts an {@link WhirlpoolAccountFetchOptions} object to define fetch and cache options when accessing on-chain accounts * @param availableData - Data that is already available to avoid redundant fetches. * @returns A map of token addresses to prices. + * + * @deprecated PriceModule will be removed in the future release. Please use endpoint which provides prices. */ static async fetchTokenPricesByMints( fetcher: WhirlpoolAccountFetcherInterface, @@ -78,6 +82,8 @@ export class PriceModule { * @param thresholdConfig The threshold configuration for the price calculation. * @param opts an {@link WhirlpoolAccountFetchOptions} object to define fetch and cache options when accessing on-chain accounts * @returns A map of token addresses to prices + * + * @deprecated PriceModule will be removed in the future release. Please use endpoint which provides prices. */ static async fetchTokenPricesByPools( fetcher: WhirlpoolAccountFetcherInterface, @@ -135,6 +141,8 @@ export class PriceModule { * @param config The configuration for the price calculation. * @param thresholdConfig The threshold configuration for the price calculation. * @returns A map of token addresses to prices. + * + * @deprecated PriceModule will be removed in the future release. Please use endpoint which provides prices. */ static calculateTokenPrices( mints: Address[], @@ -225,6 +233,8 @@ export class PriceModule { /** * A list of utility functions for the price module. * @category PriceModule + * + * @deprecated PriceModule will be removed in the future release. Please use endpoint which provides prices. */ export class PriceModuleUtils { /** @@ -236,6 +246,8 @@ export class PriceModuleUtils { * @param config The configuration for the price calculation. * @param opts an {@link WhirlpoolAccountFetchOptions} object to define fetch and cache options when accessing on-chain accounts * @returns A {@link PoolMap} of pool addresses to pool data. + * + * @deprecated PriceModule will be removed in the future release. Please use endpoint which provides prices. */ static async fetchPoolDataFromMints( fetcher: WhirlpoolAccountFetcherInterface, @@ -277,6 +289,8 @@ export class PriceModuleUtils { * @param config The configuration for the price calculation. * @param opts an {@link WhirlpoolAccountFetchOptions} object to define fetch and cache options when accessing on-chain accounts * @returns A {@link TickArrayMap} of tick-array addresses to tick-array data. + * + * @deprecated PriceModule will be removed in the future release. Please use endpoint which provides prices. */ static async fetchTickArraysForPools( fetcher: WhirlpoolAccountFetcherInterface, @@ -332,6 +346,8 @@ export class PriceModuleUtils { * @param mints The mints to fetch decimals for. * @param opts an {@link WhirlpoolAccountFetchOptions} object to define fetch and cache options when accessing on-chain accounts * @returns A {@link DecimalsMap} of mint addresses to decimals. + * + * @deprecated PriceModule will be removed in the future release. Please use endpoint which provides prices. */ static async fetchDecimalsForMints( fetcher: WhirlpoolAccountFetcherInterface, diff --git a/sdk/src/quotes/public/collect-fees-quote.ts b/sdk/src/quotes/public/collect-fees-quote.ts index d5ddeb8ba..5f1acdfa4 100644 --- a/sdk/src/quotes/public/collect-fees-quote.ts +++ b/sdk/src/quotes/public/collect-fees-quote.ts @@ -1,6 +1,7 @@ import { BN } from "@coral-xyz/anchor"; -import { MathUtil } from "@orca-so/common-sdk"; +import { MathUtil, MintWithTokenProgram } from "@orca-so/common-sdk"; import { PositionData, TickData, WhirlpoolData } from "../../types/public"; +import { TokenExtensionContextForPool, TokenExtensionUtil } from "../../utils/public/token-extension-util"; /** * @category Quotes @@ -10,6 +11,7 @@ export type CollectFeesQuoteParam = { position: PositionData; tickLower: TickData; tickUpper: TickData; + tokenExtensionCtx: TokenExtensionContextForPool; }; /** @@ -18,6 +20,10 @@ export type CollectFeesQuoteParam = { export type CollectFeesQuote = { feeOwedA: BN; feeOwedB: BN; + transferFee: { + deductedFromFeeOwedA: BN; + deductedFromFeeOwedB: BN; + }; }; /** @@ -28,7 +34,7 @@ export type CollectFeesQuote = { * @returns A quote object containing the fees owed for each token in the pool. */ export function collectFeesQuote(param: CollectFeesQuoteParam): CollectFeesQuote { - const { whirlpool, position, tickLower, tickUpper } = param; + const { whirlpool, position, tickLower, tickUpper, tokenExtensionCtx } = param; const { tickCurrentIndex, @@ -107,10 +113,25 @@ export function collectFeesQuote(param: CollectFeesQuoteParam): CollectFeesQuote .shrn(64); const updatedFeeOwedA = feeOwedA.add(feeOwedADelta); + const transferFeeExcludedAmountA = TokenExtensionUtil.calculateTransferFeeExcludedAmount( + updatedFeeOwedA, + tokenExtensionCtx.tokenMintWithProgramA, + tokenExtensionCtx.currentEpoch, + ); + const updatedFeeOwedB = feeOwedB.add(feeOwedBDelta); + const transferFeeExcludedAmountB = TokenExtensionUtil.calculateTransferFeeExcludedAmount( + updatedFeeOwedB, + tokenExtensionCtx.tokenMintWithProgramB, + tokenExtensionCtx.currentEpoch, + ); return { - feeOwedA: updatedFeeOwedA, - feeOwedB: updatedFeeOwedB, + feeOwedA: transferFeeExcludedAmountA.amount, + feeOwedB: transferFeeExcludedAmountB.amount, + transferFee: { + deductedFromFeeOwedA: transferFeeExcludedAmountA.fee, + deductedFromFeeOwedB: transferFeeExcludedAmountB.fee, + } }; } diff --git a/sdk/src/quotes/public/collect-rewards-quote.ts b/sdk/src/quotes/public/collect-rewards-quote.ts index 2ea3331ab..0fcf2eaee 100644 --- a/sdk/src/quotes/public/collect-rewards-quote.ts +++ b/sdk/src/quotes/public/collect-rewards-quote.ts @@ -4,6 +4,7 @@ import invariant from "tiny-invariant"; import { NUM_REWARDS, PositionData, TickData, WhirlpoolData } from "../../types/public"; import { BitMath } from "../../utils/math/bit-math"; import { PoolUtil } from "../../utils/public/pool-utils"; +import { TokenExtensionContextForReward, TokenExtensionUtil } from "../../utils/public/token-extension-util"; /** * Parameters needed to generate a quote on collectible rewards on a position. @@ -19,6 +20,7 @@ export type CollectRewardsQuoteParam = { position: PositionData; tickLower: TickData; tickUpper: TickData; + tokenExtensionCtx: TokenExtensionContextForReward; timeStampInSeconds?: BN; }; @@ -26,7 +28,20 @@ export type CollectRewardsQuoteParam = { * An array of reward amounts that is collectible on a position. * @category Quotes */ -export type CollectRewardsQuote = [BN | undefined, BN | undefined, BN | undefined]; +export type CollectRewardsQuote = { + rewardOwed: [ + BN | undefined, + BN | undefined, + BN | undefined, + ]; + transferFee: { + deductedFromRewardOwed: [ + BN | undefined, + BN | undefined, + BN | undefined, + ]; + }; +} /** * Get a quote on the outstanding rewards owed to a position. @@ -36,7 +51,7 @@ export type CollectRewardsQuote = [BN | undefined, BN | undefined, BN | undefine * @returns A quote object containing the rewards owed for each reward in the pool. */ export function collectRewardsQuote(param: CollectRewardsQuoteParam): CollectRewardsQuote { - const { whirlpool, position, tickLower, tickUpper, timeStampInSeconds } = param; + const { whirlpool, position, tickLower, tickUpper, timeStampInSeconds, tokenExtensionCtx } = param; const { tickCurrentIndex, @@ -47,7 +62,8 @@ export function collectRewardsQuote(param: CollectRewardsQuoteParam): CollectRew const currTimestampInSeconds = timeStampInSeconds ?? new BN(Date.now()).div(new BN(1000)); const timestampDelta = currTimestampInSeconds.sub(new BN(rewardLastUpdatedTimestamp)); - const rewardOwed: CollectRewardsQuote = [undefined, undefined, undefined]; + const rewardOwed: [BN|undefined,BN|undefined,BN|undefined] = [undefined, undefined, undefined]; + const transferFee: [BN|undefined,BN|undefined,BN|undefined] = [undefined, undefined, undefined]; for (let i = 0; i < NUM_REWARDS; i++) { // Calculate the reward growth on the outside of the position (growth_above, growth_below) @@ -105,7 +121,7 @@ export function collectRewardsQuote(param: CollectRewardsQuoteParam): CollectRew // Knowing the growth of the reward checkpoint for the position, calculate and increment the amount owed for each reward. const amountOwedX64 = positionRewardInfo.amountOwed.shln(64); - rewardOwed[i] = amountOwedX64 + const amountOwed = amountOwedX64 .add( MathUtil.subUnderflowU128( rewardGrowthInsideX64, @@ -113,7 +129,21 @@ export function collectRewardsQuote(param: CollectRewardsQuoteParam): CollectRew ).mul(liquidity) ) .shrn(64); + + const transferFeeExcluded = TokenExtensionUtil.calculateTransferFeeExcludedAmount( + amountOwed, + tokenExtensionCtx.rewardTokenMintsWithProgram[i]!, + tokenExtensionCtx.currentEpoch + ); + + rewardOwed[i] = transferFeeExcluded.amount; + transferFee[i] = transferFeeExcluded.fee; } - return rewardOwed; + return { + rewardOwed, + transferFee: { + deductedFromRewardOwed: transferFee, + }, + }; } diff --git a/sdk/src/quotes/public/decrease-liquidity-quote.ts b/sdk/src/quotes/public/decrease-liquidity-quote.ts index acd1a80ec..9614800b6 100644 --- a/sdk/src/quotes/public/decrease-liquidity-quote.ts +++ b/sdk/src/quotes/public/decrease-liquidity-quote.ts @@ -11,6 +11,7 @@ import { } from "../../utils/position-util"; import { PriceMath, TickUtil } from "../../utils/public"; import { Position, Whirlpool } from "../../whirlpool-client"; +import { TokenExtensionContextForPool, TokenExtensionUtil } from "../../utils/public/token-extension-util"; /** * @category Quotes @@ -27,6 +28,7 @@ export type DecreaseLiquidityQuoteParam = { sqrtPrice: BN; tickLowerIndex: number; tickUpperIndex: number; + tokenExtensionCtx: TokenExtensionContextForPool; slippageTolerance: Percentage; }; @@ -34,7 +36,16 @@ export type DecreaseLiquidityQuoteParam = { * Return object from decrease liquidity quote functions. * @category Quotes */ -export type DecreaseLiquidityQuote = DecreaseLiquidityInput & { tokenEstA: BN; tokenEstB: BN }; +export type DecreaseLiquidityQuote = DecreaseLiquidityInput & { + tokenEstA: BN; + tokenEstB: BN; + transferFee: { + deductedFromTokenEstA: BN; + deductedFromTokenEstB: BN; + deductedFromTokenMinA: BN; + deductedFromTokenMinB: BN; + }; +}; /** * Get an estimated quote on the minimum tokens receivable based on the desired withdraw liquidity value. @@ -50,7 +61,8 @@ export function decreaseLiquidityQuoteByLiquidity( liquidity: BN, slippageTolerance: Percentage, position: Position, - whirlpool: Whirlpool + whirlpool: Whirlpool, + tokenExtensionCtx: TokenExtensionContextForPool, ) { const positionData = position.getData(); const whirlpoolData = whirlpool.getData(); @@ -67,6 +79,7 @@ export function decreaseLiquidityQuoteByLiquidity( tickUpperIndex: positionData.tickUpperIndex, sqrtPrice: whirlpoolData.sqrtPrice, tickCurrentIndex: whirlpoolData.tickCurrentIndex, + tokenExtensionCtx, }); } @@ -106,7 +119,7 @@ export function decreaseLiquidityQuoteByLiquidityWithParams( } function quotePositionBelowRange(param: DecreaseLiquidityQuoteParam): DecreaseLiquidityQuote { - const { tickLowerIndex, tickUpperIndex, liquidity, slippageTolerance } = param; + const { tickLowerIndex, tickUpperIndex, liquidity, slippageTolerance, tokenExtensionCtx } = param; const sqrtPriceLowerX64 = PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex); const sqrtPriceUpperX64 = PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex); @@ -114,17 +127,26 @@ function quotePositionBelowRange(param: DecreaseLiquidityQuoteParam): DecreaseLi const tokenEstA = getTokenAFromLiquidity(liquidity, sqrtPriceLowerX64, sqrtPriceUpperX64, false); const tokenMinA = adjustForSlippage(tokenEstA, slippageTolerance, false); + const tokenMinAExcluded = TokenExtensionUtil.calculateTransferFeeExcludedAmount(tokenMinA, tokenExtensionCtx.tokenMintWithProgramA, tokenExtensionCtx.currentEpoch); + const tokenEstAExcluded = TokenExtensionUtil.calculateTransferFeeExcludedAmount(tokenEstA, tokenExtensionCtx.tokenMintWithProgramA, tokenExtensionCtx.currentEpoch); + return { - tokenMinA, + liquidityAmount: liquidity, + tokenMinA: tokenMinAExcluded.amount, tokenMinB: ZERO, - tokenEstA, + tokenEstA: tokenEstAExcluded.amount, tokenEstB: ZERO, - liquidityAmount: liquidity, + transferFee: { + deductedFromTokenMinA: tokenMinAExcluded.fee, + deductedFromTokenMinB: ZERO, + deductedFromTokenEstA: tokenEstAExcluded.fee, + deductedFromTokenEstB: ZERO, + }, }; } function quotePositionInRange(param: DecreaseLiquidityQuoteParam): DecreaseLiquidityQuote { - const { sqrtPrice, tickLowerIndex, tickUpperIndex, liquidity, slippageTolerance } = param; + const { sqrtPrice, tickLowerIndex, tickUpperIndex, liquidity, slippageTolerance, tokenExtensionCtx } = param; const sqrtPriceX64 = sqrtPrice; const sqrtPriceLowerX64 = PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex); @@ -135,17 +157,28 @@ function quotePositionInRange(param: DecreaseLiquidityQuoteParam): DecreaseLiqui const tokenEstB = getTokenBFromLiquidity(liquidity, sqrtPriceLowerX64, sqrtPriceX64, false); const tokenMinB = adjustForSlippage(tokenEstB, slippageTolerance, false); + const tokenMinAExcluded = TokenExtensionUtil.calculateTransferFeeExcludedAmount(tokenMinA, tokenExtensionCtx.tokenMintWithProgramA, tokenExtensionCtx.currentEpoch); + const tokenEstAExcluded = TokenExtensionUtil.calculateTransferFeeExcludedAmount(tokenEstA, tokenExtensionCtx.tokenMintWithProgramA, tokenExtensionCtx.currentEpoch); + const tokenMinBExcluded = TokenExtensionUtil.calculateTransferFeeExcludedAmount(tokenMinB, tokenExtensionCtx.tokenMintWithProgramB, tokenExtensionCtx.currentEpoch); + const tokenEstBExcluded = TokenExtensionUtil.calculateTransferFeeExcludedAmount(tokenEstB, tokenExtensionCtx.tokenMintWithProgramB, tokenExtensionCtx.currentEpoch); + return { - tokenMinA, - tokenMinB, - tokenEstA, - tokenEstB, liquidityAmount: liquidity, + tokenMinA: tokenMinAExcluded.amount, + tokenMinB: tokenMinBExcluded.amount, + tokenEstA: tokenEstAExcluded.amount, + tokenEstB: tokenEstBExcluded.amount, + transferFee: { + deductedFromTokenMinA: tokenMinAExcluded.fee, + deductedFromTokenMinB: tokenMinBExcluded.fee, + deductedFromTokenEstA: tokenEstAExcluded.fee, + deductedFromTokenEstB: tokenEstBExcluded.fee, + }, }; } function quotePositionAboveRange(param: DecreaseLiquidityQuoteParam): DecreaseLiquidityQuote { - const { tickLowerIndex, tickUpperIndex, liquidity, slippageTolerance: slippageTolerance } = param; + const { tickLowerIndex, tickUpperIndex, liquidity, slippageTolerance: slippageTolerance, tokenExtensionCtx } = param; const sqrtPriceLowerX64 = PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex); const sqrtPriceUpperX64 = PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex); @@ -153,11 +186,20 @@ function quotePositionAboveRange(param: DecreaseLiquidityQuoteParam): DecreaseLi const tokenEstB = getTokenBFromLiquidity(liquidity, sqrtPriceLowerX64, sqrtPriceUpperX64, false); const tokenMinB = adjustForSlippage(tokenEstB, slippageTolerance, false); + const tokenMinBExcluded = TokenExtensionUtil.calculateTransferFeeExcludedAmount(tokenMinB, tokenExtensionCtx.tokenMintWithProgramB, tokenExtensionCtx.currentEpoch); + const tokenEstBExcluded = TokenExtensionUtil.calculateTransferFeeExcludedAmount(tokenEstB, tokenExtensionCtx.tokenMintWithProgramB, tokenExtensionCtx.currentEpoch); + return { + liquidityAmount: liquidity, tokenMinA: ZERO, - tokenMinB, + tokenMinB: tokenMinBExcluded.amount, tokenEstA: ZERO, - tokenEstB, - liquidityAmount: liquidity, + tokenEstB: tokenEstBExcluded.amount, + transferFee: { + deductedFromTokenMinA: ZERO, + deductedFromTokenMinB: tokenMinBExcluded.fee, + deductedFromTokenEstA: ZERO, + deductedFromTokenEstB: tokenEstBExcluded.fee, + }, }; } diff --git a/sdk/src/quotes/public/increase-liquidity-quote.ts b/sdk/src/quotes/public/increase-liquidity-quote.ts index fee80f749..cb904b61c 100644 --- a/sdk/src/quotes/public/increase-liquidity-quote.ts +++ b/sdk/src/quotes/public/increase-liquidity-quote.ts @@ -1,5 +1,5 @@ import { Address } from "@coral-xyz/anchor"; -import { AddressUtil, DecimalUtil, Percentage, ZERO } from "@orca-so/common-sdk"; +import { AddressUtil, DecimalUtil, MintWithTokenProgram, Percentage, ZERO } from "@orca-so/common-sdk"; import { PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import Decimal from "decimal.js"; @@ -16,6 +16,7 @@ import { } from "../../utils/position-util"; import { PriceMath, TickUtil } from "../../utils/public"; import { Whirlpool } from "../../whirlpool-client"; +import { TokenExtensionContextForPool, TokenExtensionUtil } from "../../utils/public/token-extension-util"; /*** --------- Quote by Input Token --------- ***/ @@ -41,6 +42,7 @@ export type IncreaseLiquidityQuoteParam = { sqrtPrice: BN; tickLowerIndex: number; tickUpperIndex: number; + tokenExtensionCtx: TokenExtensionContextForPool; slippageTolerance: Percentage; }; @@ -49,7 +51,17 @@ export type IncreaseLiquidityQuoteParam = { * @category Quotes */ export type IncreaseLiquidityQuote = IncreaseLiquidityInput & IncreaseLiquidityEstimate; -type IncreaseLiquidityEstimate = { liquidityAmount: BN; tokenEstA: BN; tokenEstB: BN }; +type IncreaseLiquidityEstimate = { + liquidityAmount: BN; + tokenEstA: BN; + tokenEstB: BN; + transferFee: { + deductingFromTokenMaxA: BN; + deductingFromTokenMaxB: BN; + deductingFromTokenEstA: BN; + deductingFromTokenEstB: BN; + }; +}; /** * Get an estimated quote on the maximum tokens required to deposit based on a specified input token amount. @@ -70,7 +82,8 @@ export function increaseLiquidityQuoteByInputTokenUsingPriceSlippage( tickLower: number, tickUpper: number, slippageTolerance: Percentage, - whirlpool: Whirlpool + whirlpool: Whirlpool, + tokenExtensionCtx: TokenExtensionContextForPool, ) { const data = whirlpool.getData(); const tokenAInfo = whirlpool.getTokenAInfo(); @@ -85,6 +98,7 @@ export function increaseLiquidityQuoteByInputTokenUsingPriceSlippage( tickLowerIndex: TickUtil.getInitializableTickIndex(tickLower, data.tickSpacing), tickUpperIndex: TickUtil.getInitializableTickIndex(tickUpper, data.tickSpacing), slippageTolerance, + tokenExtensionCtx, ...data, }); } @@ -111,11 +125,17 @@ export function increaseLiquidityQuoteByInputTokenWithParamsUsingPriceSlippage( if (liquidity.eq(ZERO)) { return { + liquidityAmount: ZERO, tokenMaxA: ZERO, tokenMaxB: ZERO, - liquidityAmount: ZERO, tokenEstA: ZERO, tokenEstB: ZERO, + transferFee: { + deductingFromTokenMaxA: ZERO, + deductingFromTokenMaxB: ZERO, + deductingFromTokenEstA: ZERO, + deductingFromTokenEstB: ZERO, + }, }; } @@ -126,11 +146,12 @@ export function increaseLiquidityQuoteByInputTokenWithParamsUsingPriceSlippage( tickLowerIndex: param.tickLowerIndex, tickUpperIndex: param.tickUpperIndex, slippageTolerance: param.slippageTolerance, + tokenExtensionCtx: param.tokenExtensionCtx, }); } function getLiquidityFromInputToken(params: IncreaseLiquidityQuoteParam) { - const { inputTokenMint, inputTokenAmount, tickLowerIndex, tickUpperIndex, tickCurrentIndex, sqrtPrice } = params; + const { inputTokenMint, inputTokenAmount, tickLowerIndex, tickUpperIndex, sqrtPrice, tokenExtensionCtx } = params; invariant(tickLowerIndex < tickUpperIndex, `tickLowerIndex(${tickLowerIndex}) must be less than tickUpperIndex(${tickUpperIndex})`); if (inputTokenAmount.eq(ZERO)) { @@ -144,16 +165,46 @@ function getLiquidityFromInputToken(params: IncreaseLiquidityQuoteParam) { const positionStatus = PositionUtil.getStrictPositionStatus(sqrtPrice, tickLowerIndex, tickUpperIndex); if (positionStatus === PositionStatus.BelowRange) { - return isTokenA ? getLiquidityFromTokenA(inputTokenAmount, sqrtPriceLowerX64, sqrtPriceUpperX64, false) : ZERO; + if (!isTokenA) { + return ZERO; + } + + const transferFeeExcludedInputTokenAmount = TokenExtensionUtil.calculateTransferFeeExcludedAmount( + inputTokenAmount, + tokenExtensionCtx.tokenMintWithProgramA, + tokenExtensionCtx.currentEpoch, + ); + return getLiquidityFromTokenA(transferFeeExcludedInputTokenAmount.amount, sqrtPriceLowerX64, sqrtPriceUpperX64, false); } if (positionStatus === PositionStatus.AboveRange) { - return isTokenA ? ZERO : getLiquidityFromTokenB(inputTokenAmount, sqrtPriceLowerX64, sqrtPriceUpperX64, false); + if (isTokenA) { + return ZERO; + } + + const transferFeeExcludedInputTokenAmount = TokenExtensionUtil.calculateTransferFeeExcludedAmount( + inputTokenAmount, + tokenExtensionCtx.tokenMintWithProgramB, + tokenExtensionCtx.currentEpoch, + ); + return getLiquidityFromTokenB(transferFeeExcludedInputTokenAmount.amount, sqrtPriceLowerX64, sqrtPriceUpperX64, false); } - return isTokenA - ? getLiquidityFromTokenA(inputTokenAmount, sqrtPrice, sqrtPriceUpperX64, false) - : getLiquidityFromTokenB(inputTokenAmount, sqrtPriceLowerX64, sqrtPrice, false); + if (isTokenA) { + const transferFeeExcludedInputTokenAmount = TokenExtensionUtil.calculateTransferFeeExcludedAmount( + inputTokenAmount, + tokenExtensionCtx.tokenMintWithProgramA, + tokenExtensionCtx.currentEpoch, + ); + return getLiquidityFromTokenA(transferFeeExcludedInputTokenAmount.amount, sqrtPrice, sqrtPriceUpperX64, false) + } else { + const transferFeeExcludedInputTokenAmount = TokenExtensionUtil.calculateTransferFeeExcludedAmount( + inputTokenAmount, + tokenExtensionCtx.tokenMintWithProgramB, + tokenExtensionCtx.currentEpoch, + ); + return getLiquidityFromTokenB(transferFeeExcludedInputTokenAmount.amount, sqrtPriceLowerX64, sqrtPrice, false); + } } /*** --------- Quote by Liquidity --------- ***/ @@ -175,17 +226,24 @@ export type IncreaseLiquidityQuoteByLiquidityParam = { sqrtPrice: BN; tickLowerIndex: number; tickUpperIndex: number; + tokenExtensionCtx: TokenExtensionContextForPool; slippageTolerance: Percentage; }; export function increaseLiquidityQuoteByLiquidityWithParams(params: IncreaseLiquidityQuoteByLiquidityParam): IncreaseLiquidityQuote { if (params.liquidity.eq(ZERO)) { return { + liquidityAmount: ZERO, tokenMaxA: ZERO, tokenMaxB: ZERO, - liquidityAmount: ZERO, tokenEstA: ZERO, tokenEstB: ZERO, + transferFee: { + deductingFromTokenMaxA: ZERO, + deductingFromTokenMaxB: ZERO, + deductingFromTokenEstA: ZERO, + deductingFromTokenEstB: ZERO, + }, }; } const { tokenEstA, tokenEstB } = getTokenEstimatesFromLiquidity(params); @@ -210,12 +268,24 @@ export function increaseLiquidityQuoteByLiquidityWithParams(params: IncreaseLiqu const tokenMaxA = BN.max(BN.max(tokenEstA, tokenEstALower), tokenEstAUpper); const tokenMaxB = BN.max(BN.max(tokenEstB, tokenEstBLower), tokenEstBUpper); + const tokenExtensionCtx = params.tokenExtensionCtx; + const tokenMaxAIncluded = TokenExtensionUtil.calculateTransferFeeIncludedAmount(tokenMaxA, tokenExtensionCtx.tokenMintWithProgramA, tokenExtensionCtx.currentEpoch); + const tokenEstAIncluded = TokenExtensionUtil.calculateTransferFeeIncludedAmount(tokenEstA, tokenExtensionCtx.tokenMintWithProgramA, tokenExtensionCtx.currentEpoch); + const tokenMaxBIncluded = TokenExtensionUtil.calculateTransferFeeIncludedAmount(tokenMaxB, tokenExtensionCtx.tokenMintWithProgramB, tokenExtensionCtx.currentEpoch); + const tokenEstBIncluded = TokenExtensionUtil.calculateTransferFeeIncludedAmount(tokenEstB, tokenExtensionCtx.tokenMintWithProgramB, tokenExtensionCtx.currentEpoch); + return { - tokenMaxA, - tokenMaxB, - tokenEstA, - tokenEstB, liquidityAmount: params.liquidity, + tokenMaxA: tokenMaxAIncluded.amount, + tokenMaxB: tokenMaxBIncluded.amount, + tokenEstA: tokenEstAIncluded.amount, + tokenEstB: tokenEstBIncluded.amount, + transferFee: { + deductingFromTokenMaxA: tokenMaxAIncluded.fee, + deductingFromTokenMaxB: tokenMaxBIncluded.fee, + deductingFromTokenEstA: tokenEstAIncluded.fee, + deductingFromTokenEstB: tokenEstBIncluded.fee, + }, }; } @@ -270,7 +340,8 @@ export function increaseLiquidityQuoteByInputToken( tickLower: number, tickUpper: number, slippageTolerance: Percentage, - whirlpool: Whirlpool + whirlpool: Whirlpool, + tokenExtensionCtx: TokenExtensionContextForPool, ) { const data = whirlpool.getData(); const tokenAInfo = whirlpool.getTokenAInfo(); @@ -284,7 +355,8 @@ export function increaseLiquidityQuoteByInputToken( inputTokenAmount: DecimalUtil.toBN(inputTokenAmount, inputTokenInfo.decimals), tickLowerIndex: TickUtil.getInitializableTickIndex(tickLower, data.tickSpacing), tickUpperIndex: TickUtil.getInitializableTickIndex(tickUpper, data.tickSpacing), - slippageTolerance: slippageTolerance, + slippageTolerance, + tokenExtensionCtx, ...data, }); } @@ -335,24 +407,37 @@ function quotePositionBelowRange(param: IncreaseLiquidityQuoteParam): IncreaseLi inputTokenAmount, tickLowerIndex, tickUpperIndex, + tokenExtensionCtx, slippageTolerance, } = param; if (!tokenMintA.equals(inputTokenMint)) { return { + liquidityAmount: ZERO, tokenMaxA: ZERO, tokenMaxB: ZERO, tokenEstA: ZERO, tokenEstB: ZERO, - liquidityAmount: ZERO, + transferFee: { + deductingFromTokenMaxA: ZERO, + deductingFromTokenMaxB: ZERO, + deductingFromTokenEstA: ZERO, + deductingFromTokenEstB: ZERO, + }, }; } const sqrtPriceLowerX64 = PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex); const sqrtPriceUpperX64 = PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex); - const liquidityAmount = getLiquidityFromTokenA( + const transferFeeExcludedInputTokenAmount = TokenExtensionUtil.calculateTransferFeeExcludedAmount( inputTokenAmount, + tokenExtensionCtx.tokenMintWithProgramA, + tokenExtensionCtx.currentEpoch, + ); + + const liquidityAmount = getLiquidityFromTokenA( + transferFeeExcludedInputTokenAmount.amount, sqrtPriceLowerX64, sqrtPriceUpperX64, false @@ -366,12 +451,21 @@ function quotePositionBelowRange(param: IncreaseLiquidityQuoteParam): IncreaseLi ); const tokenMaxA = adjustForSlippage(tokenEstA, slippageTolerance, true); + const tokenMaxAIncluded = TokenExtensionUtil.calculateTransferFeeIncludedAmount(tokenMaxA, tokenExtensionCtx.tokenMintWithProgramA, tokenExtensionCtx.currentEpoch); + const tokenEstAIncluded = TokenExtensionUtil.calculateTransferFeeIncludedAmount(tokenEstA, tokenExtensionCtx.tokenMintWithProgramA, tokenExtensionCtx.currentEpoch); + return { - tokenMaxA, + liquidityAmount, + tokenMaxA: tokenMaxAIncluded.amount, tokenMaxB: ZERO, - tokenEstA, + tokenEstA: tokenEstAIncluded.amount, tokenEstB: ZERO, - liquidityAmount, + transferFee: { + deductingFromTokenMaxA: tokenMaxAIncluded.fee, + deductingFromTokenMaxB: ZERO, + deductingFromTokenEstA: tokenEstAIncluded.fee, + deductingFromTokenEstB: ZERO, + }, }; } @@ -381,11 +475,13 @@ function quotePositionBelowRange(param: IncreaseLiquidityQuoteParam): IncreaseLi function quotePositionInRange(param: IncreaseLiquidityQuoteParam): IncreaseLiquidityQuote { const { tokenMintA, + tokenMintB, sqrtPrice, inputTokenMint, inputTokenAmount, tickLowerIndex, tickUpperIndex, + tokenExtensionCtx, slippageTolerance, } = param; @@ -393,18 +489,26 @@ function quotePositionInRange(param: IncreaseLiquidityQuoteParam): IncreaseLiqui const sqrtPriceLowerX64 = PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex); const sqrtPriceUpperX64 = PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex); - let [tokenEstA, tokenEstB] = tokenMintA.equals(inputTokenMint) - ? [inputTokenAmount, undefined] - : [undefined, inputTokenAmount]; - + let tokenEstA: BN; + let tokenEstB: BN; let liquidityAmount: BN; - if (tokenEstA) { - liquidityAmount = getLiquidityFromTokenA(tokenEstA, sqrtPriceX64, sqrtPriceUpperX64, false); + if (tokenMintA.equals(inputTokenMint)) { + const transferFeeExcludedInputTokenAmount = TokenExtensionUtil.calculateTransferFeeExcludedAmount( + inputTokenAmount, + tokenExtensionCtx.tokenMintWithProgramA, + tokenExtensionCtx.currentEpoch, + ); + liquidityAmount = getLiquidityFromTokenA(transferFeeExcludedInputTokenAmount.amount, sqrtPriceX64, sqrtPriceUpperX64, false); tokenEstA = getTokenAFromLiquidity(liquidityAmount, sqrtPriceX64, sqrtPriceUpperX64, true); tokenEstB = getTokenBFromLiquidity(liquidityAmount, sqrtPriceLowerX64, sqrtPriceX64, true); - } else if (tokenEstB) { - liquidityAmount = getLiquidityFromTokenB(tokenEstB, sqrtPriceLowerX64, sqrtPriceX64, false); + } else if (tokenMintB.equals(inputTokenMint)) { + const transferFeeExcludedInputTokenAmount = TokenExtensionUtil.calculateTransferFeeExcludedAmount( + inputTokenAmount, + tokenExtensionCtx.tokenMintWithProgramB, + tokenExtensionCtx.currentEpoch, + ); + liquidityAmount = getLiquidityFromTokenB(transferFeeExcludedInputTokenAmount.amount, sqrtPriceLowerX64, sqrtPriceX64, false); tokenEstA = getTokenAFromLiquidity(liquidityAmount, sqrtPriceX64, sqrtPriceUpperX64, true); tokenEstB = getTokenBFromLiquidity(liquidityAmount, sqrtPriceLowerX64, sqrtPriceX64, true); } else { @@ -414,12 +518,23 @@ function quotePositionInRange(param: IncreaseLiquidityQuoteParam): IncreaseLiqui const tokenMaxA = adjustForSlippage(tokenEstA, slippageTolerance, true); const tokenMaxB = adjustForSlippage(tokenEstB, slippageTolerance, true); + const tokenMaxAIncluded = TokenExtensionUtil.calculateTransferFeeIncludedAmount(tokenMaxA, tokenExtensionCtx.tokenMintWithProgramA, tokenExtensionCtx.currentEpoch); + const tokenEstAIncluded = TokenExtensionUtil.calculateTransferFeeIncludedAmount(tokenEstA, tokenExtensionCtx.tokenMintWithProgramA, tokenExtensionCtx.currentEpoch); + const tokenMaxBIncluded = TokenExtensionUtil.calculateTransferFeeIncludedAmount(tokenMaxB, tokenExtensionCtx.tokenMintWithProgramB, tokenExtensionCtx.currentEpoch); + const tokenEstBIncluded = TokenExtensionUtil.calculateTransferFeeIncludedAmount(tokenEstB, tokenExtensionCtx.tokenMintWithProgramB, tokenExtensionCtx.currentEpoch); + return { - tokenMaxA, - tokenMaxB, - tokenEstA: tokenEstA!, - tokenEstB: tokenEstB!, liquidityAmount, + tokenMaxA: tokenMaxAIncluded.amount, + tokenMaxB: tokenMaxBIncluded.amount, + tokenEstA: tokenEstAIncluded.amount, + tokenEstB: tokenEstBIncluded.amount, + transferFee: { + deductingFromTokenMaxA: tokenMaxAIncluded.fee, + deductingFromTokenMaxB: tokenMaxBIncluded.fee, + deductingFromTokenEstA: tokenEstAIncluded.fee, + deductingFromTokenEstB: tokenEstBIncluded.fee, + }, }; } @@ -433,23 +548,37 @@ function quotePositionAboveRange(param: IncreaseLiquidityQuoteParam): IncreaseLi inputTokenAmount, tickLowerIndex, tickUpperIndex, + tokenExtensionCtx, slippageTolerance, } = param; if (!tokenMintB.equals(inputTokenMint)) { return { + liquidityAmount: ZERO, tokenMaxA: ZERO, tokenMaxB: ZERO, tokenEstA: ZERO, tokenEstB: ZERO, - liquidityAmount: ZERO, + transferFee: { + deductingFromTokenMaxA: ZERO, + deductingFromTokenMaxB: ZERO, + deductingFromTokenEstA: ZERO, + deductingFromTokenEstB: ZERO, + }, }; } const sqrtPriceLowerX64 = PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex); const sqrtPriceUpperX64 = PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex); - const liquidityAmount = getLiquidityFromTokenB( + + const transferFeeExcludedInputTokenAmount = TokenExtensionUtil.calculateTransferFeeExcludedAmount( inputTokenAmount, + tokenExtensionCtx.tokenMintWithProgramB, + tokenExtensionCtx.currentEpoch, + ); + + const liquidityAmount = getLiquidityFromTokenB( + transferFeeExcludedInputTokenAmount.amount, sqrtPriceLowerX64, sqrtPriceUpperX64, false @@ -463,11 +592,20 @@ function quotePositionAboveRange(param: IncreaseLiquidityQuoteParam): IncreaseLi ); const tokenMaxB = adjustForSlippage(tokenEstB, slippageTolerance, true); + const tokenMaxBIncluded = TokenExtensionUtil.calculateTransferFeeIncludedAmount(tokenMaxB, tokenExtensionCtx.tokenMintWithProgramB, tokenExtensionCtx.currentEpoch); + const tokenEstBIncluded = TokenExtensionUtil.calculateTransferFeeIncludedAmount(tokenEstB, tokenExtensionCtx.tokenMintWithProgramB, tokenExtensionCtx.currentEpoch); + return { + liquidityAmount, tokenMaxA: ZERO, - tokenMaxB, + tokenMaxB: tokenMaxBIncluded.amount, tokenEstA: ZERO, - tokenEstB, - liquidityAmount, + tokenEstB: tokenEstBIncluded.amount, + transferFee: { + deductingFromTokenMaxA: ZERO, + deductingFromTokenMaxB: tokenMaxBIncluded.fee, + deductingFromTokenEstA: ZERO, + deductingFromTokenEstB: tokenEstBIncluded.fee, + }, }; } \ No newline at end of file diff --git a/sdk/src/quotes/public/swap-quote.ts b/sdk/src/quotes/public/swap-quote.ts index 1f799de08..525dd2e42 100644 --- a/sdk/src/quotes/public/swap-quote.ts +++ b/sdk/src/quotes/public/swap-quote.ts @@ -1,9 +1,10 @@ import { Address } from "@coral-xyz/anchor"; -import { AddressUtil, Percentage } from "@orca-so/common-sdk"; +import { AddressUtil, MintWithTokenProgram, Percentage } from "@orca-so/common-sdk"; import BN from "bn.js"; import invariant from "tiny-invariant"; import { SwapInput } from "../../instructions"; import { + IGNORE_CACHE, WhirlpoolAccountFetchOptions, WhirlpoolAccountFetcherInterface, } from "../../network/public/fetcher"; @@ -13,6 +14,7 @@ import { SwapUtils } from "../../utils/public/swap-utils"; import { Whirlpool } from "../../whirlpool-client"; import { simulateSwap } from "../swap/swap-quote-impl"; import { DevFeeSwapQuote } from "./dev-fee-swap-quote"; +import { TokenExtensionContextForPool, TokenExtensionUtil } from "../../utils/public/token-extension-util"; /** * @category Quotes @@ -33,6 +35,7 @@ export type SwapQuoteParam = { aToB: boolean; amountSpecifiedIsInput: boolean; tickArrays: TickArray[]; + tokenExtensionCtx: TokenExtensionContextForPool; }; /** @@ -58,6 +61,10 @@ export type SwapEstimates = { estimatedEndTickIndex: number; estimatedEndSqrtPrice: BN; estimatedFeeAmount: BN; + transferFee: { + deductingFromEstimatedAmountIn: BN; + deductedFromEstimatedAmountOut: BN; + }; }; /** @@ -193,6 +200,8 @@ async function swapQuoteByToken( opts ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + return { whirlpoolData, tokenAmount, @@ -201,5 +210,6 @@ async function swapQuoteByToken( sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(amountSpecifiedIsInput), tickArrays, + tokenExtensionCtx, }; } diff --git a/sdk/src/quotes/swap/swap-quote-impl.ts b/sdk/src/quotes/swap/swap-quote-impl.ts index 4c25f866c..f430deeb5 100644 --- a/sdk/src/quotes/swap/swap-quote-impl.ts +++ b/sdk/src/quotes/swap/swap-quote-impl.ts @@ -5,6 +5,7 @@ import { MAX_SQRT_PRICE, MAX_SWAP_TICK_ARRAYS, MIN_SQRT_PRICE } from "../../type import { SwapQuote, SwapQuoteParam } from "../public"; import { computeSwap } from "./swap-manager"; import { TickArraySequence } from "./tick-array-sequence"; +import { TransferFeeIncludedAmount, TokenExtensionUtil } from "../../utils/public/token-extension-util"; /** * Figure out the quote parameters needed to successfully complete this trade on chain @@ -21,6 +22,7 @@ export function simulateSwap(params: SwapQuoteParam): SwapQuote { sqrtPriceLimit, otherAmountThreshold, amountSpecifiedIsInput, + tokenExtensionCtx, } = params; if (sqrtPriceLimit.gt(new BN(MAX_SQRT_PRICE)) || sqrtPriceLimit.lt(new BN(MIN_SQRT_PRICE))) { @@ -54,43 +56,120 @@ export function simulateSwap(params: SwapQuoteParam): SwapQuote { ); } - const swapResults = computeSwap( - whirlpoolData, - tickSequence, - tokenAmount, - sqrtPriceLimit, - amountSpecifiedIsInput, - aToB - ); - if (amountSpecifiedIsInput) { - if ( - (aToB && otherAmountThreshold.gt(swapResults.amountB)) || - (!aToB && otherAmountThreshold.gt(swapResults.amountA)) - ) { + // For ExactIn + + // computeSwap should be executed with "tokenAmount - transfer fee". + const transferFeeExcludedIn = TokenExtensionUtil.calculateTransferFeeExcludedAmount( + tokenAmount, + aToB ? tokenExtensionCtx.tokenMintWithProgramA : tokenExtensionCtx.tokenMintWithProgramB, + tokenExtensionCtx.currentEpoch + ); + + if (transferFeeExcludedIn.amount.eq(ZERO)) { + throw new WhirlpoolsError("Provided tokenAmount is virtually zero due to transfer fee.", SwapErrorCode.ZeroTradableAmount); + } + + const swapResults = computeSwap( + whirlpoolData, + tickSequence, + transferFeeExcludedIn.amount, + sqrtPriceLimit, + amountSpecifiedIsInput, + aToB + ); + + // otherAmountThreshold should be applied to transfer fee EXCLUDED output amount. + const transferFeeExcludedOut = TokenExtensionUtil.calculateTransferFeeExcludedAmount( + aToB ? swapResults.amountB : swapResults.amountA, + aToB ? tokenExtensionCtx.tokenMintWithProgramB : tokenExtensionCtx.tokenMintWithProgramA, + tokenExtensionCtx.currentEpoch + ); + + if (transferFeeExcludedOut.amount.lt(otherAmountThreshold)) { throw new WhirlpoolsError( "Quoted amount for the other token is below the otherAmountThreshold.", SwapErrorCode.AmountOutBelowMinimum ); } - } else { - if ( - (aToB && otherAmountThreshold.lt(swapResults.amountA)) || - (!aToB && otherAmountThreshold.lt(swapResults.amountB)) - ) { + + const fullfilled = (aToB ? swapResults.amountA : swapResults.amountB).eq(transferFeeExcludedIn.amount); + const transferFeeIncludedIn: TransferFeeIncludedAmount = fullfilled + ? { amount: tokenAmount, fee: transferFeeExcludedIn.fee } + : TokenExtensionUtil.calculateTransferFeeIncludedAmount( + aToB ? swapResults.amountA : swapResults.amountB, + aToB ? tokenExtensionCtx.tokenMintWithProgramA : tokenExtensionCtx.tokenMintWithProgramB, + tokenExtensionCtx.currentEpoch + ); + + const numOfTickCrossings = tickSequence.getNumOfTouchedArrays(); + if (numOfTickCrossings > MAX_SWAP_TICK_ARRAYS) { throw new WhirlpoolsError( - "Quoted amount for the other token is above the otherAmountThreshold.", - SwapErrorCode.AmountInAboveMaximum + `Input amount causes the quote to traverse more than the allowable amount of tick-arrays ${numOfTickCrossings}`, + SwapErrorCode.TickArrayCrossingAboveMax ); } + const touchedArrays = tickSequence.getTouchedArrays(MAX_SWAP_TICK_ARRAYS); + + return { + estimatedAmountIn: transferFeeIncludedIn.amount, + estimatedAmountOut: transferFeeExcludedOut.amount, + estimatedEndTickIndex: swapResults.nextTickIndex, + estimatedEndSqrtPrice: swapResults.nextSqrtPrice, + estimatedFeeAmount: swapResults.totalFeeAmount, + transferFee: { + deductingFromEstimatedAmountIn: transferFeeIncludedIn.fee, + deductedFromEstimatedAmountOut: transferFeeExcludedOut.fee, + }, + amount: tokenAmount, + amountSpecifiedIsInput, + aToB, + otherAmountThreshold, + sqrtPriceLimit, + tickArray0: touchedArrays[0], + tickArray1: touchedArrays[1], + tickArray2: touchedArrays[2], + }; } - const { estimatedAmountIn, estimatedAmountOut } = remapAndAdjustTokens( - swapResults.amountA, - swapResults.amountB, + // For ExactOut + + // For ExactOut, computeSwap should be executed with "tokenAmount + transfer fee". + const transferFeeIncludedOut = TokenExtensionUtil.calculateTransferFeeIncludedAmount( + tokenAmount, + aToB ? tokenExtensionCtx.tokenMintWithProgramB : tokenExtensionCtx.tokenMintWithProgramA, + tokenExtensionCtx.currentEpoch + ); + + const swapResults = computeSwap( + whirlpoolData, + tickSequence, + transferFeeIncludedOut.amount, + sqrtPriceLimit, + amountSpecifiedIsInput, aToB ); + // otherAmountThreshold should be applied to transfer fee INCLUDED input amount. + const transferFeeIncludedIn = TokenExtensionUtil.calculateTransferFeeIncludedAmount( + aToB ? swapResults.amountA : swapResults.amountB, + aToB ? tokenExtensionCtx.tokenMintWithProgramA : tokenExtensionCtx.tokenMintWithProgramB, + tokenExtensionCtx.currentEpoch + ); + + if (transferFeeIncludedIn.amount.gt(otherAmountThreshold)) { + throw new WhirlpoolsError( + "Quoted amount for the other token is above the otherAmountThreshold.", + SwapErrorCode.AmountInAboveMaximum + ); + } + + const transferFeeExcludedOut = TokenExtensionUtil.calculateTransferFeeExcludedAmount( + aToB ? swapResults.amountB : swapResults.amountA, + aToB ? tokenExtensionCtx.tokenMintWithProgramB : tokenExtensionCtx.tokenMintWithProgramA, + tokenExtensionCtx.currentEpoch + ); + const numOfTickCrossings = tickSequence.getNumOfTouchedArrays(); if (numOfTickCrossings > MAX_SWAP_TICK_ARRAYS) { throw new WhirlpoolsError( @@ -98,15 +177,18 @@ export function simulateSwap(params: SwapQuoteParam): SwapQuote { SwapErrorCode.TickArrayCrossingAboveMax ); } - const touchedArrays = tickSequence.getTouchedArrays(MAX_SWAP_TICK_ARRAYS); return { - estimatedAmountIn, - estimatedAmountOut, + estimatedAmountIn: transferFeeIncludedIn.amount, + estimatedAmountOut: transferFeeExcludedOut.amount, estimatedEndTickIndex: swapResults.nextTickIndex, estimatedEndSqrtPrice: swapResults.nextSqrtPrice, estimatedFeeAmount: swapResults.totalFeeAmount, + transferFee: { + deductingFromEstimatedAmountIn: transferFeeIncludedIn.fee, + deductedFromEstimatedAmountOut: transferFeeExcludedOut.fee, + }, amount: tokenAmount, amountSpecifiedIsInput, aToB, @@ -117,12 +199,3 @@ export function simulateSwap(params: SwapQuoteParam): SwapQuote { tickArray2: touchedArrays[2], }; } - -function remapAndAdjustTokens(amountA: BN, amountB: BN, aToB: boolean) { - const estimatedAmountIn = aToB ? amountA : amountB; - const estimatedAmountOut = aToB ? amountB : amountA; - return { - estimatedAmountIn, - estimatedAmountOut, - }; -} diff --git a/sdk/src/router/batch-swap-quote.ts b/sdk/src/router/batch-swap-quote.ts index 6dc86d7a7..f703e33bf 100644 --- a/sdk/src/router/batch-swap-quote.ts +++ b/sdk/src/router/batch-swap-quote.ts @@ -8,6 +8,7 @@ import { } from "../network/public/fetcher"; import { SwapQuoteParam } from "../quotes/public"; import { PoolUtil, SwapDirection, SwapUtils } from "../utils/public"; +import { NO_TOKEN_EXTENSION_CONTEXT } from "../utils/public/token-extension-util"; export interface SwapQuoteRequest { whirlpool: Address; @@ -60,6 +61,7 @@ export async function batchBuildSwapQuoteParams( sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(amountSpecifiedIsInput), tickArrays: tickArrays[index], + tokenExtensionCtx: NO_TOKEN_EXTENSION_CONTEXT, // WhirlpoolRouter does not support token extensions }; }); } diff --git a/sdk/src/router/public/index.ts b/sdk/src/router/public/index.ts index 24ac33f18..77415ab50 100644 --- a/sdk/src/router/public/index.ts +++ b/sdk/src/router/public/index.ts @@ -133,6 +133,8 @@ export type ExecutableRoute = readonly [TradeRoute, AddressLookupTableAccount[] * between the same token. * * @category Router + * + * @deprecated WhirlpoolRouter will be removed in the future release. Please use endpoint which provides qoutes. */ export interface WhirlpoolRouter { /** @@ -148,6 +150,8 @@ export interface WhirlpoolRouter { * @param fetchOpts * {@link WhirlpoolAccountFetchOptions} to configure the fetching of on-chain data. * @return A list of {@link TradeRoute} that can be used to execute a swap, ordered by the best other token amount. + * + * @deprecated WhirlpoolRouter will be removed in the future release. Please use endpoint which provides qoutes. */ findAllRoutes( trade: Trade, @@ -171,6 +175,8 @@ export interface WhirlpoolRouter { * {@link WhirlpoolAccountFetchOptions} to configure the fetching of on-chain data. * @returns * The best {@link ExecutableRoute} that can be used to execute a swap. If no executable route is found, null is returned. + * + * @deprecated WhirlpoolRouter will be removed in the future release. Please use endpoint which provides qoutes. */ findBestRoute( trade: Trade, @@ -190,6 +196,8 @@ export interface WhirlpoolRouter { * A {@link TransactionBuilder}that can be used to execute the trade. * If provvided from {@link ExecutableRoute}, plug the {@link AddressLookupTableAccount}s * into builder to lower the transaction size. + * + * @deprecated WhirlpoolRouter will be removed in the future release. Please use endpoint which provides qoutes. */ swap( trade: TradeRoute, diff --git a/sdk/src/router/public/router-builder.ts b/sdk/src/router/public/router-builder.ts index 74796a4ff..109acbebd 100644 --- a/sdk/src/router/public/router-builder.ts +++ b/sdk/src/router/public/router-builder.ts @@ -7,6 +7,8 @@ import { WhirlpoolRouterImpl } from "../router-impl"; /** * Builder to build instances of the {@link WhirlpoolRouter} * @category Router + * + * @deprecated WhirlpoolRouter will be removed in the future release. Please use endpoint which provides qoutes. */ export class WhirlpoolRouterBuilder { /** @@ -15,6 +17,8 @@ export class WhirlpoolRouterBuilder { * @param ctx A {@link WhirlpoolContext} for the current execution environment * @param graph A {@link PoolGraph} that represents the connections between all pools. * @returns A {@link WhirlpoolRouter} that can be used to find routes and execute swaps + * + * @deprecated WhirlpoolRouter will be removed in the future release. Please use endpoint which provides qoutes. */ static buildWithPoolGraph(ctx: WhirlpoolContext, graph: PoolGraph): WhirlpoolRouter { return new WhirlpoolRouterImpl(ctx, graph); @@ -25,6 +29,8 @@ export class WhirlpoolRouterBuilder { * @param ctx A {@link WhirlpoolContext} for the current execution environment * @param pools A list of {@link Address}es that the router will find routes through. * @returns A {@link WhirlpoolRouter} that can be used to find routes and execute swaps + * + * @deprecated WhirlpoolRouter will be removed in the future release. Please use endpoint which provides qoutes. */ static async buildWithPools(ctx: WhirlpoolContext, pools: Address[]): Promise { const poolGraph = await PoolGraphBuilder.buildPoolGraphWithFetch(pools, ctx.fetcher); diff --git a/sdk/src/router/public/router-utils.ts b/sdk/src/router/public/router-utils.ts index 23675235e..f1fc580e9 100644 --- a/sdk/src/router/public/router-utils.ts +++ b/sdk/src/router/public/router-utils.ts @@ -47,6 +47,7 @@ export type RouteSelectOptions = { /** * A selection of utility functions for the {@link WhirlpoolRouter}. * @category Router + * @deprecated WhirlpoolRouter will be removed in the future release. Please use endpoint which provides qoutes. */ export class RouterUtils { /** @@ -59,6 +60,8 @@ export class RouterUtils { * @param opts {@link RouteSelectOptions} to configure the selection of the best route. * @returns * The best {@link ExecutableRoute} that can be used to execute a swap. If no executable route is found, null is returned. + * + * @deprecated WhirlpoolRouter will be removed in the future release. Please use endpoint which provides qoutes. */ static async selectFirstExecutableRoute( ctx: WhirlpoolContext, @@ -145,6 +148,8 @@ export class RouterUtils { * @param trade The trade the user used to derive the route. * @param route The route to calculate the price impact for. * @returns A Decimal object representing the percentage value of the price impact (ex. 3.01%) + * + * @deprecated WhirlpoolRouter will be removed in the future release. Please use endpoint which provides qoutes. */ static getPriceImpactForRoute(trade: Trade, route: TradeRoute): Decimal { const { amountSpecifiedIsInput } = trade; @@ -198,6 +203,7 @@ export class RouterUtils { * Get the tick arrays addresses that are touched by a route. * @param route The route to get the tick arrays from. * @returns The tick arrays addresses that are touched by the route. + * @deprecated WhirlpoolRouter will be removed in the future release. Please use endpoint which provides qoutes. */ static getTouchedTickArraysFromRoute(route: TradeRoute): PublicKey[] { const taAddresses = new Set(); @@ -216,6 +222,7 @@ export class RouterUtils { /** * Get the default options for generating trade routes. * @returns Default options for generating trade routes. + * @deprecated WhirlpoolRouter will be removed in the future release. Please use endpoint which provides qoutes. */ static getDefaultRouteOptions(): RoutingOptions { return { diff --git a/sdk/src/types/public/anchor-types.ts b/sdk/src/types/public/anchor-types.ts index 43f8f725b..1efbde3de 100644 --- a/sdk/src/types/public/anchor-types.ts +++ b/sdk/src/types/public/anchor-types.ts @@ -21,6 +21,8 @@ export enum AccountName { Whirlpool = "Whirlpool", FeeTier = "FeeTier", PositionBundle = "PositionBundle", + WhirlpoolsConfigExtension = "WhirlpoolsConfigExtension", + TokenBadge = "TokenBadge", } export const WHIRLPOOL_IDL = WhirlpoolIDL as Idl; @@ -53,6 +55,8 @@ const RESERVED_BYTES: ReservedBytes = { [AccountName.Whirlpool]: 0, [AccountName.FeeTier]: 0, [AccountName.PositionBundle]: 64, + [AccountName.WhirlpoolsConfigExtension]: 512, + [AccountName.TokenBadge]: 128, }; type ReservedBytes = { @@ -195,3 +199,20 @@ export type PositionBundleData = { positionBundleMint: PublicKey; positionBitmap: number[]; }; + +/** + * @category Solana Accounts + */ +export type WhirlpoolsConfigExtensionData = { + whirlpoolsConfig: PublicKey; + configExtensionAuthority: PublicKey; + tokenBadgeAuthority: PublicKey; +} + +/** + * @category Solana Accounts + */ +export type TokenBadgeData = { + whirlpoolsConfig: PublicKey; + tokenMint: PublicKey; +} diff --git a/sdk/src/types/public/client-types.ts b/sdk/src/types/public/client-types.ts index ce034710c..70fa8c39d 100644 --- a/sdk/src/types/public/client-types.ts +++ b/sdk/src/types/public/client-types.ts @@ -1,19 +1,19 @@ -import { Account, Mint } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import { TickArrayData, WhirlpoolRewardInfoData } from "./anchor-types"; +import { AccountWithTokenProgram, MintWithTokenProgram } from "@orca-so/common-sdk"; /** * Extended Mint type to host token info. * @category WhirlpoolClient */ -export type TokenInfo = Mint & { mint: PublicKey }; +export type TokenInfo = MintWithTokenProgram & { mint: PublicKey }; /** * Extended (token) Account type to host account info for a Token. * @category WhirlpoolClient */ -export type TokenAccountInfo = Account; +export type TokenAccountInfo = AccountWithTokenProgram; /** * Type to represent a reward for a reward index on a Whirlpool. diff --git a/sdk/src/types/public/constants.ts b/sdk/src/types/public/constants.ts index 00828a172..5a0284946 100644 --- a/sdk/src/types/public/constants.ts +++ b/sdk/src/types/public/constants.ts @@ -15,6 +15,12 @@ export const ORCA_WHIRLPOOL_PROGRAM_ID = new PublicKey( */ export const ORCA_WHIRLPOOLS_CONFIG = new PublicKey("2LecshUwdy9xi7meFgHtFJQNSKk4KdTrcpvaB56dP2NQ"); +/** + * Orca's WhirlpoolsConfig PublicKey. + * @category Constants + */ +export const ORCA_WHIRLPOOLS_CONFIG_EXTENSION = new PublicKey("777H5H3Tp9U11uRVRzFwM8BinfiakbaLT8vQpeuhvEiH"); + /** * Orca's supported tick spacings. * @category Constants @@ -82,6 +88,13 @@ export const METADATA_PROGRAM_ADDRESS = new PublicKey( "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" ); +/** + * @category Constants + */ +export const MEMO_PROGRAM_ADDRESS = new PublicKey( + "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" +); + /** * The maximum number of tick-arrays that can traversed across in a swap. * @category Constants diff --git a/sdk/src/types/public/ix-types.ts b/sdk/src/types/public/ix-types.ts index 9b977424c..44566363a 100644 --- a/sdk/src/types/public/ix-types.ts +++ b/sdk/src/types/public/ix-types.ts @@ -32,6 +32,23 @@ export { OpenBundledPositionParams, CloseBundledPositionParams, } from "../../instructions/"; +export { + CollectFeesV2Params, + CollectProtocolFeesV2Params, + CollectRewardV2Params, + DecreaseLiquidityV2Params, + IncreaseLiquidityV2Params, + InitPoolV2Params, + InitializeRewardV2Params, + SetRewardEmissionsV2Params, + SwapV2Params, + TwoHopSwapV2Params, + InitConfigExtensionParams, + SetConfigExtensionAuthorityParams, + SetTokenBadgeAuthorityParams, + InitializeTokenBadgeParams, + DeleteTokenBadgeParams, +} from "../../instructions/v2/"; export { CollectAllParams, CollectAllPositionAddressParams, diff --git a/sdk/src/utils/public/pda-utils.ts b/sdk/src/utils/public/pda-utils.ts index 2028d32ca..c3ecc62a3 100644 --- a/sdk/src/utils/public/pda-utils.ts +++ b/sdk/src/utils/public/pda-utils.ts @@ -13,6 +13,8 @@ const PDA_FEE_TIER_SEED = "fee_tier"; const PDA_ORACLE_SEED = "oracle"; const PDA_POSITION_BUNDLE_SEED = "position_bundle"; const PDA_BUNDLED_POSITION_SEED = "bundled_position"; +const PDA_CONFIG_EXTENSION_SEED = "config_extension"; +const PDA_TOKEN_BADGE_SEED = "token_badge"; /** * @category Whirlpool Utils @@ -221,4 +223,39 @@ export class PDAUtil { METADATA_PROGRAM_ADDRESS ); } + + /** + * @category Program Derived Addresses + * @param programId + * @param whirlpoolsConfigAddress + * @returns + */ + public static getConfigExtension(programId: PublicKey, whirlpoolsConfigAddress: PublicKey) { + return AddressUtil.findProgramAddress( + [Buffer.from(PDA_CONFIG_EXTENSION_SEED), whirlpoolsConfigAddress.toBuffer()], + programId + ); + } + + /** + * @category Program Derived Addresses + * @param programId + * @param whirlpoolsConfigAddress + * @param tokenMintKey + * @returns + */ + public static getTokenBadge( + programId: PublicKey, + whirlpoolsConfigAddress: PublicKey, + tokenMintKey: PublicKey + ) { + return AddressUtil.findProgramAddress( + [ + Buffer.from(PDA_TOKEN_BADGE_SEED), + whirlpoolsConfigAddress.toBuffer(), + tokenMintKey.toBuffer(), + ], + programId + ); + } } diff --git a/sdk/src/utils/public/pool-utils.ts b/sdk/src/utils/public/pool-utils.ts index 9c70a1b4a..4e7cb6b54 100644 --- a/sdk/src/utils/public/pool-utils.ts +++ b/sdk/src/utils/public/pool-utils.ts @@ -7,6 +7,9 @@ import { WhirlpoolData, WhirlpoolRewardInfoData } from "../../types/public"; import { TOKEN_MINTS } from "../constants"; import { PriceMath } from "./price-math"; import { TokenType } from "./types"; +import { PDAUtil, WhirlpoolContext } from "../.."; +import invariant from "tiny-invariant"; +import { AccountState, ExtensionType, NATIVE_MINT_2022, TOKEN_PROGRAM_ID, getDefaultAccountState, getExtensionTypes } from "@solana/spl-token"; /** * @category Whirlpool Utils @@ -182,6 +185,75 @@ export class PoolUtil { const pair: [PublicKey, PublicKey] = [tokenMintAKey, tokenMintBKey]; return pair.sort(sortByQuotePriority); } + + public static async isSupportedToken( + ctx: WhirlpoolContext, + whirlpoolsConfig: PublicKey, + tokenMintKey: PublicKey, + ) { + // sync with is_supported_token (programs/whirlpool/src/util/v2/token.rs) + + const mintWithTokenProgram = await ctx.fetcher.getMintInfo(tokenMintKey); + invariant(mintWithTokenProgram, "Mint not found"); + + if (mintWithTokenProgram.tokenProgram.equals(TOKEN_PROGRAM_ID)) { + return true; + } + + if (mintWithTokenProgram.address.equals(NATIVE_MINT_2022)) { + return false; + } + + if (mintWithTokenProgram.freezeAuthority !== null) { + return false; + } + + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, whirlpoolsConfig, tokenMintKey); + const tokenBadge = await ctx.fetcher.getTokenBadge(tokenBadgePda.publicKey); + const isTokenBadgeInitialized = tokenBadge !== null; + + const extensions = getExtensionTypes(mintWithTokenProgram.tlvData); + for (const extension of extensions) { + switch (extension) { + // supported + case ExtensionType.TransferFeeConfig: + case ExtensionType.TokenMetadata: + case ExtensionType.MetadataPointer: + case ExtensionType.ConfidentialTransferMint: + continue; + + // supported if TokenBadge is initialized + case ExtensionType.PermanentDelegate: + case ExtensionType.TransferHook: + case ExtensionType.MintCloseAuthority: + if (!isTokenBadgeInitialized) { + return false; + } + continue; + case ExtensionType.DefaultAccountState: + if (!isTokenBadgeInitialized) { + return false; + } + + const defaultAccountState = getDefaultAccountState(mintWithTokenProgram)!; + if (defaultAccountState.state !== AccountState.Initialized) { + return false; + } + + continue; + + // not supported + case ExtensionType.NonTransferable: + return false; + + // not supported yet or unknown extension + default: + return false; + } + } + + return true; + } } /** diff --git a/sdk/src/utils/public/token-extension-util.ts b/sdk/src/utils/public/token-extension-util.ts new file mode 100644 index 000000000..23a3e1924 --- /dev/null +++ b/sdk/src/utils/public/token-extension-util.ts @@ -0,0 +1,272 @@ +import { TransferFee, calculateFee, getEpochFee, getTransferFeeConfig, TOKEN_2022_PROGRAM_ID, getTransferHook, addExtraAccountMetasForExecute } from "@solana/spl-token"; +import BN from "bn.js"; +import { MintWithTokenProgram, U64_MAX, ZERO } from "@orca-so/common-sdk"; +import { PoolUtil, WhirlpoolAccountFetchOptions, WhirlpoolAccountFetcherInterface, WhirlpoolData } from "../.."; +import { AccountMeta, Connection, PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/utils/token"; + +export type TransferFeeIncludedAmount = { + amount: BN; + fee: BN; +}; + +export type TransferFeeExcludedAmount = { + amount: BN; + fee: BN; +}; + +export type TokenExtensionContext = { + currentEpoch: number; + tokenMintWithProgramA: MintWithTokenProgram; + tokenMintWithProgramB: MintWithTokenProgram; + rewardTokenMintsWithProgram: [ + MintWithTokenProgram | null, + MintWithTokenProgram | null, + MintWithTokenProgram | null, + ]; +}; + +export type TokenExtensionContextForPool = Omit; +export type TokenExtensionContextForReward = Omit; + +const defaultTokenMintWithProgram: MintWithTokenProgram = { + address: PublicKey.default, + decimals: 0, + freezeAuthority: null, + mintAuthority: null, + isInitialized: true, + supply: 0n, + tlvData: Buffer.from([]), + tokenProgram: TOKEN_PROGRAM_ID, +}; + +export const NO_TOKEN_EXTENSION_CONTEXT: TokenExtensionContext = { + currentEpoch: 0, + tokenMintWithProgramA: defaultTokenMintWithProgram, + tokenMintWithProgramB: defaultTokenMintWithProgram, + rewardTokenMintsWithProgram: [ + defaultTokenMintWithProgram, + defaultTokenMintWithProgram, + defaultTokenMintWithProgram, + ], +}; + +export class TokenExtensionUtil { + public static calculateTransferFeeIncludedAmount( + transferFeeExcludedAmount: BN, + tokenInfo: MintWithTokenProgram, + currentEpoch: number, + ): TransferFeeIncludedAmount { + const config = getTransferFeeConfig(tokenInfo); + if (config === null) { + return { amount: transferFeeExcludedAmount, fee: ZERO }; + } + + const transferFee = getEpochFee(config, BigInt(currentEpoch)); + return calculateTransferFeeIncludedAmount(transferFee, transferFeeExcludedAmount); + } + + public static calculateTransferFeeExcludedAmount( + transferFeeIncludedAmount: BN, + tokenInfo: MintWithTokenProgram, + currentEpoch: number, + ): TransferFeeExcludedAmount { + const config = getTransferFeeConfig(tokenInfo); + if (config === null) { + return { amount: transferFeeIncludedAmount, fee: ZERO }; + } + + const transferFee = getEpochFee(config, BigInt(currentEpoch)); + return calculateTransferFeeExcludedAmount(transferFee, transferFeeIncludedAmount); + } + + public static async buildTokenExtensionContext( + fetcher: WhirlpoolAccountFetcherInterface, + whirlpoolData: WhirlpoolData, + opts?: WhirlpoolAccountFetchOptions, + ): Promise { + const mintA = whirlpoolData.tokenMintA; + const mintB = whirlpoolData.tokenMintB; + const rewards = whirlpoolData.rewardInfos; + + const [tokenMintWithProgram, currentEpoch] = await Promise.all([ + fetcher.getMintInfos([ + mintA, + mintB, + ...rewards.filter((r) => PoolUtil.isRewardInitialized(r)).map((r) => r.mint), + ], opts), + fetcher.getEpoch() + ]); + + const get = (mint: PublicKey) => tokenMintWithProgram.get(mint.toBase58())!; + + return { + tokenMintWithProgramA: get(whirlpoolData.tokenMintA), + tokenMintWithProgramB: get(whirlpoolData.tokenMintB), + rewardTokenMintsWithProgram: [ + PoolUtil.isRewardInitialized(rewards[0]) ? get(rewards[0].mint) : null, + PoolUtil.isRewardInitialized(rewards[1]) ? get(rewards[1].mint) : null, + PoolUtil.isRewardInitialized(rewards[2]) ? get(rewards[2].mint) : null, + ], + currentEpoch, + }; + } + + public static async getExtraAccountMetasForTransferHook( + connection: Connection, + tokenMintWithProgram: MintWithTokenProgram, + source: PublicKey, + destination: PublicKey, + owner: PublicKey, + ): Promise { + const transferHook = getTransferHook(tokenMintWithProgram); + + if (!transferHook) return undefined; + + const instruction = new TransactionInstruction({ + programId: TOKEN_2022_PROGRAM_ID, + keys: [ + {pubkey: source, isSigner: false, isWritable: false}, + {pubkey: tokenMintWithProgram.address, isSigner: false, isWritable: false}, + {pubkey: destination, isSigner: false, isWritable: false}, + {pubkey: owner, isSigner: false, isWritable: false}, + {pubkey: owner, isSigner: false, isWritable: false}, + ] + }); + + await addExtraAccountMetasForExecute( + connection, + instruction, + transferHook.programId, + source, + tokenMintWithProgram.address, + destination, + owner, + 0n, // extra account must not depend on the amount (the amount will be changed due to slippage) + "confirmed" + ); + + const extraAccountMetas = instruction.keys.slice(5); + return extraAccountMetas.length > 0 + ? extraAccountMetas + : undefined; + } + + public static async getExtraAccountMetasForTransferHookForPool( + connection: Connection, + tokenExtensionCtx: TokenExtensionContextForPool, + sourceA: PublicKey, + destinationA: PublicKey, + ownerA: PublicKey, + sourceB: PublicKey, + destinationB: PublicKey, + ownerB: PublicKey, + ): Promise<{ + tokenTransferHookAccountsA: AccountMeta[] | undefined, + tokenTransferHookAccountsB: AccountMeta[] | undefined, + }> { + const [tokenTransferHookAccountsA, tokenTransferHookAccountsB] = await Promise.all([ + TokenExtensionUtil.getExtraAccountMetasForTransferHook( + connection, + tokenExtensionCtx.tokenMintWithProgramA, + sourceA, + destinationA, + ownerA, + ), + TokenExtensionUtil.getExtraAccountMetasForTransferHook( + connection, + tokenExtensionCtx.tokenMintWithProgramB, + sourceB, + destinationB, + ownerB, + ), + ]); + + return { + tokenTransferHookAccountsA, + tokenTransferHookAccountsB, + }; + } + + public static isV2IxRequiredPool( + tokenExtensionCtx: TokenExtensionContextForPool + ): boolean { + return tokenExtensionCtx.tokenMintWithProgramA.tokenProgram.equals(TOKEN_2022_PROGRAM_ID) + || tokenExtensionCtx.tokenMintWithProgramB.tokenProgram.equals(TOKEN_2022_PROGRAM_ID); + } + + public static isV2IxRequiredReward( + tokenExtensionCtx: TokenExtensionContextForReward, + rewardIndex: number, + ): boolean { + return tokenExtensionCtx.rewardTokenMintsWithProgram[rewardIndex]?.tokenProgram.equals(TOKEN_2022_PROGRAM_ID) ?? false; + } +} + +function ceilDivBN(num: BN, denom: BN): BN { + return num.add(denom.subn(1)).div(denom); +} + +function calculateTransferFeeIncludedAmount( + transferFee: TransferFee, + amount: BN, +): TransferFeeIncludedAmount { + // https://github.com/solana-labs/solana-program-library/blob/master/token/program-2022/src/extension/transfer_fee/mod.rs#L90 + + const ONE_IN_BASIS_POINTS = 10_000; + const maxFeeBN = new BN(transferFee.maximumFee.toString()); + + // edge cases + + if (transferFee.transferFeeBasisPoints === 0) { + return { + amount, + fee: ZERO, + }; + } + + if (amount.isZero()) { + return { + amount, + fee: ZERO, + }; + } + + if (transferFee.transferFeeBasisPoints === ONE_IN_BASIS_POINTS) { + if (amount.add(maxFeeBN).gt(U64_MAX)) { + throw new Error("The total amount and fees overflow"); + } + return { + amount: amount.add(maxFeeBN), + fee: maxFeeBN, + }; + } + + // normal case + + const num = amount.muln(ONE_IN_BASIS_POINTS); + const denom = new BN(ONE_IN_BASIS_POINTS - transferFee.transferFeeBasisPoints); + const rawFeeIncludedAmount = ceilDivBN(num, denom); + + const result = rawFeeIncludedAmount.sub(amount).gte(maxFeeBN) + ? { amount: amount.add(maxFeeBN), fee: maxFeeBN } + : { amount: rawFeeIncludedAmount, fee: rawFeeIncludedAmount.sub(amount) }; + + if (result.amount.gt(U64_MAX)) { + throw new Error("The total amount and fees overflow"); + } + + return { ...result }; +} + +function calculateTransferFeeExcludedAmount( + transferFee: TransferFee, + amount: BN, +): TransferFeeExcludedAmount { + const fee = calculateFee(transferFee, BigInt(amount.toString())); + const feeBN = new BN(fee.toString()); + return { + amount: amount.sub(feeBN), + fee: feeBN, + }; +} diff --git a/sdk/src/utils/remaining-accounts-util.ts b/sdk/src/utils/remaining-accounts-util.ts new file mode 100644 index 000000000..ab8ea5f3c --- /dev/null +++ b/sdk/src/utils/remaining-accounts-util.ts @@ -0,0 +1,62 @@ +import { AccountMeta } from "@solana/web3.js"; + +export enum RemainingAccountsType { + TransferHookA = "transferHookA", + TransferHookB = "transferHookB", + TransferHookReward = "transferHookReward", + TransferHookInput = "transferHookInput", + TransferHookIntermediate = "transferHookIntermediate", + TransferHookOutput = "transferHookOutput", + //TickArray = "tickArray", + //TickArrayOne = "tickArrayOne", + //TickArrayTwo = "tickArrayTwo", +} + +type RemainingAccountsAnchorType = + { transferHookA: {} } | + { transferHookB: {} } | + { transferHookReward: {} } | + { transferHookInput: {} } | + { transferHookIntermediate: {} } | + { transferHookOutput: {} } + //{ tickArray: {} } | + //{ tickArrayOne: {} } | + //{ tickArrayTwo: {} } | + +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]; + } +} \ No newline at end of file diff --git a/sdk/src/utils/txn-utils.ts b/sdk/src/utils/txn-utils.ts index d2c5e62e1..a496e3623 100644 --- a/sdk/src/utils/txn-utils.ts +++ b/sdk/src/utils/txn-utils.ts @@ -1,9 +1,16 @@ import { + Instruction, + MEASUREMENT_BLOCKHASH, + ResolvedTokenAddressInstruction, + TokenUtil, TransactionBuilder, TransactionBuilderOptions, + ZERO, defaultTransactionBuilderOptions, } from "@orca-so/common-sdk"; -import { WhirlpoolContext, WhirlpoolContextOpts as WhirlpoolContextOptions } from ".."; +import { WhirlpoolContext, WhirlpoolContextOpts as WhirlpoolContextOptions, toTx } from ".."; +import { NATIVE_MINT } from "@solana/spl-token"; +import { PublicKey } from "@solana/web3.js"; export function convertListToMap(fetchedData: T[], addresses: string[]): Record { const result: Record = {}; @@ -71,3 +78,77 @@ export function contextOptionsToBuilderOptions( defaultTransactionBuilderOptions.defaultConfirmationCommitment, }; } + +export class MultipleTransactionBuilderFactoryWithAccountResolver { + private txBuilders: TransactionBuilder[] = []; + private pendingTxBuilder: TransactionBuilder | null = null; + private touchedMints: Set | null = null; + private accountExemption: number | null = null; + + constructor( + private ctx: WhirlpoolContext, + private resolvedAtas: Record, + private tokenOwner: PublicKey = ctx.wallet.publicKey, + private payer: PublicKey = tokenOwner, + ) {} + + public async addInstructions(generator: (resolve: (mint: string) => PublicKey) => Promise) { + if (this.accountExemption === null) { + this.accountExemption = await this.ctx.fetcher.getAccountRentExempt(); + } + + for (let iter = 0; iter < 2; iter++) { + if (!this.pendingTxBuilder || !this.touchedMints) { + this.pendingTxBuilder = new TransactionBuilder(this.ctx.connection, this.ctx.wallet, this.ctx.txBuilderOpts); + this.touchedMints = new Set(); + this.resolvedAtas[NATIVE_MINT.toBase58()] = TokenUtil.createWrappedNativeAccountInstruction( + this.tokenOwner, + ZERO, + this.accountExemption, + this.payer, + this.tokenOwner, + this.ctx.accountResolverOpts.createWrappedSolAccountMethod + ); + } + + const newTxBuilder = new TransactionBuilder(this.ctx.connection, this.ctx.wallet, this.ctx.txBuilderOpts); + const resolve = (mint: string): PublicKey => { + if (!this.touchedMints!.has(mint)) { + newTxBuilder.addInstruction(this.resolvedAtas[mint]); + this.touchedMints!.add(mint); + } + return this.resolvedAtas[mint].address; + }; + + const ixs = await generator(resolve.bind(this)); + newTxBuilder.addInstructions(ixs); + + // Attempt to push the new instructions into the pending builder + const mergeable = await checkMergedTransactionSizeIsValid( + this.ctx, + [this.pendingTxBuilder, newTxBuilder], + MEASUREMENT_BLOCKHASH + ); + if (mergeable) { + this.pendingTxBuilder.addInstruction(newTxBuilder.compressIx(false)); + break; + } else { + if (iter !== 0) { + throw new Error( + `instruction is too large.` + ); + } + + this.txBuilders.push(this.pendingTxBuilder); + this.pendingTxBuilder = null; + this.touchedMints = null; + } + } + } + + public build(): TransactionBuilder[] { + return this.pendingTxBuilder + ? [...this.txBuilders, this.pendingTxBuilder] + : [...this.txBuilders]; + } +} \ No newline at end of file diff --git a/sdk/src/whirlpool-client.ts b/sdk/src/whirlpool-client.ts index d24b11a4c..304da4168 100644 --- a/sdk/src/whirlpool-client.ts +++ b/sdk/src/whirlpool-client.ts @@ -40,6 +40,7 @@ export interface WhirlpoolClient { * Get a WhirlpoolRouter to help generate the best prices when transacting across a set of pools. * @param poolAddresses the addresses of the Whirlpool account addresses to route through * @returns a {@link WhirlpoolRouter} instance + * @deprecated WhirlpoolRouter will be removed in the future release. Please use endpoint which provides qoutes. */ getRouter: (poolAddresses: Address[]) => Promise; @@ -259,6 +260,7 @@ export interface Whirlpool { * @param destinationWallet - The wallet that the tokens withdrawn and rent lamports will be sent to. If null, the WhirlpoolContext wallet is used. * @param positionWallet - The wallet that houses the position token that corresponds to this position address. If null, the WhirlpoolContext wallet is used. * @param payer - the wallet that will fund the cost needed to initialize the token ATA accounts. If null, the WhirlpoolContext wallet is used. + * @return transactions that will close the position. The transactions must be executed serially. */ closePosition: ( positionAddress: Address, @@ -409,7 +411,7 @@ export interface Position { * @param positionWallet - the wallet to that houses the position token. If null, the WhirlpoolContext wallet is used. * @param ataPayer - wallet that will fund the creation of the new associated token accounts * @param opts an options object to define fetch and cache options when accessing on-chain accounts - * @return the transaction that will collect fees from the position + * @return the transactions that will collect rewards from the position. The transactions must be executed serially. */ collectRewards: ( rewardsToCollect?: Address[], @@ -419,5 +421,5 @@ export interface Position { positionWallet?: Address, ataPayer?: Address, opts?: WhirlpoolAccountFetchOptions - ) => Promise; + ) => Promise; } diff --git a/sdk/tests/external_program/mpl_token_metadata.20240214.so b/sdk/tests/external_program/mpl_token_metadata.20240214.so new file mode 100644 index 000000000..fdc129a7a Binary files /dev/null and b/sdk/tests/external_program/mpl_token_metadata.20240214.so differ diff --git a/sdk/tests/external_program/transfer_hook_counter.so b/sdk/tests/external_program/transfer_hook_counter.so new file mode 100755 index 000000000..2327dd76c Binary files /dev/null and b/sdk/tests/external_program/transfer_hook_counter.so differ diff --git a/sdk/tests/integration/close_bundled_position.test.ts b/sdk/tests/integration/close_bundled_position.test.ts index c972c2c07..e0592e496 100644 --- a/sdk/tests/integration/close_bundled_position.test.ts +++ b/sdk/tests/integration/close_bundled_position.test.ts @@ -23,6 +23,7 @@ import { import { defaultConfirmOptions } from "../utils/const"; import { initTestPool, initializePositionBundle, openBundledPosition, openPosition } from "../utils/init-utils"; import { mintTokensToTestAccount } from "../utils/test-builders"; +import { TokenExtensionUtil } from "../../src/utils/public/token-extension-util"; describe("close_bundled_position", () => { const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); @@ -206,6 +207,7 @@ describe("close_bundled_position", () => { tickCurrentIndex: pool.getData().tickCurrentIndex, inputTokenMint: poolInitInfo.tokenMintB, inputTokenAmount: new BN(1_000_000), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, pool.getData(), IGNORE_CACHE), }); await mintTokensToTestAccount( diff --git a/sdk/tests/integration/collect_fees.test.ts b/sdk/tests/integration/collect_fees.test.ts index feb903118..7f13ec452 100644 --- a/sdk/tests/integration/collect_fees.test.ts +++ b/sdk/tests/integration/collect_fees.test.ts @@ -26,6 +26,7 @@ import { import { defaultConfirmOptions } from "../utils/const"; import { WhirlpoolTestFixture } from "../utils/fixture"; import { initTestPool } from "../utils/init-utils"; +import { TokenExtensionUtil } from "../../src/utils/public/token-extension-util"; describe("collect_fees", () => { const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); @@ -140,6 +141,7 @@ describe("collect_fees", () => { position: positionBeforeCollect, tickLower: lowerTick, tickUpper: upperTick, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), }); // Perform collect fees tx diff --git a/sdk/tests/integration/collect_reward.test.ts b/sdk/tests/integration/collect_reward.test.ts index a39f11b8f..2252b8dbc 100644 --- a/sdk/tests/integration/collect_reward.test.ts +++ b/sdk/tests/integration/collect_reward.test.ts @@ -24,6 +24,7 @@ import { import { defaultConfirmOptions } from "../utils/const"; import { WhirlpoolTestFixture } from "../utils/fixture"; import { initTestPool } from "../utils/init-utils"; +import { TokenExtensionUtil } from "../../src/utils/public/token-extension-util"; describe("collect_reward", () => { const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); @@ -92,12 +93,13 @@ describe("collect_reward", () => { position: positionPreCollect.getData(), tickLower: positionPreCollect.getLowerTickData(), tickUpper: positionPreCollect.getUpperTickData(), - timeStampInSeconds: pool.getData().rewardLastUpdatedTimestamp + timeStampInSeconds: pool.getData().rewardLastUpdatedTimestamp, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, pool.getData(), IGNORE_CACHE), }); // Check that the expectation is not zero for (let i = 0; i < NUM_REWARDS; i++) { - assert.ok(!expectation[i]!.isZero()); + assert.ok(!expectation.rewardOwed[i]!.isZero()); } // Perform collect rewards tx @@ -122,7 +124,7 @@ describe("collect_reward", () => { ).buildAndExecute(); const collectedBalance = parseInt(await getTokenBalance(provider, rewardOwnerAccount)); - assert.equal(collectedBalance, expectation[i]?.toNumber()); + assert.equal(collectedBalance, expectation.rewardOwed[i]?.toNumber()); const vaultBalance = parseInt( await getTokenBalance(provider, rewards[i].rewardVaultKeypair.publicKey) ); diff --git a/sdk/tests/integration/decrease_liquidity.test.ts b/sdk/tests/integration/decrease_liquidity.test.ts index 47b05abc7..e2e4ff891 100644 --- a/sdk/tests/integration/decrease_liquidity.test.ts +++ b/sdk/tests/integration/decrease_liquidity.test.ts @@ -27,6 +27,7 @@ import { import { defaultConfirmOptions } from "../utils/const"; import { WhirlpoolTestFixture } from "../utils/fixture"; import { initTestPool, initTickArray, openPosition } from "../utils/init-utils"; +import { TokenExtensionUtil } from "../../src/utils/public/token-extension-util"; describe("decrease_liquidity", () => { const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); @@ -58,6 +59,7 @@ describe("decrease_liquidity", () => { tickCurrentIndex: poolBefore.tickCurrentIndex, tickLowerIndex: tickLower, tickUpperIndex: tickUpper, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolBefore, IGNORE_CACHE), }); await toTx( @@ -114,6 +116,7 @@ describe("decrease_liquidity", () => { tickCurrentIndex: poolBefore.tickCurrentIndex, tickLowerIndex: tickLower, tickUpperIndex: tickUpper, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolBefore, IGNORE_CACHE), }); await toTx( diff --git a/sdk/tests/integration/get_pool_prices.test.ts b/sdk/tests/integration/get_pool_prices.test.ts index c48851695..b3478842b 100644 --- a/sdk/tests/integration/get_pool_prices.test.ts +++ b/sdk/tests/integration/get_pool_prices.test.ts @@ -155,10 +155,15 @@ describe("get_pool_prices", () => { it("successfully calculates the price for one token with multiple pools against a quote token", async () => { const aqConfig = getDefaultAquarium(); + // Add a third token and account and a second pool + aqConfig.initFeeTierParams.push({ + tickSpacing: TickSpacing.SixtyFour + }); aqConfig.initPoolParams.push({ mintIndices: [0, 1], tickSpacing: TickSpacing.SixtyFour, + feeTierIndex: 1, initSqrtPrice: MathUtil.toX64(new Decimal(5.2)), }); diff --git a/sdk/tests/integration/initialize_pool.test.ts b/sdk/tests/integration/initialize_pool.test.ts index f85d36a1f..8f8e1dd43 100644 --- a/sdk/tests/integration/initialize_pool.test.ts +++ b/sdk/tests/integration/initialize_pool.test.ts @@ -3,6 +3,7 @@ import { MathUtil, PDA } from "@orca-so/common-sdk"; import * as assert from "assert"; import Decimal from "decimal.js"; import { + IGNORE_CACHE, InitPoolParams, MAX_SQRT_PRICE, MIN_SQRT_PRICE, @@ -22,7 +23,7 @@ import { systemTransferTx } from "../utils"; import { defaultConfirmOptions } from "../utils/const"; -import { buildTestPoolParams, initTestPool } from "../utils/init-utils"; +import { buildTestPoolParams, initFeeTier, initTestPool } from "../utils/init-utils"; describe("initialize_pool", () => { const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); @@ -190,13 +191,13 @@ describe("initialize_pool", () => { configInitInfo.whirlpoolsConfigKeypair.publicKey, poolInitInfo.tokenMintB, poolInitInfo.tokenMintA, - TickSpacing.Stable + TickSpacing.Standard ); const modifiedPoolInitInfo: InitPoolParams = { ...poolInitInfo, whirlpoolPda, - tickSpacing: TickSpacing.Stable, + tickSpacing: TickSpacing.Standard, tokenMintA: poolInitInfo.tokenMintB, tokenMintB: poolInitInfo.tokenMintA, }; @@ -215,13 +216,13 @@ describe("initialize_pool", () => { configInitInfo.whirlpoolsConfigKeypair.publicKey, poolInitInfo.tokenMintA, poolInitInfo.tokenMintA, - TickSpacing.Stable + TickSpacing.Standard ); const modifiedPoolInitInfo: InitPoolParams = { ...poolInitInfo, whirlpoolPda, - tickSpacing: TickSpacing.Stable, + tickSpacing: TickSpacing.Standard, tokenMintB: poolInitInfo.tokenMintA, }; @@ -260,6 +261,72 @@ describe("initialize_pool", () => { ); }); + it("fails when FeeTier and tick_spacing passed unmatch", async () => { + const { poolInitInfo, configInitInfo, configKeypairs } = await buildTestPoolParams( + ctx, + TickSpacing.Standard + ); + + // now FeeTier for TickSpacing.Standard is initialized, but not for TickSpacing.Stable + const config = poolInitInfo.whirlpoolsConfig; + const feeTierStandardPda = PDAUtil.getFeeTier(ctx.program.programId, config, TickSpacing.Standard) + const feeTierStablePda = PDAUtil.getFeeTier(ctx.program.programId, config, TickSpacing.Stable); + + const feeTierStandard = await fetcher.getFeeTier(feeTierStandardPda.publicKey, IGNORE_CACHE); + const feeTierStable = await fetcher.getFeeTier(feeTierStablePda.publicKey, IGNORE_CACHE); + assert.ok(feeTierStandard !== null); // should be initialized + assert.ok(feeTierStable === null); // shoud be NOT initialized + + const whirlpoolWithStableTickSpacing = PDAUtil.getWhirlpool( + ctx.program.programId, + config, + poolInitInfo.tokenMintA, + poolInitInfo.tokenMintB, + TickSpacing.Stable, + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializePoolIx(ctx.program, { + ...poolInitInfo, + whirlpoolPda: whirlpoolWithStableTickSpacing, + tickSpacing: TickSpacing.Stable, + feeTierKey: feeTierStandardPda.publicKey, // tickSpacing is Stable, but FeeTier is standard + }) + ).buildAndExecute(), + /custom program error: 0x7d3/ // ConstraintRaw + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializePoolIx(ctx.program, { + ...poolInitInfo, + whirlpoolPda: whirlpoolWithStableTickSpacing, + tickSpacing: TickSpacing.Stable, + feeTierKey: feeTierStablePda.publicKey, // FeeTier is stable, but not initialized + }) + ).buildAndExecute(), + /custom program error: 0xbc4/ // AccountNotInitialized + ); + + await initFeeTier(ctx, configInitInfo, configKeypairs.feeAuthorityKeypair, TickSpacing.Stable, 3000); + const feeTierStableAfterInit = await fetcher.getFeeTier(feeTierStablePda.publicKey, IGNORE_CACHE); + assert.ok(feeTierStableAfterInit !== null); + + // Now it should work because FeeTier for stable have been initialized + await toTx( + ctx, + WhirlpoolIx.initializePoolIx(ctx.program, { + ...poolInitInfo, + whirlpoolPda: whirlpoolWithStableTickSpacing, + tickSpacing: TickSpacing.Stable, + feeTierKey: feeTierStablePda.publicKey, + }) + ).buildAndExecute(); + }) + it("ignore passed bump", async () => { const { poolInitInfo } = await buildTestPoolParams(ctx, TickSpacing.Standard); diff --git a/sdk/tests/integration/initialize_position_bundle_with_metadata.test.ts b/sdk/tests/integration/initialize_position_bundle_with_metadata.test.ts index 55bfbfdff..6af305bf3 100644 --- a/sdk/tests/integration/initialize_position_bundle_with_metadata.test.ts +++ b/sdk/tests/integration/initialize_position_bundle_with_metadata.test.ts @@ -1,5 +1,4 @@ import * as anchor from "@coral-xyz/anchor"; -import { Metadata } from "@metaplex-foundation/mpl-token-metadata"; import { PDA } from "@orca-so/common-sdk"; import { Account, @@ -26,6 +25,7 @@ import { } from "../utils"; import { defaultConfirmOptions } from "../utils/const"; import { initializePositionBundleWithMetadata } from "../utils/init-utils"; +import { MetaplexHttpClient } from "../utils/metaplex"; describe("initialize_position_bundle_with_metadata", () => { const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); @@ -33,6 +33,8 @@ describe("initialize_position_bundle_with_metadata", () => { const program = anchor.workspace.Whirlpool; const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const metaplex = new MetaplexHttpClient(); + async function createInitializePositionBundleWithMetadataTx( ctx: WhirlpoolContext, overwrite: any, @@ -135,13 +137,16 @@ describe("initialize_position_bundle_with_metadata", () => { WPB_METADATA_NAME_PREFIX + " " + mintAddress.slice(0, 4) + "..." + mintAddress.slice(-4); assert.ok(metadataPda != null); - const metadata = await Metadata.fromAccountAddress(provider.connection, metadataPda.publicKey); + const metadataAccountInfo = await provider.connection.getAccountInfo(metadataPda.publicKey); + assert.ok(metadataAccountInfo !== null); + const metadata = metaplex.parseOnChainMetadata(metadataPda.publicKey, metadataAccountInfo!.data); + assert.ok(metadata !== null); assert.ok(metadata.mint.toBase58() === positionMint.toString()); assert.ok(metadata.updateAuthority.toBase58() === WHIRLPOOL_NFT_UPDATE_AUTH.toBase58()); assert.ok(metadata.isMutable); - assert.strictEqual(metadata.data.name.replace(/\0/g, ''), nftName); - assert.strictEqual(metadata.data.symbol.replace(/\0/g, ''), WPB_METADATA_SYMBOL); - assert.strictEqual(metadata.data.uri.replace(/\0/g, ''), WPB_METADATA_URI); + assert.strictEqual(metadata.name.replace(/\0/g, ''), nftName); + assert.strictEqual(metadata.symbol.replace(/\0/g, ''), WPB_METADATA_SYMBOL); + assert.strictEqual(metadata.uri.replace(/\0/g, ''), WPB_METADATA_URI); } async function createOtherWallet(): Promise { @@ -381,7 +386,7 @@ describe("initialize_position_bundle_with_metadata", () => { await assert.rejects( tx.buildAndExecute(), - /0x7dc/ // ConstraintAddress + /0xbc0/ // InvalidProgramId ); }); }); diff --git a/sdk/tests/integration/multi-ix/bundled_position_management.test.ts b/sdk/tests/integration/multi-ix/bundled_position_management.test.ts index 650ac4b3f..6bd0d6327 100644 --- a/sdk/tests/integration/multi-ix/bundled_position_management.test.ts +++ b/sdk/tests/integration/multi-ix/bundled_position_management.test.ts @@ -12,6 +12,7 @@ import { TickSpacing, ZERO_BN, createTokenAccount } from "../../utils"; import { defaultConfirmOptions } from "../../utils/const"; import { WhirlpoolTestFixture } from "../../utils/fixture"; import { initializePositionBundle, openBundledPosition } from "../../utils/init-utils"; +import { TokenExtensionUtil } from "../../../src/utils/public/token-extension-util"; interface SharedTestContext { @@ -147,6 +148,7 @@ describe("bundled position management tests", () => { position: positionData, tickLower: tickLowerData, tickUpper: tickUpperData, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(ctx.fetcher, poolData, IGNORE_CACHE), }); assert.ok(quote.feeOwedA.gtn(0) || quote.feeOwedB.gtn(0)); diff --git a/sdk/tests/integration/open_position_with_metadata.test.ts b/sdk/tests/integration/open_position_with_metadata.test.ts index 0988ffdd2..8ed12d2fa 100644 --- a/sdk/tests/integration/open_position_with_metadata.test.ts +++ b/sdk/tests/integration/open_position_with_metadata.test.ts @@ -1,6 +1,5 @@ import * as anchor from "@coral-xyz/anchor"; import { web3 } from "@coral-xyz/anchor"; -import { Metadata } from "@metaplex-foundation/mpl-token-metadata"; import { PDA, TransactionBuilder } from "@orca-so/common-sdk"; import { TOKEN_PROGRAM_ID, getAccount, getAssociatedTokenAddressSync } from "@solana/spl-token"; import { Keypair, PublicKey } from "@solana/web3.js"; @@ -31,6 +30,7 @@ import { import { defaultConfirmOptions } from "../utils/const"; import { initTestPool, openPositionWithMetadata } from "../utils/init-utils"; import { generateDefaultOpenPositionParams } from "../utils/test-builders"; +import { MetaplexHttpClient } from "../utils/metaplex"; describe("open_position_with_metadata", () => { const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); @@ -39,6 +39,8 @@ describe("open_position_with_metadata", () => { const ctx = WhirlpoolContext.fromWorkspace(provider, program); const fetcher = ctx.fetcher; + const metaplex = new MetaplexHttpClient(); + let defaultParams: Required; let defaultMint: Keypair; const tickLowerIndex = 0; @@ -66,11 +68,15 @@ describe("open_position_with_metadata", () => { async function checkMetadata(metadataPda: PDA | undefined, positionMint: PublicKey) { assert.ok(metadataPda != null); - const metadata = await Metadata.fromAccountAddress(provider.connection, metadataPda.publicKey); + const metadataAccountInfo = await provider.connection.getAccountInfo(metadataPda.publicKey); + assert.ok(metadataAccountInfo !== null); + const metadata = metaplex.parseOnChainMetadata(metadataPda.publicKey, metadataAccountInfo!.data); + assert.ok(metadata !== null); + assert.ok(metadata.updateAuthority.toBase58() === "3axbTs2z5GBy6usVbNVoqEgZMng3vZvMnAoX29BFfwhr"); assert.ok(metadata.mint.toBase58() === positionMint.toString()); assert.ok( - metadata.data.uri.replace(/\0/g, '') === `https://arweave.net/E19ZNY2sqMqddm1Wx7mrXPUZ0ZZ5ISizhebb0UsVEws` + metadata.uri.replace(/\0/g, '') === `https://arweave.net/E19ZNY2sqMqddm1Wx7mrXPUZ0ZZ5ISizhebb0UsVEws` ); } @@ -301,9 +307,9 @@ describe("open_position_with_metadata", () => { await assert.rejects( tx.addSigner(defaultMint).buildAndExecute(), - // AddressConstraint - // https://github.com/project-serum/anchor/blob/master/lang/src/error.rs#L84 - /0x7dc/ + // InvalidProgramId + // https://github.com/project-serum/anchor/blob/master/lang/src/error.rs#L180 + /0xbc0/ ); }); @@ -316,9 +322,9 @@ describe("open_position_with_metadata", () => { await assert.rejects( tx.addSigner(defaultMint).buildAndExecute(), - // AddressConstraint - // https://github.com/project-serum/anchor/blob/master/lang/src/error.rs#L84 - /0x7dc/ + // InvalidProgramId + // https://github.com/project-serum/anchor/blob/master/lang/src/error.rs#L180 + /0xbc0/ ); }); diff --git a/sdk/tests/integration/set_default_fee_rate.test.ts b/sdk/tests/integration/set_default_fee_rate.test.ts index ef442fc79..0a1b6dbf2 100644 --- a/sdk/tests/integration/set_default_fee_rate.test.ts +++ b/sdk/tests/integration/set_default_fee_rate.test.ts @@ -58,7 +58,7 @@ describe("set_default_fee_rate", () => { whirlpoolsConfigKey, tokenMintA, tokenMintB, - TickSpacing.Stable + TickSpacing.Standard ); const tokenVaultAKeypair = anchor.web3.Keypair.generate(); const tokenVaultBKeypair = anchor.web3.Keypair.generate(); @@ -70,7 +70,7 @@ describe("set_default_fee_rate", () => { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair, - tickSpacing: TickSpacing.Stable, + tickSpacing: TickSpacing.Standard, }; await toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, newPoolInitInfo)).buildAndExecute(); @@ -107,7 +107,7 @@ describe("set_default_fee_rate", () => { whirlpoolsConfigKey, tokenMintA, tokenMintB, - TickSpacing.Stable + TickSpacing.Standard ); const tokenVaultAKeypair = anchor.web3.Keypair.generate(); const tokenVaultBKeypair = anchor.web3.Keypair.generate(); @@ -119,7 +119,7 @@ describe("set_default_fee_rate", () => { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair, - tickSpacing: TickSpacing.Stable, + tickSpacing: TickSpacing.Standard, }; await toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, newPoolInitInfo)).buildAndExecute(); diff --git a/sdk/tests/integration/set_default_protocol_fee_rate.test.ts b/sdk/tests/integration/set_default_protocol_fee_rate.test.ts index 5eef75651..70d221218 100644 --- a/sdk/tests/integration/set_default_protocol_fee_rate.test.ts +++ b/sdk/tests/integration/set_default_protocol_fee_rate.test.ts @@ -56,7 +56,7 @@ describe("set_default_protocol_fee_rate", () => { whirlpoolsConfigKey, tokenMintA, tokenMintB, - TickSpacing.Stable + TickSpacing.Standard ); const tokenVaultAKeypair = anchor.web3.Keypair.generate(); const tokenVaultBKeypair = anchor.web3.Keypair.generate(); @@ -68,7 +68,7 @@ describe("set_default_protocol_fee_rate", () => { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair, - tickSpacing: TickSpacing.Stable, + tickSpacing: TickSpacing.Standard, }; await toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, newPoolInitInfo)).buildAndExecute(); diff --git a/sdk/tests/integration/v2/collect_fees_v2.test.ts b/sdk/tests/integration/v2/collect_fees_v2.test.ts new file mode 100644 index 000000000..ea5a6bca9 --- /dev/null +++ b/sdk/tests/integration/v2/collect_fees_v2.test.ts @@ -0,0 +1,1373 @@ +import * as anchor from "@coral-xyz/anchor"; +import { BN } from "@coral-xyz/anchor"; +import { MathUtil } from "@orca-so/common-sdk"; +import * as assert from "assert"; +import Decimal from "decimal.js"; +import { + collectFeesQuote, + METADATA_PROGRAM_ADDRESS, + PDAUtil, + PositionData, + TickArrayData, + TickArrayUtil, + toTx, + WhirlpoolContext, + WhirlpoolData, + WhirlpoolIx, +} from "../../../src"; +import { IGNORE_CACHE } from "../../../src/network/public/fetcher"; +import { + approveToken, + getTokenBalance, + TEST_TOKEN_2022_PROGRAM_ID, + TEST_TOKEN_PROGRAM_ID, + TickSpacing, + transferToken, + ZERO_BN, +} from "../../utils"; +import { defaultConfirmOptions } from "../../utils/const"; +import { WhirlpoolTestFixtureV2 } from "../../utils/v2/fixture-v2"; +import { TokenTrait } from "../../utils/v2/init-utils-v2"; +import { createMintV2, createTokenAccountV2 } from "../../utils/v2/token-2022"; +import { createTokenAccount as createTokenAccountForPosition } from "../../utils/token"; +import { NATIVE_MINT } from "@solana/spl-token"; +import { RemainingAccountsSliceData } from "../../../src/utils/remaining-accounts-util"; +import { TokenExtensionUtil } from "../../../src/utils/public/token-extension-util"; + +describe("collect_fees_v2", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + + describe("v1 parity", () => { + const tokenTraitVariations: { tokenTraitA: TokenTrait; tokenTraitB: TokenTrait }[] = [ + { tokenTraitA: { isToken2022: false }, tokenTraitB: { isToken2022: false } }, + { tokenTraitA: { isToken2022: true }, tokenTraitB: { isToken2022: false } }, + { tokenTraitA: { isToken2022: false }, tokenTraitB: { isToken2022: true } }, + { tokenTraitA: { isToken2022: true }, tokenTraitB: { isToken2022: true } }, + ]; + tokenTraitVariations.forEach((tokenTraits) => { + describe(`tokenTraitA: ${ + tokenTraits.tokenTraitA.isToken2022 ? "Token2022" : "Token" + }, tokenTraitB: ${tokenTraits.tokenTraitB.isToken2022 ? "Token2022" : "Token"}`, () => { + it("successfully collect fees", async () => { + // In same tick array - start index 22528 + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }, // In range position + { tickLowerIndex: 0, tickUpperIndex: 128, liquidityAmount: new anchor.BN(1_000_000) }, // Out of range position + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + const tickArrayPda = PDAUtil.getTickArray( + ctx.program.programId, + whirlpoolPda.publicKey, + 22528 + ); + const positionBeforeSwap = (await fetcher.getPosition( + positions[0].publicKey + )) as PositionData; + assert.ok(positionBeforeSwap.feeOwedA.eq(ZERO_BN)); + assert.ok(positionBeforeSwap.feeOwedB.eq(ZERO_BN)); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + // Accrue fees in token A + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + // Accrue fees in token B + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(5)), + amountSpecifiedIsInput: true, + aToB: false, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + await toTx( + ctx, + WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + position: positions[0].publicKey, + tickArrayLower: tickArrayPda.publicKey, + tickArrayUpper: tickArrayPda.publicKey, + }) + ).buildAndExecute(); + + const positionBeforeCollect = (await fetcher.getPosition( + positions[0].publicKey, + IGNORE_CACHE + )) as PositionData; + assert.ok(positionBeforeCollect.feeOwedA.eq(new BN(581))); + assert.ok(positionBeforeCollect.feeOwedB.eq(new BN(581))); + + const feeAccountA = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitA, + tokenMintA, + provider.wallet.publicKey + ); + const feeAccountB = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitB, + tokenMintB, + provider.wallet.publicKey + ); + + // Generate collect fees expectation + const whirlpoolData = (await fetcher.getPool(whirlpoolPda.publicKey)) as WhirlpoolData; + const tickArrayData = (await fetcher.getTickArray( + tickArrayPda.publicKey + )) as TickArrayData; + const lowerTick = TickArrayUtil.getTickFromArray( + tickArrayData, + tickLowerIndex, + tickSpacing + ); + const upperTick = TickArrayUtil.getTickFromArray( + tickArrayData, + tickUpperIndex, + tickSpacing + ); + const expectation = collectFeesQuote({ + whirlpool: whirlpoolData, + position: positionBeforeCollect, + tickLower: lowerTick, + tickUpper: upperTick, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }); + + // Perform collect fees tx + await toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(); + const positionAfter = (await fetcher.getPosition( + positions[0].publicKey, + IGNORE_CACHE + )) as PositionData; + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + + assert.equal(feeBalanceA, expectation.feeOwedA); + assert.equal(feeBalanceB, expectation.feeOwedB); + assert.ok(positionAfter.feeOwedA.eq(ZERO_BN)); + assert.ok(positionAfter.feeOwedB.eq(ZERO_BN)); + + // Assert out of range position values + await toTx( + ctx, + WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + position: positions[1].publicKey, + tickArrayLower: positions[1].tickArrayLower, + tickArrayUpper: positions[1].tickArrayUpper, + }) + ).buildAndExecute(); + const outOfRangePosition = await fetcher.getPosition( + positions[1].publicKey, + IGNORE_CACHE + ); + assert.ok(outOfRangePosition?.feeOwedA.eq(ZERO_BN)); + assert.ok(outOfRangePosition?.feeOwedB.eq(ZERO_BN)); + }); + + it("successfully collect fees with approved delegate", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { + tickLowerIndex: 0, + tickUpperIndex: 128, + liquidityAmount: new anchor.BN(10_000_000), + }, // In range position + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + const position = positions[0]; + + const delegate = anchor.web3.Keypair.generate(); + await approveToken(provider, position.tokenAccount, delegate.publicKey, 1); + + await toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ) + .addSigner(delegate) + .buildAndExecute(); + }); + + it("successfully collect fees with owner even if there is approved delegate", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { + tickLowerIndex: 0, + tickUpperIndex: 128, + liquidityAmount: new anchor.BN(10_000_000), + }, // In range position + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + const position = positions[0]; + + const delegate = anchor.web3.Keypair.generate(); + await approveToken(provider, position.tokenAccount, delegate.publicKey, 1); + + await toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(); + }); + + it("successfully collect fees with transferred position token", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { + tickLowerIndex: 0, + tickUpperIndex: 128, + liquidityAmount: new anchor.BN(10_000_000), + }, // In range position + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + const position = positions[0]; + + const newOwner = anchor.web3.Keypair.generate(); + const newOwnerPositionTokenAccount = await createTokenAccountForPosition( + provider, + position.mintKeypair.publicKey, + newOwner.publicKey + ); + + await transferToken(provider, position.tokenAccount, newOwnerPositionTokenAccount, 1); + + await toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: newOwner.publicKey, + position: position.publicKey, + positionTokenAccount: newOwnerPositionTokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ) + .addSigner(newOwner) + .buildAndExecute(); + }); + + it("fails when position does not match whirlpool", async () => { + // In same tick array - start index 22528 + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }, + ], + }); + const { + poolInitInfo: { + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + const anotherFixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + }); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: anotherFixture.getInfos().poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(), + /0x7d1/ // ConstraintHasOne + ); + }); + + it("fails when position token account does not contain exactly one token", async () => { + // In same tick array - start index 22528 + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + const positionTokenAccount2 = await createTokenAccountForPosition( + provider, + positions[0].mintKeypair.publicKey, + provider.wallet.publicKey + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positionTokenAccount2, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + + await transferToken(provider, positions[0].tokenAccount, positionTokenAccount2, 1); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + + it("fails when position authority is not approved delegate for position token account", async () => { + // In same tick array - start index 22528 + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + const delegate = anchor.web3.Keypair.generate(); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ) + .addSigner(delegate) + .buildAndExecute(), + /0x1783/ // MissingOrInvalidDelegate + ); + }); + + it("fails when position authority is not authorized to transfer exactly one token", async () => { + // In same tick array - start index 22528 + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + const delegate = anchor.web3.Keypair.generate(); + await approveToken(provider, positions[0].tokenAccount, delegate.publicKey, 2); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ) + .addSigner(delegate) + .buildAndExecute(), + /0x1784/ // InvalidPositionTokenAmount + ); + }); + + it("fails when position authority is not a signer", async () => { + // In same tick array - start index 22528 + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + const delegate = anchor.web3.Keypair.generate(); + await approveToken(provider, positions[0].tokenAccount, delegate.publicKey, 1); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(), + /.*signature verification fail.*/i + ); + }); + + it("fails when position token account mint does not equal position mint", async () => { + // In same tick array - start index 22528 + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + const fakePositionTokenAccount = await createTokenAccountForPosition( + provider, + NATIVE_MINT, + provider.wallet.publicKey + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: fakePositionTokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + + it("fails when token vault does not match whirlpool token vault", async () => { + // In same tick array - start index 22528 + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + const fakeVaultA = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitA, + tokenMintA, + provider.wallet.publicKey + ); + const fakeVaultB = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitB, + tokenMintB, + provider.wallet.publicKey + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: fakeVaultA, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: fakeVaultB, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when owner token account mint does not match whirlpool token mint", async () => { + // In same tick array - start index 22528 + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + const invalidOwnerAccountA = await createTokenAccountV2( + provider, + // invalid token trait & mint + tokenTraits.tokenTraitB, + tokenMintB, + provider.wallet.publicKey + ); + const invalidOwnerAccountB = await createTokenAccountV2( + provider, + // invalid token trait & mint + tokenTraits.tokenTraitA, + tokenMintA, + provider.wallet.publicKey + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: invalidOwnerAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: invalidOwnerAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + }); + }); + }); + + describe("v2 specific accounts", () => { + it("fails when passed token_mint_a does not match whirlpool's token_mint_a", async () => { + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + //tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + const otherTokenPublicKey = await createMintV2(provider, { isToken2022: true }); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA: otherTokenPublicKey, // invalid + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_mint_b does not match whirlpool's token_mint_b", async () => { + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + //tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + const otherTokenPublicKey = await createMintV2(provider, { isToken2022: true }); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB: otherTokenPublicKey, // invalid + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_a is not token program (token-2022 is passed)", async () => { + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + tickSpacing, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + assert.ok(tokenProgramA.equals(TEST_TOKEN_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA: TEST_TOKEN_2022_PROGRAM_ID, // invalid + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_a is not token-2022 program (token is passed)", async () => { + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + assert.ok(tokenProgramA.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA: TEST_TOKEN_PROGRAM_ID, // invalid + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_a is token_metadata", async () => { + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + assert.ok(tokenProgramA.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA: METADATA_PROGRAM_ADDRESS, // invalid + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("fails when passed token_program_b is not token program (token-2022 is passed)", async () => { + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + tickSpacing, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + assert.ok(tokenProgramB.equals(TEST_TOKEN_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB: TEST_TOKEN_2022_PROGRAM_ID, // invalid + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_b is not token-2022 program (token is passed)", async () => { + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + assert.ok(tokenProgramB.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB: TEST_TOKEN_PROGRAM_ID, // invalid + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_b is token_metadata", async () => { + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + assert.ok(tokenProgramB.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB: METADATA_PROGRAM_ADDRESS, // invalid + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("fails when passed memo_program is token_metadata", async () => { + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + const invalidMemoProgram = METADATA_PROGRAM_ADDRESS; + + await assert.rejects( + toTx(ctx, { + cleanupInstructions: [], + signers: [], + instructions: [ + ctx.program.instruction.collectFeesV2( + { slices: [] }, + { + accounts: { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenProgramA, + tokenProgramB, + memoProgram: invalidMemoProgram, + }, + } + ), + ], + }).buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + }); +}); diff --git a/sdk/tests/integration/v2/collect_protocol_fees_v2.test.ts b/sdk/tests/integration/v2/collect_protocol_fees_v2.test.ts new file mode 100644 index 000000000..40503fbe6 --- /dev/null +++ b/sdk/tests/integration/v2/collect_protocol_fees_v2.test.ts @@ -0,0 +1,1030 @@ +import * as anchor from "@coral-xyz/anchor"; +import { BN } from "@coral-xyz/anchor"; +import { MathUtil } from "@orca-so/common-sdk"; +import * as assert from "assert"; +import Decimal from "decimal.js"; +import { + METADATA_PROGRAM_ADDRESS, + PDAUtil, + toTx, + WhirlpoolContext, + WhirlpoolData, + WhirlpoolIx, +} from "../../../src"; +import { IGNORE_CACHE } from "../../../src/network/public/fetcher"; +import { + getTokenBalance, + TEST_TOKEN_2022_PROGRAM_ID, + TEST_TOKEN_PROGRAM_ID, + TickSpacing, + ZERO_BN, +} from "../../utils"; +import { defaultConfirmOptions } from "../../utils/const"; +import { WhirlpoolTestFixtureV2 } from "../../utils/v2/fixture-v2"; +import { TokenTrait } from "../../utils/v2/init-utils-v2"; +import { createMintV2, createTokenAccountV2 } from "../../utils/v2/token-2022"; + +describe("collect_protocol_fees_v2", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + + describe("v1 parity", () => { + const tokenTraitVariations: { tokenTraitA: TokenTrait; tokenTraitB: TokenTrait }[] = [ + { tokenTraitA: { isToken2022: false }, tokenTraitB: { isToken2022: false } }, + { tokenTraitA: { isToken2022: true }, tokenTraitB: { isToken2022: false } }, + { tokenTraitA: { isToken2022: false }, tokenTraitB: { isToken2022: true } }, + { tokenTraitA: { isToken2022: true }, tokenTraitB: { isToken2022: true } }, + ]; + tokenTraitVariations.forEach((tokenTraits) => { + describe(`tokenTraitA: ${ + tokenTraits.tokenTraitA.isToken2022 ? "Token2022" : "Token" + }, tokenTraitB: ${tokenTraits.tokenTraitB.isToken2022 ? "Token2022" : "Token"}`, () => { + it("successfully collects fees", async () => { + // In same tick array - start index 22528 + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { feeAuthorityKeypair, collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKeypair }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + await toTx( + ctx, + WhirlpoolIx.setProtocolFeeRateIx(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + feeAuthority: feeAuthorityKeypair.publicKey, + protocolFeeRate: 2500, + }) + ) + .addSigner(feeAuthorityKeypair) + .buildAndExecute(); + + const poolBefore = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + assert.ok(poolBefore?.protocolFeeOwedA.eq(ZERO_BN)); + assert.ok(poolBefore?.protocolFeeOwedB.eq(ZERO_BN)); + + const tickArrayPda = positions[0].tickArrayLower; + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + // Accrue fees in token A + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda, + tickArray1: tickArrayPda, + tickArray2: tickArrayPda, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + // Accrue fees in token B + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(5)), + amountSpecifiedIsInput: true, + aToB: false, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda, + tickArray1: tickArrayPda, + tickArray2: tickArrayPda, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + const poolAfter = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + assert.ok(poolAfter?.protocolFeeOwedA.eq(new BN(150))); + assert.ok(poolAfter?.protocolFeeOwedB.eq(new BN(150))); + + const destA = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitA, + tokenMintA, + provider.wallet.publicKey + ); + const destB = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitB, + tokenMintB, + provider.wallet.publicKey + ); + + await toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: destA, + tokenOwnerAccountB: destB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(); + + const balanceDestA = await getTokenBalance(provider, destA); + const balanceDestB = await getTokenBalance(provider, destB); + assert.equal(balanceDestA, "150"); + assert.equal(balanceDestB, "150"); + assert.ok(poolBefore?.protocolFeeOwedA.eq(ZERO_BN)); + assert.ok(poolBefore?.protocolFeeOwedB.eq(ZERO_BN)); + }); + + it("fails to collect fees without the authority's signature", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { + tickLowerIndex: 29440, + tickUpperIndex: 33536, + liquidityAmount: new anchor.BN(10_000_000), + }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair }, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + }) + ).buildAndExecute(), + /.*signature verification fail.*/i + ); + }); + + it("fails when collect_protocol_fees_authority is invalid", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { + tickLowerIndex: 29440, + tickUpperIndex: 33536, + liquidityAmount: new anchor.BN(10_000_000), + }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { rewardEmissionsSuperAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair }, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: rewardEmissionsSuperAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + }) + ) + .addSigner(rewardEmissionsSuperAuthorityKeypair) + .buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when whirlpool does not match config", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { + tickLowerIndex: 29440, + tickUpperIndex: 33536, + liquidityAmount: new anchor.BN(10_000_000), + }, + ], + }); + const { + poolInitInfo: { + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair }, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + + const anotherFixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + }); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: anotherFixture.getInfos().poolInitInfo.whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(), + /0x7d1/ // ConstraintHasOne + ); + }); + + it("fails when vaults do not match whirlpool vaults", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { + tickLowerIndex: 29440, + tickUpperIndex: 33536, + liquidityAmount: new anchor.BN(10_000_000), + }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKeypair }, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + + const fakeVaultA = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitA, + tokenMintA, + provider.wallet.publicKey + ); + const fakeVaultB = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitB, + tokenMintB, + provider.wallet.publicKey + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: fakeVaultA, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: fakeVaultB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when destination mints do not match whirlpool mints", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { + tickLowerIndex: 29440, + tickUpperIndex: 33536, + liquidityAmount: new anchor.BN(10_000_000), + }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKepair }, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + + const invalidDestA = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitB, + tokenMintB, + provider.wallet.publicKey + ); + const invalidDestB = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitA, + tokenMintA, + provider.wallet.publicKey + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKepair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: invalidDestA, + tokenOwnerAccountB: tokenAccountB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKepair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: invalidDestB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + }); + }); + }); + + describe("v2 specific accounts", () => { + it("fails when passed token_mint_a does not match whirlpool's token_mint_a", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing, + positions: [ + { + tickLowerIndex: 29440, + tickUpperIndex: 33536, + liquidityAmount: new anchor.BN(10_000_000), + }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + //tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair }, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + + const otherTokenPublicKey = await createMintV2(provider, { isToken2022: true }); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA: otherTokenPublicKey, // invalid + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_mint_b does not match whirlpool's token_mint_b", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing, + positions: [ + { + tickLowerIndex: 29440, + tickUpperIndex: 33536, + liquidityAmount: new anchor.BN(10_000_000), + }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + //tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair }, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + + const otherTokenPublicKey = await createMintV2(provider, { isToken2022: true }); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB: otherTokenPublicKey, // invalid + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_a is not token program (token-2022 is passed)", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + tickSpacing, + positions: [ + { + tickLowerIndex: 29440, + tickUpperIndex: 33536, + liquidityAmount: new anchor.BN(10_000_000), + }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair }, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + + assert.ok(tokenProgramA.equals(TEST_TOKEN_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA: TEST_TOKEN_2022_PROGRAM_ID, // invalid + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_a is not token-2022 program (token is passed)", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing, + positions: [ + { + tickLowerIndex: 29440, + tickUpperIndex: 33536, + liquidityAmount: new anchor.BN(10_000_000), + }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair }, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + + assert.ok(tokenProgramA.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA: TEST_TOKEN_PROGRAM_ID, // invalid + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_a is token_metadata", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing, + positions: [ + { + tickLowerIndex: 29440, + tickUpperIndex: 33536, + liquidityAmount: new anchor.BN(10_000_000), + }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair }, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + + assert.ok(tokenProgramA.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA: METADATA_PROGRAM_ADDRESS, // invalid + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("fails when passed token_program_b is not token program (token-2022 is passed)", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + tickSpacing, + positions: [ + { + tickLowerIndex: 29440, + tickUpperIndex: 33536, + liquidityAmount: new anchor.BN(10_000_000), + }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair }, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + + assert.ok(tokenProgramB.equals(TEST_TOKEN_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB: TEST_TOKEN_2022_PROGRAM_ID, // invalid + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_b is not token-2022 program (token is passed)", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing, + positions: [ + { + tickLowerIndex: 29440, + tickUpperIndex: 33536, + liquidityAmount: new anchor.BN(10_000_000), + }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair }, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + + assert.ok(tokenProgramB.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB: TEST_TOKEN_PROGRAM_ID, // invalid + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_b is token_metadata", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing, + positions: [ + { + tickLowerIndex: 29440, + tickUpperIndex: 33536, + liquidityAmount: new anchor.BN(10_000_000), + }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair }, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + + assert.ok(tokenProgramB.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB: METADATA_PROGRAM_ADDRESS, // invalid + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("fails when passed memo_program is token_metadata", async () => { + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing, + positions: [ + { + tickLowerIndex: 29440, + tickUpperIndex: 33536, + liquidityAmount: new anchor.BN(10_000_000), + }, + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair }, + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + + const invalidMemoProgram = METADATA_PROGRAM_ADDRESS; + + await assert.rejects( + toTx(ctx, { + cleanupInstructions: [], + signers: [], + instructions: [ + ctx.program.instruction.collectProtocolFeesV2( + { slices: [] }, + { + accounts: { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenDestinationA: tokenAccountA, + tokenDestinationB: tokenAccountB, + memoProgram: invalidMemoProgram, + }, + } + ), + ], + }) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + }); +}); diff --git a/sdk/tests/integration/v2/collect_reward_v2.test.ts b/sdk/tests/integration/v2/collect_reward_v2.test.ts new file mode 100644 index 000000000..3a2e2e7b3 --- /dev/null +++ b/sdk/tests/integration/v2/collect_reward_v2.test.ts @@ -0,0 +1,1236 @@ +import * as anchor from "@coral-xyz/anchor"; +import { BN } from "@coral-xyz/anchor"; +import { MathUtil } from "@orca-so/common-sdk"; +import * as assert from "assert"; +import Decimal from "decimal.js"; +import { + buildWhirlpoolClient, + collectRewardsQuote, + METADATA_PROGRAM_ADDRESS, + NUM_REWARDS, + toTx, + WhirlpoolContext, + WhirlpoolData, + WhirlpoolIx, +} from "../../../src"; +import { IGNORE_CACHE } from "../../../src/network/public/fetcher"; +import { + approveToken, + getTokenBalance, + sleep, + TEST_TOKEN_2022_PROGRAM_ID, + TEST_TOKEN_PROGRAM_ID, + TickSpacing, + transferToken, + ZERO_BN, +} from "../../utils"; +import { defaultConfirmOptions } from "../../utils/const"; +import { WhirlpoolTestFixtureV2 } from "../../utils/v2/fixture-v2"; +import { TokenTrait } from "../../utils/v2/init-utils-v2"; +import { createTokenAccountV2, createMintV2 } from "../../utils/v2/token-2022"; +import { createTokenAccount as createTokenAccountForPosition } from "../../utils/token"; +import { NATIVE_MINT } from "@solana/spl-token"; +import { TokenExtensionUtil } from "../../../src/utils/public/token-extension-util"; + +describe("collect_reward_v2", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + const client = buildWhirlpoolClient(ctx); + + describe("v1 parity", () => { + const tokenTraitVariations: { tokenTraitAB: TokenTrait; tokenTraitR: TokenTrait }[] = [ + { tokenTraitAB: { isToken2022: false }, tokenTraitR: { isToken2022: false } }, + { tokenTraitAB: { isToken2022: true }, tokenTraitR: { isToken2022: false } }, + { tokenTraitAB: { isToken2022: false }, tokenTraitR: { isToken2022: true } }, + { tokenTraitAB: { isToken2022: true }, tokenTraitR: { isToken2022: true } }, + ]; + tokenTraitVariations.forEach((tokenTraits) => { + describe(`tokenTraitA/B: ${ + tokenTraits.tokenTraitAB.isToken2022 ? "Token2022" : "Token" + }, tokenTraitReward: ${tokenTraits.tokenTraitR.isToken2022 ? "Token2022" : "Token"}`, () => { + it("successfully collect rewards", async () => { + const vaultStartBalance = 1_000_000; + const lowerTickIndex = -1280, + upperTickIndex = 1280, + tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: tokenTraits.tokenTraitAB, + tokenTraitB: tokenTraits.tokenTraitAB, + tickSpacing: tickSpacing, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: lowerTickIndex, + tickUpperIndex: upperTickIndex, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + // accrue rewards + await sleep(1200); + + await toTx( + ctx, + WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + position: positions[0].publicKey, + tickArrayLower: positions[0].tickArrayLower, + tickArrayUpper: positions[0].tickArrayUpper, + }) + ).buildAndExecute(); + + // Generate collect reward expectation + const whirlpoolData = (await fetcher.getPool(whirlpoolPda.publicKey)) as WhirlpoolData; + const positionPreCollect = await client.getPosition(positions[0].publicKey, IGNORE_CACHE); + + // Lock the collectRewards quote to the last time we called updateFeesAndRewards + const expectation = collectRewardsQuote({ + whirlpool: whirlpoolData, + position: positionPreCollect.getData(), + tickLower: positionPreCollect.getLowerTickData(), + tickUpper: positionPreCollect.getUpperTickData(), + timeStampInSeconds: whirlpoolData.rewardLastUpdatedTimestamp, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }); + + // Check that the expectation is not zero + for (let i = 0; i < NUM_REWARDS; i++) { + assert.ok(!expectation.rewardOwed[i]!.isZero()); + } + + // Perform collect rewards tx + for (let i = 0; i < NUM_REWARDS; i++) { + const rewardOwnerAccount = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitR, + rewards[i].rewardMint, + provider.wallet.publicKey + ); + + await toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[i].rewardMint, + rewardTokenProgram: rewards[i].tokenProgram, + rewardOwnerAccount: rewardOwnerAccount, + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + rewardIndex: i, + }) + ).buildAndExecute(); + + const collectedBalance = parseInt(await getTokenBalance(provider, rewardOwnerAccount)); + assert.equal(collectedBalance, expectation.rewardOwed[i]?.toNumber()); + const vaultBalance = parseInt( + await getTokenBalance(provider, rewards[i].rewardVaultKeypair.publicKey) + ); + assert.equal(vaultStartBalance - collectedBalance, vaultBalance); + const position = await fetcher.getPosition(positions[0].publicKey, IGNORE_CACHE); + assert.equal(position?.rewardInfos[i].amountOwed, 0); + assert.ok(position?.rewardInfos[i].growthInsideCheckpoint.gte(ZERO_BN)); + } + }); + + it("successfully collect reward with a position authority delegate", async () => { + const vaultStartBalance = 1_000_000; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: tokenTraits.tokenTraitAB, + tokenTraitB: tokenTraits.tokenTraitAB, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: -1280, + tickUpperIndex: 1280, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + // accrue rewards + await sleep(1200); + + const rewardOwnerAccount = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitR, + rewards[0].rewardMint, + provider.wallet.publicKey + ); + + await toTx( + ctx, + WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + position: positions[0].publicKey, + tickArrayLower: positions[0].tickArrayLower, + tickArrayUpper: positions[0].tickArrayUpper, + }) + ).buildAndExecute(); + + const delegate = anchor.web3.Keypair.generate(); + await approveToken(provider, positions[0].tokenAccount, delegate.publicKey, 1); + + await toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[0].rewardMint, + rewardTokenProgram: rewards[0].tokenProgram, + rewardOwnerAccount, + rewardVault: rewards[0].rewardVaultKeypair.publicKey, + rewardIndex: 0, + }) + ) + .addSigner(delegate) + .buildAndExecute(); + }); + + it("successfully collect reward with transferred position token", async () => { + const vaultStartBalance = 1_000_000; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: tokenTraits.tokenTraitAB, + tokenTraitB: tokenTraits.tokenTraitAB, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: -1280, + tickUpperIndex: 1280, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + // accrue rewards + await sleep(1200); + + const rewardOwnerAccount = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitR, + rewards[0].rewardMint, + provider.wallet.publicKey + ); + + const delegate = anchor.web3.Keypair.generate(); + const delegatePositionAccount = await createTokenAccountForPosition( + provider, + positions[0].mintKeypair.publicKey, + delegate.publicKey + ); + await transferToken(provider, positions[0].tokenAccount, delegatePositionAccount, 1); + + await toTx( + ctx, + WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + position: positions[0].publicKey, + tickArrayLower: positions[0].tickArrayLower, + tickArrayUpper: positions[0].tickArrayUpper, + }) + ).buildAndExecute(); + + await toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: positions[0].publicKey, + positionTokenAccount: delegatePositionAccount, + rewardMint: rewards[0].rewardMint, + rewardTokenProgram: rewards[0].tokenProgram, + rewardOwnerAccount, + rewardVault: rewards[0].rewardVaultKeypair.publicKey, + rewardIndex: 0, + }) + ) + .addSigner(delegate) + .buildAndExecute(); + }); + + it("successfully collect reward with owner even when there is a delegate", async () => { + const vaultStartBalance = 1_000_000; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: tokenTraits.tokenTraitAB, + tokenTraitB: tokenTraits.tokenTraitAB, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: -1280, + tickUpperIndex: 1280, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + // accrue rewards + await sleep(1200); + + const rewardOwnerAccount = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitR, + rewards[0].rewardMint, + provider.wallet.publicKey + ); + + await toTx( + ctx, + WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + position: positions[0].publicKey, + tickArrayLower: positions[0].tickArrayLower, + tickArrayUpper: positions[0].tickArrayUpper, + }) + ).buildAndExecute(); + + const delegate = anchor.web3.Keypair.generate(); + await approveToken(provider, positions[0].tokenAccount, delegate.publicKey, 1); + + await toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[0].rewardMint, + rewardTokenProgram: rewards[0].tokenProgram, + rewardOwnerAccount, + rewardVault: rewards[0].rewardVaultKeypair.publicKey, + rewardIndex: 0, + }) + ).buildAndExecute(); + }); + + it("fails when reward index references an uninitialized reward", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: tokenTraits.tokenTraitAB, + tokenTraitB: tokenTraits.tokenTraitAB, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: -1280, + tickUpperIndex: 1280, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + } = fixture.getInfos(); + + // accrue rewards + await sleep(1200); + + const fakeRewardMint = await createMintV2(provider, tokenTraits.tokenTraitR); + const rewardOwnerAccount = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitR, + fakeRewardMint, + provider.wallet.publicKey + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: fakeRewardMint, + rewardTokenProgram: tokenTraits.tokenTraitR.isToken2022 + ? TEST_TOKEN_2022_PROGRAM_ID + : TEST_TOKEN_PROGRAM_ID, + rewardOwnerAccount, + rewardVault: anchor.web3.PublicKey.default, + rewardIndex: 0, + }) + ).buildAndExecute(), + /0xbbf/ // AccountNotInitialized + ); + }); + + it("fails when position does not match whirlpool", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: tokenTraits.tokenTraitAB, + tokenTraitB: tokenTraits.tokenTraitAB, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: -1280, + tickUpperIndex: 1280, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), + vaultAmount: new BN(1_000_000), + }, + ], + }); + const { positions, rewards } = fixture.getInfos(); + + // accrue rewards + await sleep(1200); + + const anotherFixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: tokenTraits.tokenTraitAB, + tokenTraitB: tokenTraits.tokenTraitAB, + tickSpacing: TickSpacing.Standard, + }); + + const rewardOwnerAccount = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitR, + rewards[0].rewardMint, + provider.wallet.publicKey + ); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: anotherFixture.getInfos().poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[0].rewardMint, + rewardTokenProgram: rewards[0].tokenProgram, + rewardOwnerAccount, + rewardVault: rewards[0].rewardVaultKeypair.publicKey, + rewardIndex: 0, + }) + ).buildAndExecute(), + /0x7d1/ // ConstraintHasOne + ); + }); + + it("fails when position token account does not have exactly one token", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: tokenTraits.tokenTraitAB, + tokenTraitB: tokenTraits.tokenTraitAB, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: -1280, + tickUpperIndex: 1280, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), + vaultAmount: new BN(1_000_000), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + // accrue rewards + await sleep(1200); + + const rewardOwnerAccount = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitR, + rewards[0].rewardMint, + provider.wallet.publicKey + ); + const otherPositionAcount = await createTokenAccountForPosition( + provider, + positions[0].mintKeypair.publicKey, + provider.wallet.publicKey + ); + await transferToken(provider, positions[0].tokenAccount, otherPositionAcount, 1); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[0].rewardMint, + rewardTokenProgram: rewards[0].tokenProgram, + rewardOwnerAccount, + rewardVault: rewards[0].rewardVaultKeypair.publicKey, + rewardIndex: 0, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + + it("fails when position token account mint does not match position mint", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: tokenTraits.tokenTraitAB, + tokenTraitB: tokenTraits.tokenTraitAB, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: -1280, + tickUpperIndex: 1280, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), + vaultAmount: new BN(1_000_000), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda, tokenMintA }, + positions, + rewards, + } = fixture.getInfos(); + + // accrue rewards + await sleep(1200); + + const rewardOwnerAccount = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitR, + rewards[0].rewardMint, + provider.wallet.publicKey + ); + + const fakePositionTokenAccount = await createTokenAccountForPosition( + provider, + NATIVE_MINT, + provider.wallet.publicKey + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: fakePositionTokenAccount, + rewardMint: rewards[0].rewardMint, + rewardTokenProgram: rewards[0].tokenProgram, + rewardOwnerAccount, + rewardVault: rewards[0].rewardVaultKeypair.publicKey, + rewardIndex: 0, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + + it("fails when position authority is not approved delegate for position token account", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: tokenTraits.tokenTraitAB, + tokenTraitB: tokenTraits.tokenTraitAB, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: -1280, + tickUpperIndex: 1280, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), + vaultAmount: new BN(1_000_000), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + // accrue rewards + await sleep(1200); + + const rewardOwnerAccount = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitR, + rewards[0].rewardMint, + provider.wallet.publicKey + ); + const delegate = anchor.web3.Keypair.generate(); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[0].rewardMint, + rewardTokenProgram: rewards[0].tokenProgram, + rewardOwnerAccount, + rewardVault: rewards[0].rewardVaultKeypair.publicKey, + rewardIndex: 0, + }) + ) + .addSigner(delegate) + .buildAndExecute(), + /0x1783/ // MissingOrInvalidDelegate + ); + }); + + it("fails when position authority is not authorized for exactly one token", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: tokenTraits.tokenTraitAB, + tokenTraitB: tokenTraits.tokenTraitAB, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: -1280, + tickUpperIndex: 1280, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), + vaultAmount: new BN(1_000_000), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + // accrue rewards + await sleep(1200); + + const rewardOwnerAccount = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitR, + rewards[0].rewardMint, + provider.wallet.publicKey + ); + const delegate = anchor.web3.Keypair.generate(); + await approveToken(provider, positions[0].tokenAccount, delegate.publicKey, 2); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[0].rewardMint, + rewardTokenProgram: rewards[0].tokenProgram, + rewardOwnerAccount, + rewardVault: rewards[0].rewardVaultKeypair.publicKey, + rewardIndex: 0, + }) + ) + .addSigner(delegate) + .buildAndExecute(), + /0x1784/ // InvalidPositionTokenAmount + ); + }); + + it("fails when position authority was not a signer", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: tokenTraits.tokenTraitAB, + tokenTraitB: tokenTraits.tokenTraitAB, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: -1280, + tickUpperIndex: 1280, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), + vaultAmount: new BN(1_000_000), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + // accrue rewards + await sleep(1200); + + const rewardOwnerAccount = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitR, + rewards[0].rewardMint, + provider.wallet.publicKey + ); + const delegate = anchor.web3.Keypair.generate(); + await approveToken(provider, positions[0].tokenAccount, delegate.publicKey, 1); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[0].rewardMint, + rewardTokenProgram: rewards[0].tokenProgram, + rewardOwnerAccount, + rewardVault: rewards[0].rewardVaultKeypair.publicKey, + rewardIndex: 0, + }) + ).buildAndExecute(), + /.*signature verification fail.*/i + ); + }); + + it("fails when reward vault does not match whirlpool reward vault", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: tokenTraits.tokenTraitAB, + tokenTraitB: tokenTraits.tokenTraitAB, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: -1280, + tickUpperIndex: 1280, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), + vaultAmount: new BN(1_000_000), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + // accrue rewards + await sleep(1200); + + const rewardOwnerAccount = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitR, + rewards[0].rewardMint, + provider.wallet.publicKey + ); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[0].rewardMint, + rewardTokenProgram: rewards[0].tokenProgram, + rewardOwnerAccount, + rewardVault: rewardOwnerAccount, + rewardIndex: 0, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when reward owner account mint does not match whirlpool reward mint", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: tokenTraits.tokenTraitAB, + tokenTraitB: tokenTraits.tokenTraitAB, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: -1280, + tickUpperIndex: 1280, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), + vaultAmount: new BN(1_000_000), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda, tokenMintA }, + positions, + rewards, + } = fixture.getInfos(); + + // accrue rewards + await sleep(1200); + + const fakeMint = await createMintV2(provider, tokenTraits.tokenTraitR); + const rewardOwnerAccount = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitR, + fakeMint, + provider.wallet.publicKey + ); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[0].rewardMint, + rewardTokenProgram: rewards[0].tokenProgram, + rewardOwnerAccount, + rewardVault: rewards[0].rewardVaultKeypair.publicKey, + rewardIndex: 0, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + + it("fails when reward index is out of bounds", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: tokenTraits.tokenTraitAB, + tokenTraitB: tokenTraits.tokenTraitAB, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: -1280, + tickUpperIndex: 1280, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), + vaultAmount: new BN(1_000_000), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda, tokenMintA }, + positions, + rewards, + } = fixture.getInfos(); + + // accrue rewards + await sleep(1200); + + const rewardOwnerAccount = await createTokenAccountV2( + provider, + tokenTraits.tokenTraitR, + rewards[0].rewardMint, + provider.wallet.publicKey + ); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[0].rewardMint, + rewardTokenProgram: rewards[0].tokenProgram, + rewardOwnerAccount, + rewardVault: rewards[0].rewardVaultKeypair.publicKey, + rewardIndex: 4, + }) + ).buildAndExecute(), + /Program failed to complete/ // index out of bounds + ); + }); + }); + }); + }); + + describe("v2 specific accounts", () => { + it("fails when passed reward_mint does not match whirlpool's reward_infos", async () => { + const tokenTraits: TokenTrait[] = [ + { isToken2022: true }, + { isToken2022: false }, + { isToken2022: true }, + ]; + + const vaultStartBalance = 1_000_000; + const lowerTickIndex = -1280, + upperTickIndex = 1280, + tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: tickSpacing, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: lowerTickIndex, + tickUpperIndex: upperTickIndex, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: tokenTraits[0], + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: tokenTraits[1], + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: tokenTraits[2], + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + const otherTokenPublicKey = await createMintV2(provider, { isToken2022: true }); + + for (let i = 0; i < NUM_REWARDS; i++) { + const rewardOwnerAccount = await createTokenAccountV2( + provider, + tokenTraits[i], + rewards[i].rewardMint, + provider.wallet.publicKey + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: otherTokenPublicKey, // invalid + rewardTokenProgram: rewards[i].tokenProgram, + rewardOwnerAccount: rewardOwnerAccount, + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + rewardIndex: i, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + } + }); + + it("fails when passed token_program is not token program (token-2022 is passed)", async () => { + const vaultStartBalance = 1_000_000; + const lowerTickIndex = -1280, + upperTickIndex = 1280, + tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: tickSpacing, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: lowerTickIndex, + tickUpperIndex: upperTickIndex, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: { isToken2022: false }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: { isToken2022: false }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: { isToken2022: false }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + for (let i = 0; i < NUM_REWARDS; i++) { + const rewardOwnerAccount = await createTokenAccountV2( + provider, + { isToken2022: false }, + rewards[i].rewardMint, + provider.wallet.publicKey + ); + + assert.ok(rewards[i].tokenProgram.equals(TEST_TOKEN_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[i].rewardMint, + rewardTokenProgram: TEST_TOKEN_2022_PROGRAM_ID, // invalid + rewardOwnerAccount: rewardOwnerAccount, + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + rewardIndex: i, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + } + }); + + it("fails when passed token_program is not token-2022 program (token is passed)", async () => { + const vaultStartBalance = 1_000_000; + const lowerTickIndex = -1280, + upperTickIndex = 1280, + tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: tickSpacing, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: lowerTickIndex, + tickUpperIndex: upperTickIndex, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: { isToken2022: true }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: { isToken2022: true }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: { isToken2022: true }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + for (let i = 0; i < NUM_REWARDS; i++) { + const rewardOwnerAccount = await createTokenAccountV2( + provider, + { isToken2022: true }, + rewards[i].rewardMint, + provider.wallet.publicKey + ); + + assert.ok(rewards[i].tokenProgram.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[i].rewardMint, + rewardTokenProgram: TEST_TOKEN_PROGRAM_ID, // invalid + rewardOwnerAccount: rewardOwnerAccount, + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + rewardIndex: i, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + } + }); + + it("fails when passed token_program is token_metadata", async () => { + const vaultStartBalance = 1_000_000; + const lowerTickIndex = -1280, + upperTickIndex = 1280, + tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: tickSpacing, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: lowerTickIndex, + tickUpperIndex: upperTickIndex, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: { isToken2022: true }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: { isToken2022: true }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: { isToken2022: true }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + for (let i = 0; i < NUM_REWARDS; i++) { + const rewardOwnerAccount = await createTokenAccountV2( + provider, + { isToken2022: true }, + rewards[i].rewardMint, + provider.wallet.publicKey + ); + + assert.ok(rewards[i].tokenProgram.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[i].rewardMint, + rewardTokenProgram: METADATA_PROGRAM_ADDRESS, // invalid + rewardOwnerAccount: rewardOwnerAccount, + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + rewardIndex: i, + }) + ).buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + } + }); + + it("fails when passed memo_program is token_metadata", async () => {}); + }); +}); diff --git a/sdk/tests/integration/v2/decrease_liquidity_v2.test.ts b/sdk/tests/integration/v2/decrease_liquidity_v2.test.ts new file mode 100644 index 000000000..f27640db9 --- /dev/null +++ b/sdk/tests/integration/v2/decrease_liquidity_v2.test.ts @@ -0,0 +1,1547 @@ +import * as anchor from "@coral-xyz/anchor"; +import { MathUtil, Percentage } from "@orca-so/common-sdk"; +import * as assert from "assert"; +import { BN } from "bn.js"; +import Decimal from "decimal.js"; +import { + METADATA_PROGRAM_ADDRESS, + PositionData, + TickArrayData, + WhirlpoolContext, + WhirlpoolData, + WhirlpoolIx, + toTx, +} from "../../../src"; +import { IGNORE_CACHE } from "../../../src/network/public/fetcher"; +import { decreaseLiquidityQuoteByLiquidityWithParams } from "../../../src/quotes/public/decrease-liquidity-quote"; +import { + TEST_TOKEN_2022_PROGRAM_ID, + TEST_TOKEN_PROGRAM_ID, + TickSpacing, + ZERO_BN, + approveToken as approveTokenForPosition, + assertTick, + sleep, + transferToken, +} from "../../utils"; +import { defaultConfirmOptions } from "../../utils/const"; +import { WhirlpoolTestFixtureV2 } from "../../utils/v2/fixture-v2"; +import { initTickArray, openPosition } from "../../utils/init-utils"; +import { TokenTrait } from "../../utils/v2/init-utils-v2"; +import { + createMintV2, + createAndMintToTokenAccountV2, + approveTokenV2, +} from "../../utils/v2/token-2022"; +import { + createTokenAccount as createTokenAccountForPosition, + createAndMintToTokenAccount as createAndMintToTokenAccountForPosition, +} from "../../utils/token"; +import { TokenExtensionUtil } from "../../../src/utils/public/token-extension-util"; + +describe("decrease_liquidity_v2", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + + describe("v1 parity", () => { + const tokenTraitVariations: { tokenTraitA: TokenTrait; tokenTraitB: TokenTrait }[] = [ + { tokenTraitA: { isToken2022: false }, tokenTraitB: { isToken2022: false } }, + { tokenTraitA: { isToken2022: true }, tokenTraitB: { isToken2022: false } }, + { tokenTraitA: { isToken2022: false }, tokenTraitB: { isToken2022: true } }, + { tokenTraitA: { isToken2022: true }, tokenTraitB: { isToken2022: true } }, + ]; + tokenTraitVariations.forEach((tokenTraits) => { + describe(`tokenTraitA: ${ + tokenTraits.tokenTraitA.isToken2022 ? "Token2022" : "Token" + }, tokenTraitB: ${tokenTraits.tokenTraitB.isToken2022 ? "Token2022" : "Token"}`, () => { + it("successfully decrease liquidity from position in one tick array", async () => { + const liquidityAmount = new anchor.BN(1_250_000); + const tickLower = 7168, + tickUpper = 8960; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1.48)), + positions: [{ tickLowerIndex: tickLower, tickUpperIndex: tickUpper, liquidityAmount }], + }); + const { poolInitInfo, tokenAccountA, tokenAccountB, positions } = fixture.getInfos(); + const { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair } = poolInitInfo; + const poolBefore = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + + // To check if rewardLastUpdatedTimestamp is updated + await sleep(3000); + + const removalQuote = decreaseLiquidityQuoteByLiquidityWithParams({ + liquidity: new anchor.BN(1_000_000), + sqrtPrice: poolBefore.sqrtPrice, + slippageTolerance: Percentage.fromFraction(1, 100), + tickCurrentIndex: poolBefore.tickCurrentIndex, + tickLowerIndex: tickLower, + tickUpperIndex: tickUpper, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolBefore, IGNORE_CACHE), + }); + + await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + ...removalQuote, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArrayLower: positions[0].tickArrayLower, + tickArrayUpper: positions[0].tickArrayUpper, + }) + ).buildAndExecute(); + + const remainingLiquidity = liquidityAmount.sub(removalQuote.liquidityAmount); + const poolAfter = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + assert.ok(poolAfter.rewardLastUpdatedTimestamp.gt(poolBefore.rewardLastUpdatedTimestamp)); + assert.ok(poolAfter.liquidity.eq(remainingLiquidity)); + + const position = await fetcher.getPosition(positions[0].publicKey, IGNORE_CACHE); + assert.ok(position?.liquidity.eq(remainingLiquidity)); + + const tickArray = (await fetcher.getTickArray( + positions[0].tickArrayLower, + IGNORE_CACHE + )) as TickArrayData; + assertTick(tickArray.ticks[56], true, remainingLiquidity, remainingLiquidity); + assertTick(tickArray.ticks[70], true, remainingLiquidity, remainingLiquidity.neg()); + }); + + it("successfully decrease liquidity from position in two tick arrays", async () => { + const liquidityAmount = new anchor.BN(1_250_000); + const tickLower = -1280, + tickUpper = 1280; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair } = poolInitInfo; + const position = positions[0]; + const poolBefore = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const removalQuote = decreaseLiquidityQuoteByLiquidityWithParams({ + liquidity: new anchor.BN(1_000_000), + sqrtPrice: poolBefore.sqrtPrice, + slippageTolerance: Percentage.fromFraction(1, 100), + tickCurrentIndex: poolBefore.tickCurrentIndex, + tickLowerIndex: tickLower, + tickUpperIndex: tickUpper, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolBefore, IGNORE_CACHE), + }); + + await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + ...removalQuote, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ).buildAndExecute(); + + const remainingLiquidity = liquidityAmount.sub(removalQuote.liquidityAmount); + const poolAfter = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + + assert.ok( + poolAfter.rewardLastUpdatedTimestamp.gte(poolBefore.rewardLastUpdatedTimestamp) + ); + assert.ok(poolAfter.liquidity.eq(remainingLiquidity)); + + const positionAfter = (await fetcher.getPosition( + position.publicKey, + IGNORE_CACHE + )) as PositionData; + assert.ok(positionAfter.liquidity.eq(remainingLiquidity)); + + const tickArrayLower = (await fetcher.getTickArray( + position.tickArrayLower, + IGNORE_CACHE + )) as TickArrayData; + assertTick(tickArrayLower.ticks[78], true, remainingLiquidity, remainingLiquidity); + const tickArrayUpper = (await fetcher.getTickArray( + position.tickArrayUpper, + IGNORE_CACHE + )) as TickArrayData; + assertTick(tickArrayUpper.ticks[10], true, remainingLiquidity, remainingLiquidity.neg()); + }); + + it("successfully decrease liquidity with approved delegate", async () => { + const liquidityAmount = new anchor.BN(1_250_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const position = positions[0]; + + const delegate = anchor.web3.Keypair.generate(); + + await approveTokenForPosition(provider, positions[0].tokenAccount, delegate.publicKey, 1); + await approveTokenV2( + provider, + tokenTraits.tokenTraitA, + tokenAccountA, + delegate.publicKey, + 1_000_000 + ); + await approveTokenV2( + provider, + tokenTraits.tokenTraitB, + tokenAccountB, + delegate.publicKey, + 1_000_000 + ); + + const removeAmount = new anchor.BN(1_000_000); + + await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount: removeAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ) + .addSigner(delegate) + .buildAndExecute(); + }); + + it("successfully decrease liquidity with owner even if there is approved delegate", async () => { + const liquidityAmount = new anchor.BN(1_250_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1.48)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const position = positions[0]; + + const delegate = anchor.web3.Keypair.generate(); + + await approveTokenForPosition(provider, positions[0].tokenAccount, delegate.publicKey, 1); + await approveTokenV2( + provider, + tokenTraits.tokenTraitA, + tokenAccountA, + delegate.publicKey, + 1_000_000 + ); + await approveTokenV2( + provider, + tokenTraits.tokenTraitB, + tokenAccountB, + delegate.publicKey, + 1_000_000 + ); + + const removeAmount = new anchor.BN(1_000_000); + + await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount: removeAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ).buildAndExecute(); + }); + + it("successfully decrease liquidity with transferred position token", async () => { + const liquidityAmount = new anchor.BN(1_250_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1.48)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const position = positions[0]; + + const removeAmount = new anchor.BN(1_000_000); + const newOwner = anchor.web3.Keypair.generate(); + const newOwnerPositionTokenAccount = await createTokenAccountForPosition( + provider, + position.mintKeypair.publicKey, + newOwner.publicKey + ); + await transferToken(provider, position.tokenAccount, newOwnerPositionTokenAccount, 1); + + await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount: removeAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: newOwner.publicKey, + position: position.publicKey, + positionTokenAccount: newOwnerPositionTokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ) + .addSigner(newOwner) + .buildAndExecute(); + }); + + it("fails when liquidity amount is zero", async () => { + const liquidityAmount = new anchor.BN(1_250_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair } = poolInitInfo; + const position = positions[0]; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount: new anchor.BN(0), + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ).buildAndExecute(), + /0x177c/ // LiquidityZero + ); + }); + + it("fails when position has insufficient liquidity for the withdraw amount", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: ZERO_BN }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair } = poolInitInfo; + const position = positions[0]; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount: new anchor.BN(1_000), + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ).buildAndExecute(), + /0x177f/ // LiquidityUnderflow + ); + }); + + it("fails when token min a subceeded", async () => { + const liquidityAmount = new anchor.BN(1_250_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(0.005)), + positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair } = poolInitInfo; + const position = positions[0]; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(1_000_000), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ).buildAndExecute(), + /0x1782/ // TokenMinSubceeded + ); + }); + + it("fails when token min b subceeded", async () => { + const liquidityAmount = new anchor.BN(1_250_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(5)), + positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair } = poolInitInfo; + const position = positions[0]; + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(1_000_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ).buildAndExecute(), + /0x1782/ // TokenMinSubceeded + ); + }); + + it("fails when position account does not have exactly 1 token", async () => { + const liquidityAmount = new anchor.BN(1_250_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const position = positions[0]; + + // Create a position token account that contains 0 tokens + const newPositionTokenAccount = await createTokenAccountForPosition( + provider, + positions[0].mintKeypair.publicKey, + provider.wallet.publicKey + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: newPositionTokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + + // Send position token to other position token account + await transferToken(provider, position.tokenAccount, newPositionTokenAccount, 1); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + + it("fails when position token account mint does not match position mint", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda, tokenMintA } = poolInitInfo; + const position = positions[0]; + + const fakeMint = await createMintV2(provider, { isToken2022: false }); + const invalidPositionTokenAccount = await createAndMintToTokenAccountForPosition( + provider, + fakeMint, + 1 + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: invalidPositionTokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7d3/ // A raw constraint was violated + ); + }); + + it("fails when position does not match whirlpool", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const tickArray = positions[0].tickArrayLower; + + const anotherFixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + }); + + const { + params: { positionPda, positionTokenAccount: positionTokenAccountAddress }, + } = await openPosition( + ctx, + anotherFixture.getInfos().poolInitInfo.whirlpoolPda.publicKey, + 7168, + 8960 + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionPda.publicKey, + positionTokenAccount: positionTokenAccountAddress, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArray, + tickArrayUpper: tickArray, + }) + ).buildAndExecute(), + /0x7d1/ // A has_one constraint was violated + ); + }); + + it("fails when token vaults do not match whirlpool vaults", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda, tokenMintA, tokenMintB } = poolInitInfo; + const position = positions[0]; + + const fakeVaultA = await createAndMintToTokenAccountV2( + provider, + tokenTraits.tokenTraitA, + tokenMintA, + 1_000 + ); + const fakeVaultB = await createAndMintToTokenAccountV2( + provider, + tokenTraits.tokenTraitB, + tokenMintB, + 1_000 + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: fakeVaultA, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: fakeVaultB, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + + it("fails when owner token account mint does not match whirlpool token mint", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + + const invalidMintA = await createMintV2(provider, tokenTraits.tokenTraitA); + const invalidTokenAccountA = await createAndMintToTokenAccountV2( + provider, + tokenTraits.tokenTraitA, + invalidMintA, + 1_000_000 + ); + const invalidMintB = await createMintV2(provider, tokenTraits.tokenTraitB); + const invalidTokenAccountB = await createAndMintToTokenAccountV2( + provider, + tokenTraits.tokenTraitB, + invalidMintB, + 1_000_000 + ); + + const position = positions[0]; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: invalidTokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: invalidTokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + + it("fails when position authority is not approved delegate for position token account", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const position = positions[0]; + const delegate = anchor.web3.Keypair.generate(); + + await approveTokenV2( + provider, + tokenTraits.tokenTraitA, + tokenAccountA, + delegate.publicKey, + 1_000_000 + ); + await approveTokenV2( + provider, + tokenTraits.tokenTraitB, + tokenAccountB, + delegate.publicKey, + 1_000_000 + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ) + .addSigner(delegate) + .buildAndExecute(), + /0x1783/ // MissingOrInvalidDelegate + ); + }); + + it("fails when position authority is not authorized for exactly 1 token", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const position = positions[0]; + const delegate = anchor.web3.Keypair.generate(); + + await approveTokenForPosition(provider, position.tokenAccount, delegate.publicKey, 0); + await approveTokenV2( + provider, + tokenTraits.tokenTraitA, + tokenAccountA, + delegate.publicKey, + 1_000_000 + ); + await approveTokenV2( + provider, + tokenTraits.tokenTraitB, + tokenAccountB, + delegate.publicKey, + 1_000_000 + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ) + .addSigner(delegate) + .buildAndExecute(), + /0x1784/ // InvalidPositionTokenAmount + ); + }); + + it("fails when position authority was not a signer", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const position = positions[0]; + const delegate = anchor.web3.Keypair.generate(); + + await approveTokenForPosition(provider, position.tokenAccount, delegate.publicKey, 1); + await approveTokenV2( + provider, + tokenTraits.tokenTraitA, + tokenAccountA, + delegate.publicKey, + 1_000_000 + ); + await approveTokenV2( + provider, + tokenTraits.tokenTraitB, + tokenAccountB, + delegate.publicKey, + 1_000_000 + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(167_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }) + ).buildAndExecute(), + /.*signature verification fail.*/i + ); + }); + + it("fails when tick arrays do not match the position", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const position = positions[0]; + + const { + params: { tickArrayPda: tickArrayLowerPda }, + } = await initTickArray(ctx, whirlpoolPda.publicKey, 11264); + + const { + params: { tickArrayPda: tickArrayUpperPda }, + } = await initTickArray(ctx, whirlpoolPda.publicKey, 22528); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArrayLowerPda.publicKey, + tickArrayUpper: tickArrayUpperPda.publicKey, + }) + ).buildAndExecute(), + /0x1779/ // TicKNotFound + ); + }); + + it("fails when the tick arrays are for a different whirlpool", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const position = positions[0]; + + const anotherFixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + }); + const poolInitInfo2 = anotherFixture.getInfos().poolInitInfo; + + const { + params: { tickArrayPda: tickArrayLowerPda }, + } = await initTickArray(ctx, poolInitInfo2.whirlpoolPda.publicKey, -11264); + + const { + params: { tickArrayPda: tickArrayUpperPda }, + } = await initTickArray(ctx, poolInitInfo2.whirlpoolPda.publicKey, 0); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArrayLowerPda.publicKey, + tickArrayUpper: tickArrayUpperPda.publicKey, + }) + ).buildAndExecute(), + /0x7d1/ // A has one constraint was violated + ); + }); + }); + }); + }); + + describe("v2 specific accounts", () => { + it("fails when passed token_mint_a does not match whirlpool's token_mint_a", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const tickArray = positions[0].tickArrayLower; + + const { + params: { positionPda, positionTokenAccount: positionTokenAccountAddress }, + } = await openPosition(ctx, poolInitInfo.whirlpoolPda.publicKey, 7168, 8960); + + const otherTokenPublicKey = await createMintV2(provider, { isToken2022: true }); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionPda.publicKey, + positionTokenAccount: positionTokenAccountAddress, + tokenMintA: otherTokenPublicKey, // invalid + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArray, + tickArrayUpper: tickArray, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_mint_b does not match whirlpool's token_mint_b", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const tickArray = positions[0].tickArrayLower; + + const { + params: { positionPda, positionTokenAccount: positionTokenAccountAddress }, + } = await openPosition(ctx, poolInitInfo.whirlpoolPda.publicKey, 7168, 8960); + + const otherTokenPublicKey = await createMintV2(provider, { isToken2022: true }); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionPda.publicKey, + positionTokenAccount: positionTokenAccountAddress, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: otherTokenPublicKey, // invalid + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArray, + tickArrayUpper: tickArray, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_a is not token program (token-2022 is passed)", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const tickArray = positions[0].tickArrayLower; + + const { + params: { positionPda, positionTokenAccount: positionTokenAccountAddress }, + } = await openPosition(ctx, poolInitInfo.whirlpoolPda.publicKey, 7168, 8960); + + assert.ok(poolInitInfo.tokenProgramA.equals(TEST_TOKEN_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionPda.publicKey, + positionTokenAccount: positionTokenAccountAddress, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: TEST_TOKEN_2022_PROGRAM_ID, // invalid + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArray, + tickArrayUpper: tickArray, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_a is not token-2022 program (token is passed)", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const tickArray = positions[0].tickArrayLower; + + const { + params: { positionPda, positionTokenAccount: positionTokenAccountAddress }, + } = await openPosition(ctx, poolInitInfo.whirlpoolPda.publicKey, 7168, 8960); + + assert.ok(poolInitInfo.tokenProgramA.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionPda.publicKey, + positionTokenAccount: positionTokenAccountAddress, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: TEST_TOKEN_PROGRAM_ID, // invalid + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArray, + tickArrayUpper: tickArray, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_a is token_metadata", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const tickArray = positions[0].tickArrayLower; + + const { + params: { positionPda, positionTokenAccount: positionTokenAccountAddress }, + } = await openPosition(ctx, poolInitInfo.whirlpoolPda.publicKey, 7168, 8960); + + assert.ok(poolInitInfo.tokenProgramA.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionPda.publicKey, + positionTokenAccount: positionTokenAccountAddress, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: METADATA_PROGRAM_ADDRESS, // invalid + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArray, + tickArrayUpper: tickArray, + }) + ).buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("fails when passed token_program_b is not token program (token-2022 is passed)", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const tickArray = positions[0].tickArrayLower; + + const { + params: { positionPda, positionTokenAccount: positionTokenAccountAddress }, + } = await openPosition(ctx, poolInitInfo.whirlpoolPda.publicKey, 7168, 8960); + + assert.ok(poolInitInfo.tokenProgramB.equals(TEST_TOKEN_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionPda.publicKey, + positionTokenAccount: positionTokenAccountAddress, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: TEST_TOKEN_2022_PROGRAM_ID, // invalid + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArray, + tickArrayUpper: tickArray, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_b is not token-2022 program (token is passed)", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const tickArray = positions[0].tickArrayLower; + + const { + params: { positionPda, positionTokenAccount: positionTokenAccountAddress }, + } = await openPosition(ctx, poolInitInfo.whirlpoolPda.publicKey, 7168, 8960); + + assert.ok(poolInitInfo.tokenProgramB.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionPda.publicKey, + positionTokenAccount: positionTokenAccountAddress, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: TEST_TOKEN_PROGRAM_ID, // invalid + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArray, + tickArrayUpper: tickArray, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_b is token_metadata", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const tickArray = positions[0].tickArrayLower; + + const { + params: { positionPda, positionTokenAccount: positionTokenAccountAddress }, + } = await openPosition(ctx, poolInitInfo.whirlpoolPda.publicKey, 7168, 8960); + + assert.ok(poolInitInfo.tokenProgramB.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMinA: new BN(0), + tokenMinB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionPda.publicKey, + positionTokenAccount: positionTokenAccountAddress, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: METADATA_PROGRAM_ADDRESS, // invalid + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArray, + tickArrayUpper: tickArray, + }) + ).buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("fails when passed memo_program is token_metadata", async () => { + const liquidityAmount = new anchor.BN(6_500_000); + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const tickArray = positions[0].tickArrayLower; + + const { + params: { positionPda, positionTokenAccount: positionTokenAccountAddress }, + } = await openPosition(ctx, poolInitInfo.whirlpoolPda.publicKey, 7168, 8960); + + const invalidMemoProgram = METADATA_PROGRAM_ADDRESS; + + await assert.rejects( + toTx(ctx, { + cleanupInstructions: [], + signers: [], + instructions: [ + ctx.program.instruction.decreaseLiquidityV2( + liquidityAmount, + new BN(0), // minA + new BN(0), // minB + { slices: [] }, + { + accounts: { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionPda.publicKey, + positionTokenAccount: positionTokenAccountAddress, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArray, + tickArrayUpper: tickArray, + memoProgram: invalidMemoProgram, + }, + } + ), + ], + }).buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + }); +}); diff --git a/sdk/tests/integration/v2/increase_liquidity_v2.test.ts b/sdk/tests/integration/v2/increase_liquidity_v2.test.ts new file mode 100644 index 000000000..5ae6ee025 --- /dev/null +++ b/sdk/tests/integration/v2/increase_liquidity_v2.test.ts @@ -0,0 +1,1870 @@ +import * as anchor from "@coral-xyz/anchor"; +import { MathUtil, TransactionBuilder } from "@orca-so/common-sdk"; +import * as assert from "assert"; +import { BN } from "bn.js"; +import Decimal from "decimal.js"; +import { + METADATA_PROGRAM_ADDRESS, + PDAUtil, + PositionData, + PriceMath, + TickArrayData, + TickUtil, + WhirlpoolContext, + WhirlpoolData, + WhirlpoolIx, + toTx, +} from "../../../src"; +import { IGNORE_CACHE } from "../../../src/network/public/fetcher"; +import { PoolUtil, toTokenAmount } from "../../../src/utils/public/pool-utils"; +import { + MAX_U64, + TEST_TOKEN_2022_PROGRAM_ID, + TEST_TOKEN_PROGRAM_ID, + TickSpacing, + ZERO_BN, + approveToken as approveTokenForPosition, + assertTick, + getTokenBalance, + sleep, + transferToken, +} from "../../utils"; +import { defaultConfirmOptions } from "../../utils/const"; +import { WhirlpoolTestFixtureV2 } from "../../utils/v2/fixture-v2"; +import { initTickArray, openPosition } from "../../utils/init-utils"; +import { TokenTrait } from "../../utils/v2/init-utils-v2"; +import { + createMintV2, + createAndMintToTokenAccountV2, + approveTokenV2, +} from "../../utils/v2/token-2022"; +import { + createTokenAccount as createTokenAccountForPosition, + createAndMintToTokenAccount as createAndMintToTokenAccountForPosition, +} from "../../utils/token"; +import { + generateDefaultInitTickArrayParams, + generateDefaultOpenPositionParams, +} from "../../utils/test-builders"; + +describe("increase_liquidity_v2", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + + describe("v1 parity", () => { + const tokenTraitVariations: { tokenTraitA: TokenTrait; tokenTraitB: TokenTrait }[] = [ + { tokenTraitA: { isToken2022: false }, tokenTraitB: { isToken2022: false } }, + { tokenTraitA: { isToken2022: true }, tokenTraitB: { isToken2022: false } }, + { tokenTraitA: { isToken2022: false }, tokenTraitB: { isToken2022: true } }, + { tokenTraitA: { isToken2022: true }, tokenTraitB: { isToken2022: true } }, + ]; + tokenTraitVariations.forEach((tokenTraits) => { + describe(`tokenTraitA: ${ + tokenTraits.tokenTraitA.isToken2022 ? "Token2022" : "Token" + }, tokenTraitB: ${tokenTraits.tokenTraitB.isToken2022 ? "Token2022" : "Token"}`, () => { + it("increase liquidity of a position spanning two tick arrays", async () => { + const currTick = 0; + const tickLowerIndex = -1280, + tickUpperIndex = 1280; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const poolBefore = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + const tokenAmount = toTokenAmount(167_000, 167_000); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + // To check if rewardLastUpdatedTimestamp is updated + await sleep(3000); + + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(); + + const position = (await fetcher.getPosition( + positionInitInfo.publicKey, + IGNORE_CACHE + )) as PositionData; + assert.ok(position.liquidity.eq(liquidityAmount)); + + const poolAfter = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + assert.ok(poolAfter.rewardLastUpdatedTimestamp.gt(poolBefore.rewardLastUpdatedTimestamp)); + assert.equal( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + tokenAmount.tokenA.toString() + ); + assert.equal( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + tokenAmount.tokenB.toString() + ); + assert.ok(poolAfter.liquidity.eq(new anchor.BN(liquidityAmount))); + + const tickArrayLower = (await fetcher.getTickArray( + positionInitInfo.tickArrayLower, + IGNORE_CACHE + )) as TickArrayData; + assertTick(tickArrayLower.ticks[78], true, liquidityAmount, liquidityAmount); + const tickArrayUpper = (await fetcher.getTickArray( + positionInitInfo.tickArrayUpper, + IGNORE_CACHE + )) as TickArrayData; + assertTick(tickArrayUpper.ticks[10], true, liquidityAmount, liquidityAmount.neg()); + }); + + it("increase liquidity of a position contained in one tick array", async () => { + const currTick = 500; + const tickLowerIndex = 7168; + const tickUpperIndex = 8960; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + const poolBefore = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const tokenAmount = toTokenAmount(1_000_000, 0); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(); + + assert.equal( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + tokenAmount.tokenA.toString() + ); + + assert.equal( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + tokenAmount.tokenB.toString() + ); + + const expectedLiquidity = new anchor.BN(liquidityAmount); + const position = (await fetcher.getPosition( + positionInitInfo.publicKey, + IGNORE_CACHE + )) as PositionData; + assert.ok(position.liquidity.eq(expectedLiquidity)); + + const tickArray = (await fetcher.getTickArray( + positionInitInfo.tickArrayLower, + IGNORE_CACHE + )) as TickArrayData; + + assertTick(tickArray.ticks[56], true, expectedLiquidity, expectedLiquidity); + assertTick(tickArray.ticks[70], true, expectedLiquidity, expectedLiquidity.neg()); + + const poolAfter = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + assert.ok( + poolAfter.rewardLastUpdatedTimestamp.gte(poolBefore.rewardLastUpdatedTimestamp) + ); + assert.equal(poolAfter.liquidity, 0); + }); + + it("initialize and increase liquidity of a position in a single transaction", async () => { + const currTick = 500; + const tickLowerIndex = 7168; + const tickUpperIndex = 8960; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + }); + const { poolInitInfo, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda, tickSpacing } = poolInitInfo; + const poolBefore = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const tokenAmount = toTokenAmount(1_000_000, 0); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + const { params, mint } = await generateDefaultOpenPositionParams( + ctx, + whirlpoolPda.publicKey, + tickLowerIndex, + tickUpperIndex, + ctx.wallet.publicKey + ); + + const tickArrayLower = PDAUtil.getTickArray( + ctx.program.programId, + whirlpoolPda.publicKey, + TickUtil.getStartTickIndex(tickLowerIndex, tickSpacing) + ).publicKey; + + const tickArrayUpper = PDAUtil.getTickArray( + ctx.program.programId, + whirlpoolPda.publicKey, + TickUtil.getStartTickIndex(tickUpperIndex, tickSpacing) + ).publicKey; + + await new TransactionBuilder( + ctx.provider.connection, + ctx.provider.wallet, + ctx.txBuilderOpts + ) + // TODO: create a ComputeBudgetInstruction to request more compute + .addInstruction( + WhirlpoolIx.initTickArrayIx( + ctx.program, + generateDefaultInitTickArrayParams( + ctx, + whirlpoolPda.publicKey, + TickUtil.getStartTickIndex(tickLowerIndex, tickSpacing) + ) + ) + ) + // .addInstruction( + // buildtoTx(ctx, WhirlpoolIx.initTickArrayIx(generateDefaultInitTickArrayParams( + // ctx, + // whirlpoolPda.publicKey, + // getStartTickIndex(pos[0].tickLowerIndex + TICK_ARRAY_SIZE * tickSpacing, tickSpacing), + // )) + // ) + .addInstruction(WhirlpoolIx.openPositionIx(ctx.program, params)) + // .addInstruction( + // buildWhirlpoolIx.openPositionWithMetadataIx(ctx.program, params) + // ) + .addSigner(mint) + .addInstruction( + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: params.positionPda.publicKey, + positionTokenAccount: params.positionTokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArrayLower, + tickArrayUpper: tickArrayUpper, + }) + ) + .buildAndExecute(); + + assert.equal( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + tokenAmount.tokenA.toString() + ); + + assert.equal( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + tokenAmount.tokenB.toString() + ); + + const expectedLiquidity = new anchor.BN(liquidityAmount); + const position = (await fetcher.getPosition( + params.positionPda.publicKey, + IGNORE_CACHE + )) as PositionData; + assert.ok(position.liquidity.eq(expectedLiquidity)); + + const tickArray = (await fetcher.getTickArray( + tickArrayLower, + IGNORE_CACHE + )) as TickArrayData; + + assertTick(tickArray.ticks[56], true, expectedLiquidity, expectedLiquidity); + assertTick(tickArray.ticks[70], true, expectedLiquidity, expectedLiquidity.neg()); + + const poolAfter = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + assert.ok( + poolAfter.rewardLastUpdatedTimestamp.gte(poolBefore.rewardLastUpdatedTimestamp) + ); + assert.equal(poolAfter.liquidity, 0); + }); + + it("increase liquidity of a position with an approved position authority delegate", async () => { + const currTick = 1300; + const tickLowerIndex = -1280, + tickUpperIndex = 1280; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const poolBefore = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + const tokenAmount = toTokenAmount(0, 167_000); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + const delegate = anchor.web3.Keypair.generate(); + await approveTokenForPosition( + provider, + positionInitInfo.tokenAccount, + delegate.publicKey, + 1 + ); + await approveTokenV2( + provider, + tokenTraits.tokenTraitA, + tokenAccountA, + delegate.publicKey, + 1_000_000 + ); + await approveTokenV2( + provider, + tokenTraits.tokenTraitB, + tokenAccountB, + delegate.publicKey, + 1_000_000 + ); + + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ) + .addSigner(delegate) + .buildAndExecute(); + + const position = (await fetcher.getPosition( + positionInitInfo.publicKey, + IGNORE_CACHE + )) as PositionData; + assert.ok(position.liquidity.eq(liquidityAmount)); + + const poolAfter = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + assert.ok( + poolAfter.rewardLastUpdatedTimestamp.gte(poolBefore.rewardLastUpdatedTimestamp) + ); + assert.equal( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + tokenAmount.tokenA.toString() + ); + assert.equal( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + tokenAmount.tokenB.toString() + ); + assert.equal(poolAfter.liquidity, 0); + + const tickArrayLower = (await fetcher.getTickArray( + positionInitInfo.tickArrayLower, + IGNORE_CACHE + )) as TickArrayData; + assertTick(tickArrayLower.ticks[78], true, liquidityAmount, liquidityAmount); + const tickArrayUpper = (await fetcher.getTickArray( + positionInitInfo.tickArrayUpper, + IGNORE_CACHE + )) as TickArrayData; + assertTick(tickArrayUpper.ticks[10], true, liquidityAmount, liquidityAmount.neg()); + }); + + it("add maximum amount of liquidity near minimum price", async () => { + const currTick = -443621; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Stable, + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + mintAmount: MAX_U64, + }); + const { poolInitInfo, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + + const { + params: { tickArrayPda }, + } = await initTickArray(ctx, whirlpoolPda.publicKey, -444224); + + const tickLowerIndex = -443632; + const tickUpperIndex = -443624; + const positionInfo = await openPosition( + ctx, + whirlpoolPda.publicKey, + tickLowerIndex, + tickUpperIndex + ); + const { positionPda, positionTokenAccount: positionTokenAccountAddress } = + positionInfo.params; + + const tokenAmount = { + tokenA: new BN(0), + tokenB: MAX_U64, + }; + const estLiquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount: estLiquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionPda.publicKey, + positionTokenAccount: positionTokenAccountAddress, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArrayPda.publicKey, + tickArrayUpper: tickArrayPda.publicKey, + }) + ).buildAndExecute(); + + const position = (await fetcher.getPosition( + positionPda.publicKey, + IGNORE_CACHE + )) as PositionData; + assert.ok(position.liquidity.eq(estLiquidityAmount)); + }); + + it("add maximum amount of liquidity near maximum price", async () => { + const currTick = 443635; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Stable, + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + mintAmount: MAX_U64, + }); + const { poolInitInfo, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + + const { + params: { tickArrayPda }, + } = await initTickArray(ctx, whirlpoolPda.publicKey, 436480); + + const tickLowerIndex = 436488; + const tickUpperIndex = 436496; + const positionInfo = await openPosition( + ctx, + whirlpoolPda.publicKey, + tickLowerIndex, + tickUpperIndex + ); + const { positionPda, positionTokenAccount: positionTokenAccountAddress } = + positionInfo.params; + + const tokenAmount = { + tokenA: new BN(0), + tokenB: MAX_U64, + }; + const estLiquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount: estLiquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionPda.publicKey, + positionTokenAccount: positionTokenAccountAddress, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArrayPda.publicKey, + tickArrayUpper: tickArrayPda.publicKey, + }) + ).buildAndExecute(); + + const position = (await fetcher.getPosition( + positionPda.publicKey, + IGNORE_CACHE + )) as PositionData; + assert.ok(position.liquidity.eq(estLiquidityAmount)); + }); + + it("fails with zero liquidity amount", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount: ZERO_BN }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount: ZERO_BN, + tokenMaxA: new BN(0), + tokenMaxB: new BN(1_000_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0x177c/ // LiquidityZero + ); + }); + + it("fails when token max a exceeded", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount: ZERO_BN }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const liquidityAmount = new anchor.BN(6_500_000); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: new BN(0), + tokenMaxB: new BN(999_999_999), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0x1781/ // TokenMaxExceeded + ); + }); + + it("fails when token max b exceeded", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount: ZERO_BN }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const liquidityAmount = new anchor.BN(6_500_000); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: new BN(999_999_999), + tokenMaxB: new BN(0), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0x1781/ // TokenMaxExceeded + ); + }); + + it("fails when position account does not have exactly 1 token", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount: ZERO_BN }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + // Create a position token account that contains 0 tokens + const newPositionTokenAccount = await createTokenAccountForPosition( + provider, + positionInitInfo.mintKeypair.publicKey, + provider.wallet.publicKey + ); + + const liquidityAmount = new anchor.BN(6_500_000); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: new BN(0), + tokenMaxB: new BN(1_000_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: newPositionTokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + + // Send position token to other position token account + await transferToken(provider, positionInitInfo.tokenAccount, newPositionTokenAccount, 1); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: new BN(0), + tokenMaxB: new BN(1_000_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + + it("fails when position token account mint does not match position mint", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount: ZERO_BN }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda, tokenMintA } = poolInitInfo; + const positionInitInfo = positions[0]; + + // Create a position token account that contains 0 tokens + const fakeMint = await createMintV2(provider, { isToken2022: false }); + const invalidPositionTokenAccount = await createAndMintToTokenAccountForPosition( + provider, + fakeMint, + 1 + ); + + const liquidityAmount = new anchor.BN(6_500_000); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: new BN(0), + tokenMaxB: new BN(1_000_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: invalidPositionTokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7d3/ // A raw constraint was violated + ); + }); + + it("fails when position does not match whirlpool", async () => { + const tickLowerIndex = 7168; + const tickUpperIndex = 8960; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }], + }); + const { poolInitInfo, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + + const anotherFixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + }); + const poolInitInfo2 = anotherFixture.getInfos().poolInitInfo; + + const positionInitInfo = await openPosition( + ctx, + poolInitInfo2.whirlpoolPda.publicKey, + tickLowerIndex, + tickUpperIndex + ); + const { positionPda, positionTokenAccount: positionTokenAccountAddress } = + positionInitInfo.params; + + const { + params: { tickArrayPda }, + } = await initTickArray(ctx, poolInitInfo2.whirlpoolPda.publicKey, 0); + + const liquidityAmount = new anchor.BN(6_500_000); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: new BN(0), + tokenMaxB: new BN(1_000_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionPda.publicKey, + positionTokenAccount: positionTokenAccountAddress, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArrayPda.publicKey, + tickArrayUpper: tickArrayPda.publicKey, + }) + ).buildAndExecute(), + /0x7d1/ // A has_one constraint was violated + ); + }); + + it("fails when token vaults do not match whirlpool vaults", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount: ZERO_BN }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda, tokenMintA, tokenMintB } = poolInitInfo; + const positionInitInfo = positions[0]; + const liquidityAmount = new anchor.BN(6_500_000); + + const fakeVaultA = await createAndMintToTokenAccountV2( + provider, + tokenTraits.tokenTraitA, + tokenMintA, + 1_000 + ); + const fakeVaultB = await createAndMintToTokenAccountV2( + provider, + tokenTraits.tokenTraitB, + tokenMintB, + 1_000 + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: new BN(0), + tokenMaxB: new BN(1_000_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: fakeVaultA, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: new BN(0), + tokenMaxB: new BN(1_000_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: fakeVaultB, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + + it("fails when owner token account mint does not match whirlpool token mint", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount: ZERO_BN }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + const liquidityAmount = new anchor.BN(6_500_000); + + const invalidMintA = await createMintV2(provider, tokenTraits.tokenTraitA); + const invalidTokenAccountA = await createAndMintToTokenAccountV2( + provider, + tokenTraits.tokenTraitA, + invalidMintA, + 1_000_000 + ); + const invalidMintB = await createMintV2(provider, tokenTraits.tokenTraitB); + const invalidTokenAccountB = await createAndMintToTokenAccountV2( + provider, + tokenTraits.tokenTraitB, + invalidMintB, + 1_000_000 + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: new BN(0), + tokenMaxB: new BN(1_000_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: invalidTokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: new BN(0), + tokenMaxB: new BN(1_000_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: invalidTokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + + it("fails when position authority is not approved delegate for position token account", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: ZERO_BN }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const delegate = anchor.web3.Keypair.generate(); + + const liquidityAmount = new anchor.BN(1_250_000); + + await approveTokenV2( + provider, + tokenTraits.tokenTraitA, + tokenAccountA, + delegate.publicKey, + 1_000_000 + ); + await approveTokenV2( + provider, + tokenTraits.tokenTraitB, + tokenAccountB, + delegate.publicKey, + 1_000_000 + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: new BN(0), + tokenMaxB: new BN(167_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ) + .addSigner(delegate) + .buildAndExecute(), + /0x1783/ // MissingOrInvalidDelegate + ); + }); + + it("fails when position authority is not authorized for exactly 1 token", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: ZERO_BN }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const delegate = anchor.web3.Keypair.generate(); + + const liquidityAmount = new anchor.BN(1_250_000); + + await approveTokenForPosition( + provider, + positionInitInfo.tokenAccount, + delegate.publicKey, + 0 + ); + await approveTokenV2( + provider, + tokenTraits.tokenTraitA, + tokenAccountA, + delegate.publicKey, + 1_000_000 + ); + await approveTokenV2( + provider, + tokenTraits.tokenTraitB, + tokenAccountB, + delegate.publicKey, + 1_000_000 + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: new BN(0), + tokenMaxB: new BN(167_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ) + .addSigner(delegate) + .buildAndExecute(), + /0x1784/ // InvalidPositionTokenAmount + ); + }); + + it("fails when position authority was not a signer", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: ZERO_BN }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const delegate = anchor.web3.Keypair.generate(); + + const liquidityAmount = new anchor.BN(1_250_000); + + await approveTokenForPosition( + provider, + positionInitInfo.tokenAccount, + delegate.publicKey, + 1 + ); + await approveTokenV2( + provider, + tokenTraits.tokenTraitA, + tokenAccountA, + delegate.publicKey, + 1_000_000 + ); + await approveTokenV2( + provider, + tokenTraits.tokenTraitB, + tokenAccountB, + delegate.publicKey, + 1_000_000 + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: new BN(0), + tokenMaxB: new BN(167_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /.*signature verification fail.*/i + ); + }); + + it("fails when position authority is not approved for token owner accounts", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: ZERO_BN }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const delegate = anchor.web3.Keypair.generate(); + + const liquidityAmount = new anchor.BN(1_250_000); + + await approveTokenForPosition( + provider, + positionInitInfo.tokenAccount, + delegate.publicKey, + 1 + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: new BN(0), + tokenMaxB: new BN(167_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: delegate.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ) + .addSigner(delegate) + .buildAndExecute(), + /0x4/ // owner does not match + ); + }); + + it("fails when tick arrays do not match the position", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: ZERO_BN }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const { + params: { tickArrayPda: tickArrayLowerPda }, + } = await initTickArray(ctx, whirlpoolPda.publicKey, 11264); + + const { + params: { tickArrayPda: tickArrayUpperPda }, + } = await initTickArray(ctx, whirlpoolPda.publicKey, 22528); + + const liquidityAmount = new anchor.BN(1_250_000); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: new BN(0), + tokenMaxB: new BN(167_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArrayLowerPda.publicKey, + tickArrayUpper: tickArrayUpperPda.publicKey, + }) + ).buildAndExecute(), + /0x1779/ // TicKNotFound + ); + }); + + it("fails when the tick arrays are for a different whirlpool", async () => { + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: ZERO_BN }], + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const anotherFixture = await new WhirlpoolTestFixtureV2(ctx).init({ + ...tokenTraits, + tickSpacing: TickSpacing.Standard, + }); + const poolInitInfo2 = anotherFixture.getInfos().poolInitInfo; + + const { + params: { tickArrayPda: tickArrayLowerPda }, + } = await initTickArray(ctx, poolInitInfo2.whirlpoolPda.publicKey, -11264); + + const { + params: { tickArrayPda: tickArrayUpperPda }, + } = await initTickArray(ctx, poolInitInfo2.whirlpoolPda.publicKey, 0); + + const liquidityAmount = new anchor.BN(1_250_000); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: new BN(0), + tokenMaxB: new BN(167_000), + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: tickArrayLowerPda.publicKey, + tickArrayUpper: tickArrayUpperPda.publicKey, + }) + ).buildAndExecute(), + /0x7d1/ // A has one constraint was violated + ); + }); + }); + }); + }); + + describe("v2 specific accounts", () => { + it("fails when passed token_mint_a does not match whirlpool's token_mint_a", async () => { + const currTick = 500; + const tickLowerIndex = 7168; + const tickUpperIndex = 8960; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 0); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + const otherTokenPublicKey = await createMintV2(provider, { isToken2022: true }); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: otherTokenPublicKey, // invalid + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_mint_b does not match whirlpool's token_mint_b", async () => { + const currTick = 500; + const tickLowerIndex = 7168; + const tickUpperIndex = 8960; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 0); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + const otherTokenPublicKey = await createMintV2(provider, { isToken2022: true }); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: otherTokenPublicKey, // invalid + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_a is not token program (token-2022 is passed)", async () => { + const currTick = 500; + const tickLowerIndex = 7168; + const tickUpperIndex = 8960; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 0); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + assert.ok(poolInitInfo.tokenProgramA.equals(TEST_TOKEN_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: TEST_TOKEN_2022_PROGRAM_ID, // invalid + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_a is not token-2022 program (token is passed)", async () => { + const currTick = 500; + const tickLowerIndex = 7168; + const tickUpperIndex = 8960; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 0); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + assert.ok(poolInitInfo.tokenProgramA.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: TEST_TOKEN_PROGRAM_ID, // invalid + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_a is token_metadata", async () => { + const currTick = 500; + const tickLowerIndex = 7168; + const tickUpperIndex = 8960; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 0); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + assert.ok(poolInitInfo.tokenProgramA.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: METADATA_PROGRAM_ADDRESS, // invalid + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("fails when passed token_program_b is not token program (token-2022 is passed)", async () => { + const currTick = 500; + const tickLowerIndex = 7168; + const tickUpperIndex = 8960; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 0); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + assert.ok(poolInitInfo.tokenProgramB.equals(TEST_TOKEN_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: TEST_TOKEN_2022_PROGRAM_ID, // invalid + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_b is not token-2022 program (token is passed)", async () => { + const currTick = 500; + const tickLowerIndex = 7168; + const tickUpperIndex = 8960; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 0); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + assert.ok(poolInitInfo.tokenProgramB.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: TEST_TOKEN_PROGRAM_ID, // invalid + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_b is token_metadata", async () => { + const currTick = 500; + const tickLowerIndex = 7168; + const tickUpperIndex = 8960; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 0); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + assert.ok(poolInitInfo.tokenProgramB.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: METADATA_PROGRAM_ADDRESS, // invalid + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("fails when passed memo_program is token_metadata", async () => { + const currTick = 500; + const tickLowerIndex = 7168; + const tickUpperIndex = 8960; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + }); + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 0); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + const invalidMemoProgram = METADATA_PROGRAM_ADDRESS; + + await assert.rejects( + toTx(ctx, { + cleanupInstructions: [], + signers: [], + instructions: [ + ctx.program.instruction.increaseLiquidityV2( + liquidityAmount, + tokenAmount.tokenA, // maxA + tokenAmount.tokenB, // maxB + { slices: [] }, + { + accounts: { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + memoProgram: invalidMemoProgram, + } + } + ), + ] + }).buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + }); +}); diff --git a/sdk/tests/integration/v2/initialize_pool_v2.test.ts b/sdk/tests/integration/v2/initialize_pool_v2.test.ts new file mode 100644 index 000000000..9b9314090 --- /dev/null +++ b/sdk/tests/integration/v2/initialize_pool_v2.test.ts @@ -0,0 +1,1247 @@ +import * as anchor from "@coral-xyz/anchor"; +import { MathUtil, PDA } from "@orca-so/common-sdk"; +import * as assert from "assert"; +import Decimal from "decimal.js"; +import { + IGNORE_CACHE, + InitPoolV2Params, + MAX_SQRT_PRICE, + METADATA_PROGRAM_ADDRESS, + MIN_SQRT_PRICE, + PDAUtil, + PoolUtil, + PriceMath, + WhirlpoolContext, + WhirlpoolData, + WhirlpoolIx, + toTx, +} from "../../../src"; +import { + ONE_SOL, + TEST_TOKEN_2022_PROGRAM_ID, + TEST_TOKEN_PROGRAM_ID, + TickSpacing, + ZERO_BN, + systemTransferTx, +} from "../../utils"; +import { defaultConfirmOptions } from "../../utils/const"; +import { TokenTrait, buildTestPoolV2Params, initTestPoolV2 } from "../../utils/v2/init-utils-v2"; +import { + asyncAssertOwnerProgram, + asyncAssertTokenVaultV2, + createMintV2, + initializeNativeMint2022Idempotent, +} from "../../utils/v2/token-2022"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import { AccountState, NATIVE_MINT, NATIVE_MINT_2022 } from "@solana/spl-token"; +import { initFeeTier } from "../../utils/init-utils"; + +describe("initialize_pool_v2", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + + describe("v1 parity", () => { + const tokenTraitVariations: { tokenTraitA: TokenTrait; tokenTraitB: TokenTrait }[] = [ + { tokenTraitA: { isToken2022: false }, tokenTraitB: { isToken2022: false } }, + { tokenTraitA: { isToken2022: true }, tokenTraitB: { isToken2022: false } }, + { tokenTraitA: { isToken2022: false }, tokenTraitB: { isToken2022: true } }, + { tokenTraitA: { isToken2022: true }, tokenTraitB: { isToken2022: true } }, + ]; + tokenTraitVariations.forEach((tokenTraits) => { + describe(`tokenTraitA: ${ + tokenTraits.tokenTraitA.isToken2022 ? "Token2022" : "Token" + }, tokenTraitB: ${tokenTraits.tokenTraitB.isToken2022 ? "Token2022" : "Token"}`, () => { + it("successfully init a Standard account", async () => { + const price = MathUtil.toX64(new Decimal(5)); + const { configInitInfo, poolInitInfo, feeTierParams } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard, + price + ); + const whirlpool = (await fetcher.getPool( + poolInitInfo.whirlpoolPda.publicKey + )) as WhirlpoolData; + + const expectedWhirlpoolPda = PDAUtil.getWhirlpool( + program.programId, + configInitInfo.whirlpoolsConfigKeypair.publicKey, + poolInitInfo.tokenMintA, + poolInitInfo.tokenMintB, + TickSpacing.Standard + ); + + assert.ok(poolInitInfo.whirlpoolPda.publicKey.equals(expectedWhirlpoolPda.publicKey)); + assert.equal(expectedWhirlpoolPda.bump, whirlpool.whirlpoolBump[0]); + + assert.ok(whirlpool.whirlpoolsConfig.equals(poolInitInfo.whirlpoolsConfig)); + + assert.ok(whirlpool.tokenMintA.equals(poolInitInfo.tokenMintA)); + assert.ok(whirlpool.tokenVaultA.equals(poolInitInfo.tokenVaultAKeypair.publicKey)); + await asyncAssertOwnerProgram( + provider, + whirlpool.tokenMintA, + tokenTraits.tokenTraitA.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID + ); + + assert.ok(whirlpool.tokenMintB.equals(poolInitInfo.tokenMintB)); + assert.ok(whirlpool.tokenVaultB.equals(poolInitInfo.tokenVaultBKeypair.publicKey)); + await asyncAssertOwnerProgram( + provider, + whirlpool.tokenMintB, + tokenTraits.tokenTraitB.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID + ); + + assert.equal(whirlpool.feeRate, feeTierParams.defaultFeeRate); + assert.equal(whirlpool.protocolFeeRate, configInitInfo.defaultProtocolFeeRate); + + assert.ok(whirlpool.sqrtPrice.eq(new anchor.BN(poolInitInfo.initSqrtPrice.toString()))); + assert.ok(whirlpool.liquidity.eq(ZERO_BN)); + + assert.equal( + whirlpool.tickCurrentIndex, + PriceMath.sqrtPriceX64ToTickIndex(poolInitInfo.initSqrtPrice) + ); + + assert.ok(whirlpool.protocolFeeOwedA.eq(ZERO_BN)); + assert.ok(whirlpool.protocolFeeOwedB.eq(ZERO_BN)); + assert.ok(whirlpool.feeGrowthGlobalA.eq(ZERO_BN)); + assert.ok(whirlpool.feeGrowthGlobalB.eq(ZERO_BN)); + + assert.ok(whirlpool.tickSpacing === TickSpacing.Standard); + + await asyncAssertTokenVaultV2( + provider, + poolInitInfo.tokenVaultAKeypair.publicKey, + poolInitInfo.tokenMintA, + poolInitInfo.whirlpoolPda.publicKey, + tokenTraits.tokenTraitA.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID + ); + await asyncAssertTokenVaultV2( + provider, + poolInitInfo.tokenVaultBKeypair.publicKey, + poolInitInfo.tokenMintB, + poolInitInfo.whirlpoolPda.publicKey, + tokenTraits.tokenTraitB.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID + ); + + whirlpool.rewardInfos.forEach((rewardInfo) => { + assert.equal(rewardInfo.emissionsPerSecondX64, 0); + assert.equal(rewardInfo.growthGlobalX64, 0); + assert.ok(rewardInfo.authority.equals(configInitInfo.rewardEmissionsSuperAuthority)); + assert.ok(rewardInfo.mint.equals(anchor.web3.PublicKey.default)); + assert.ok(rewardInfo.vault.equals(anchor.web3.PublicKey.default)); + }); + }); + + it("successfully init a Stable account", async () => { + const price = MathUtil.toX64(new Decimal(5)); + const { configInitInfo, poolInitInfo, feeTierParams } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Stable, + price + ); + const whirlpool = (await fetcher.getPool( + poolInitInfo.whirlpoolPda.publicKey + )) as WhirlpoolData; + + assert.ok(whirlpool.whirlpoolsConfig.equals(poolInitInfo.whirlpoolsConfig)); + + assert.ok(whirlpool.tokenMintA.equals(poolInitInfo.tokenMintA)); + assert.ok(whirlpool.tokenVaultA.equals(poolInitInfo.tokenVaultAKeypair.publicKey)); + await asyncAssertOwnerProgram( + provider, + whirlpool.tokenMintA, + tokenTraits.tokenTraitA.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID + ); + + assert.ok(whirlpool.tokenMintB.equals(poolInitInfo.tokenMintB)); + assert.ok(whirlpool.tokenVaultB.equals(poolInitInfo.tokenVaultBKeypair.publicKey)); + await asyncAssertOwnerProgram( + provider, + whirlpool.tokenMintB, + tokenTraits.tokenTraitB.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID + ); + + assert.equal(whirlpool.feeRate, feeTierParams.defaultFeeRate); + assert.equal(whirlpool.protocolFeeRate, configInitInfo.defaultProtocolFeeRate); + + assert.ok(whirlpool.sqrtPrice.eq(new anchor.BN(poolInitInfo.initSqrtPrice.toString()))); + assert.ok(whirlpool.liquidity.eq(ZERO_BN)); + + assert.equal( + whirlpool.tickCurrentIndex, + PriceMath.sqrtPriceX64ToTickIndex(poolInitInfo.initSqrtPrice) + ); + + assert.ok(whirlpool.protocolFeeOwedA.eq(ZERO_BN)); + assert.ok(whirlpool.protocolFeeOwedB.eq(ZERO_BN)); + assert.ok(whirlpool.feeGrowthGlobalA.eq(ZERO_BN)); + assert.ok(whirlpool.feeGrowthGlobalB.eq(ZERO_BN)); + + assert.ok(whirlpool.tickSpacing === TickSpacing.Stable); + + await asyncAssertTokenVaultV2( + provider, + poolInitInfo.tokenVaultAKeypair.publicKey, + poolInitInfo.tokenMintA, + poolInitInfo.whirlpoolPda.publicKey, + tokenTraits.tokenTraitA.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID + ); + await asyncAssertTokenVaultV2( + provider, + poolInitInfo.tokenVaultBKeypair.publicKey, + poolInitInfo.tokenMintB, + poolInitInfo.whirlpoolPda.publicKey, + tokenTraits.tokenTraitB.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID + ); + + whirlpool.rewardInfos.forEach((rewardInfo) => { + assert.equal(rewardInfo.emissionsPerSecondX64, 0); + assert.equal(rewardInfo.growthGlobalX64, 0); + assert.ok(rewardInfo.authority.equals(configInitInfo.rewardEmissionsSuperAuthority)); + assert.ok(rewardInfo.mint.equals(anchor.web3.PublicKey.default)); + assert.ok(rewardInfo.vault.equals(anchor.web3.PublicKey.default)); + }); + }); + + it("succeeds when funder is different than account paying for transaction fee", async () => { + const funderKeypair = anchor.web3.Keypair.generate(); + await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute(); + await initTestPoolV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard, + MathUtil.toX64(new Decimal(5)), + funderKeypair + ); + }); + + it("fails when tokenVaultA mint does not match tokenA mint", async () => { + const { poolInitInfo } = await buildTestPoolV2Params( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + const otherTokenPublicKey = await createMintV2(provider, tokenTraits.tokenTraitA); + + const modifiedPoolInitInfo: InitPoolV2Params = { + ...poolInitInfo, + tokenMintA: otherTokenPublicKey, + }; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializePoolV2Ix(ctx.program, modifiedPoolInitInfo) + ).buildAndExecute(), + /custom program error: 0x7d6/ // ConstraintSeeds + ); + }); + + it("fails when tokenVaultB mint does not match tokenB mint", async () => { + const { poolInitInfo } = await buildTestPoolV2Params( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + const otherTokenPublicKey = await createMintV2(provider, tokenTraits.tokenTraitB); + + const modifiedPoolInitInfo: InitPoolV2Params = { + ...poolInitInfo, + tokenMintB: otherTokenPublicKey, + }; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializePoolV2Ix(ctx.program, modifiedPoolInitInfo) + ).buildAndExecute(), + /custom program error: 0x7d6/ // ConstraintSeeds + ); + }); + + it("fails when token mints are in the wrong order", async () => { + const { poolInitInfo, configInitInfo } = await buildTestPoolV2Params( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const whirlpoolPda = PDAUtil.getWhirlpool( + ctx.program.programId, + configInitInfo.whirlpoolsConfigKeypair.publicKey, + poolInitInfo.tokenMintB, + poolInitInfo.tokenMintA, + TickSpacing.Standard + ); + + const modifiedPoolInitInfo: InitPoolV2Params = { + ...poolInitInfo, + whirlpoolPda, + tickSpacing: TickSpacing.Standard, + tokenMintA: poolInitInfo.tokenMintB, + tokenBadgeA: poolInitInfo.tokenBadgeB, + tokenProgramA: tokenTraits.tokenTraitB.isToken2022 + ? TEST_TOKEN_2022_PROGRAM_ID + : TEST_TOKEN_PROGRAM_ID, + tokenMintB: poolInitInfo.tokenMintA, + tokenBadgeB: poolInitInfo.tokenBadgeA, + tokenProgramB: tokenTraits.tokenTraitA.isToken2022 + ? TEST_TOKEN_2022_PROGRAM_ID + : TEST_TOKEN_PROGRAM_ID, + }; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializePoolV2Ix(ctx.program, modifiedPoolInitInfo) + ).buildAndExecute(), + /custom program error: 0x1788/ // InvalidTokenMintOrder + ); + }); + + it("fails when the same token mint is passed in", async () => { + const { poolInitInfo, configInitInfo } = await buildTestPoolV2Params( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const whirlpoolPda = PDAUtil.getWhirlpool( + ctx.program.programId, + configInitInfo.whirlpoolsConfigKeypair.publicKey, + poolInitInfo.tokenMintA, + poolInitInfo.tokenMintA, + TickSpacing.Standard + ); + + const modifiedPoolInitInfo: InitPoolV2Params = { + ...poolInitInfo, + whirlpoolPda, + tickSpacing: TickSpacing.Standard, + tokenMintB: poolInitInfo.tokenMintA, + tokenBadgeB: poolInitInfo.tokenBadgeA, + tokenProgramB: tokenTraits.tokenTraitA.isToken2022 + ? TEST_TOKEN_2022_PROGRAM_ID + : TEST_TOKEN_PROGRAM_ID, + }; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializePoolV2Ix(ctx.program, modifiedPoolInitInfo) + ).buildAndExecute(), + /custom program error: 0x1788/ // InvalidTokenMintOrder + ); + }); + + it("fails when sqrt-price exceeds max", async () => { + const { poolInitInfo } = await buildTestPoolV2Params( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const modifiedPoolInitInfo: InitPoolV2Params = { + ...poolInitInfo, + initSqrtPrice: new anchor.BN(MAX_SQRT_PRICE).add(new anchor.BN(1)), + }; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializePoolV2Ix(ctx.program, modifiedPoolInitInfo) + ).buildAndExecute(), + /custom program error: 0x177b/ // SqrtPriceOutOfBounds + ); + }); + + it("fails when sqrt-price subceeds min", async () => { + const { poolInitInfo } = await buildTestPoolV2Params( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const modifiedPoolInitInfo: InitPoolV2Params = { + ...poolInitInfo, + initSqrtPrice: new anchor.BN(MIN_SQRT_PRICE).sub(new anchor.BN(1)), + }; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializePoolV2Ix(ctx.program, modifiedPoolInitInfo) + ).buildAndExecute(), + /custom program error: 0x177b/ // SqrtPriceOutOfBounds + ); + }); + + it("ignore passed bump", async () => { + const { poolInitInfo } = await buildTestPoolV2Params( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const whirlpoolPda = poolInitInfo.whirlpoolPda; + const validBump = whirlpoolPda.bump; + const invalidBump = (validBump + 1) % 256; // +1 shift mod 256 + const modifiedWhirlpoolPda: PDA = { + publicKey: whirlpoolPda.publicKey, + bump: invalidBump, + }; + + const modifiedPoolInitInfo: InitPoolV2Params = { + ...poolInitInfo, + whirlpoolPda: modifiedWhirlpoolPda, + }; + + await toTx( + ctx, + WhirlpoolIx.initializePoolV2Ix(ctx.program, modifiedPoolInitInfo) + ).buildAndExecute(); + + // check if passed invalid bump was ignored + const whirlpool = (await fetcher.getPool( + poolInitInfo.whirlpoolPda.publicKey + )) as WhirlpoolData; + assert.equal(whirlpool.whirlpoolBump, validBump); + assert.notEqual(whirlpool.whirlpoolBump, invalidBump); + }); + }); + }); + }); + + it("fails when FeeTier and tick_spacing passed unmatch", async () => { + const { poolInitInfo, configInitInfo, configKeypairs } = await buildTestPoolV2Params( + ctx, + {isToken2022: true}, + {isToken2022: true}, + TickSpacing.Standard + ); + + // now FeeTier for TickSpacing.Standard is initialized, but not for TickSpacing.Stable + const config = poolInitInfo.whirlpoolsConfig; + const feeTierStandardPda = PDAUtil.getFeeTier(ctx.program.programId, config, TickSpacing.Standard) + const feeTierStablePda = PDAUtil.getFeeTier(ctx.program.programId, config, TickSpacing.Stable); + + const feeTierStandard = await fetcher.getFeeTier(feeTierStandardPda.publicKey, IGNORE_CACHE); + const feeTierStable = await fetcher.getFeeTier(feeTierStablePda.publicKey, IGNORE_CACHE); + assert.ok(feeTierStandard !== null); // should be initialized + assert.ok(feeTierStable === null); // shoud be NOT initialized + + const whirlpoolWithStableTickSpacing = PDAUtil.getWhirlpool( + ctx.program.programId, + config, + poolInitInfo.tokenMintA, + poolInitInfo.tokenMintB, + TickSpacing.Stable, + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializePoolV2Ix(ctx.program, { + ...poolInitInfo, + whirlpoolPda: whirlpoolWithStableTickSpacing, + tickSpacing: TickSpacing.Stable, + feeTierKey: feeTierStandardPda.publicKey, // tickSpacing is Stable, but FeeTier is standard + }) + ).buildAndExecute(), + /custom program error: 0x7d3/ // ConstraintRaw + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializePoolV2Ix(ctx.program, { + ...poolInitInfo, + whirlpoolPda: whirlpoolWithStableTickSpacing, + tickSpacing: TickSpacing.Stable, + feeTierKey: feeTierStablePda.publicKey, // FeeTier is stable, but not initialized + }) + ).buildAndExecute(), + /custom program error: 0xbc4/ // AccountNotInitialized + ); + + await initFeeTier(ctx, configInitInfo, configKeypairs.feeAuthorityKeypair, TickSpacing.Stable, 3000); + const feeTierStableAfterInit = await fetcher.getFeeTier(feeTierStablePda.publicKey, IGNORE_CACHE); + assert.ok(feeTierStableAfterInit !== null); + + // Now it should work because FeeTier for stable have been initialized + await toTx( + ctx, + WhirlpoolIx.initializePoolV2Ix(ctx.program, { + ...poolInitInfo, + whirlpoolPda: whirlpoolWithStableTickSpacing, + tickSpacing: TickSpacing.Stable, + feeTierKey: feeTierStablePda.publicKey, + }) + ).buildAndExecute(); +}) + + describe("v2 specific accounts", () => { + it("fails when passed token_program_a is not token program (token-2022 is passed)", async () => { + const { poolInitInfo } = await buildTestPoolV2Params( + ctx, + {isToken2022: false}, + {isToken2022: false}, + TickSpacing.Standard + ); + + assert.ok(poolInitInfo.tokenProgramA.equals(TEST_TOKEN_PROGRAM_ID)); + const modifiedPoolInitInfo: InitPoolV2Params = { + ...poolInitInfo, + tokenProgramA: TEST_TOKEN_2022_PROGRAM_ID, + }; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializePoolV2Ix(ctx.program, modifiedPoolInitInfo) + ).buildAndExecute(), + /incorrect program id for instruction/ // Anchor will try to create vault account + ); + }); + + it("fails when passed token_program_a is not token-2022 program (token is passed)", async () => { + const { poolInitInfo } = await buildTestPoolV2Params( + ctx, + {isToken2022: true}, + {isToken2022: true}, + TickSpacing.Standard + ); + + assert.ok(poolInitInfo.tokenProgramA.equals(TEST_TOKEN_2022_PROGRAM_ID)); + const modifiedPoolInitInfo: InitPoolV2Params = { + ...poolInitInfo, + tokenProgramA: TEST_TOKEN_PROGRAM_ID, + }; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializePoolV2Ix(ctx.program, modifiedPoolInitInfo) + ).buildAndExecute(), + /incorrect program id for instruction/ // Anchor will try to create vault account + ); + }); + + it("fails when passed token_program_a is token_metadata", async () => { + const { poolInitInfo } = await buildTestPoolV2Params( + ctx, + {isToken2022: true}, + {isToken2022: true}, + TickSpacing.Standard + ); + + assert.ok(poolInitInfo.tokenProgramA.equals(TEST_TOKEN_2022_PROGRAM_ID)); + const modifiedPoolInitInfo: InitPoolV2Params = { + ...poolInitInfo, + tokenProgramA: METADATA_PROGRAM_ADDRESS, + }; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializePoolV2Ix(ctx.program, modifiedPoolInitInfo) + ).buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("fails when passed token_program_b is not token program (token-2022 is passed)", async () => { + const { poolInitInfo } = await buildTestPoolV2Params( + ctx, + {isToken2022: false}, + {isToken2022: false}, + TickSpacing.Standard + ); + + assert.ok(poolInitInfo.tokenProgramB.equals(TEST_TOKEN_PROGRAM_ID)); + const modifiedPoolInitInfo: InitPoolV2Params = { + ...poolInitInfo, + tokenProgramB: TEST_TOKEN_2022_PROGRAM_ID, + }; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializePoolV2Ix(ctx.program, modifiedPoolInitInfo) + ).buildAndExecute(), + /incorrect program id for instruction/ // Anchor will try to create vault account + ); + }); + + it("fails when passed token_program_b is not token-2022 program (token is passed)", async () => { + const { poolInitInfo } = await buildTestPoolV2Params( + ctx, + {isToken2022: true}, + {isToken2022: true}, + TickSpacing.Standard + ); + + assert.ok(poolInitInfo.tokenProgramB.equals(TEST_TOKEN_2022_PROGRAM_ID)); + const modifiedPoolInitInfo: InitPoolV2Params = { + ...poolInitInfo, + tokenProgramB: TEST_TOKEN_PROGRAM_ID, + }; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializePoolV2Ix(ctx.program, modifiedPoolInitInfo) + ).buildAndExecute(), + /incorrect program id for instruction/ // Anchor will try to create vault account + ); + }); + + it("fails when passed token_program_b is token_metadata", async () => { + const { poolInitInfo } = await buildTestPoolV2Params( + ctx, + {isToken2022: true}, + {isToken2022: true}, + TickSpacing.Standard + ); + + assert.ok(poolInitInfo.tokenProgramB.equals(TEST_TOKEN_2022_PROGRAM_ID)); + const modifiedPoolInitInfo: InitPoolV2Params = { + ...poolInitInfo, + tokenProgramB: METADATA_PROGRAM_ADDRESS, + }; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializePoolV2Ix(ctx.program, modifiedPoolInitInfo) + ).buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + describe("invalid badge account", () => { + let baseIxParams: InitPoolV2Params; + + beforeEach(async () => { + // create tokens + const [tokenAKeypair, tokenBKeypair] = [Keypair.generate(), Keypair.generate()].sort((a, b) => PoolUtil.compareMints(a.publicKey, b.publicKey)); + await createMintV2(provider, {isToken2022: true, hasPermanentDelegate: true}, undefined, tokenAKeypair); + await createMintV2(provider, {isToken2022: true, hasPermanentDelegate: true}, undefined, tokenBKeypair); + + // create config and feetier + const configKeypair = Keypair.generate(); + await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, { + collectProtocolFeesAuthority: provider.wallet.publicKey, + feeAuthority: provider.wallet.publicKey, + rewardEmissionsSuperAuthority: provider.wallet.publicKey, + defaultProtocolFeeRate: 300, + funder: provider.wallet.publicKey, + whirlpoolsConfigKeypair: configKeypair, + })).addSigner(configKeypair).buildAndExecute(); + + const tickSpacing = TickSpacing.SixtyFour; + const feeTierPda = PDAUtil.getFeeTier(ctx.program.programId, configKeypair.publicKey, tickSpacing); + await toTx(ctx, WhirlpoolIx.initializeFeeTierIx(ctx.program, { + defaultFeeRate: 3000, + feeAuthority: provider.wallet.publicKey, + funder: provider.wallet.publicKey, + tickSpacing, + whirlpoolsConfig: configKeypair.publicKey, + feeTierPda: feeTierPda, + })).buildAndExecute(); + + // create config extension + const configExtensionPda = PDAUtil.getConfigExtension(ctx.program.programId, configKeypair.publicKey); + await toTx(ctx, WhirlpoolIx.initializeConfigExtensionIx(ctx.program, { + feeAuthority: provider.wallet.publicKey, + funder: provider.wallet.publicKey, + whirlpoolsConfig: configKeypair.publicKey, + whirlpoolsConfigExtensionPda: configExtensionPda, + })).buildAndExecute(); + + const whirlpoolPda = PDAUtil.getWhirlpool(ctx.program.programId, configKeypair.publicKey, tokenAKeypair.publicKey, tokenBKeypair.publicKey, tickSpacing); + baseIxParams = { + tokenVaultAKeypair: Keypair.generate(), + tokenVaultBKeypair: Keypair.generate(), + funder: provider.wallet.publicKey, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(0), + tickSpacing, + tokenMintA: tokenAKeypair.publicKey, + tokenMintB: tokenBKeypair.publicKey, + whirlpoolsConfig: configKeypair.publicKey, + feeTierKey: feeTierPda.publicKey, + tokenBadgeA: PDAUtil.getTokenBadge(ctx.program.programId, configKeypair.publicKey, tokenAKeypair.publicKey).publicKey, + tokenBadgeB: PDAUtil.getTokenBadge(ctx.program.programId, configKeypair.publicKey, tokenBKeypair.publicKey).publicKey, + tokenProgramA: TEST_TOKEN_2022_PROGRAM_ID, + tokenProgramB: TEST_TOKEN_2022_PROGRAM_ID, + whirlpoolPda, + } + }); + + it("fails when token_badge_a/b address invalid (uninitialized)", async () => { + const fakeAddress = Keypair.generate().publicKey; + await assert.rejects( + toTx(ctx, WhirlpoolIx.initializePoolV2Ix(ctx.program, { + ...baseIxParams, + tokenBadgeA: fakeAddress, + })).buildAndExecute(), + /custom program error: 0x7d6/ // ConstraintSeeds + ); + + await assert.rejects( + toTx(ctx, WhirlpoolIx.initializePoolV2Ix(ctx.program, { + ...baseIxParams, + tokenBadgeB: fakeAddress, + })).buildAndExecute(), + /custom program error: 0x7d6/ // ConstraintSeeds + ); + }); + + it("fails when token_badge_a/b address invalid (initialized, same config / different mint)", async () => { + const config = baseIxParams.whirlpoolsConfig; + + const anotherTokenKeypair = Keypair.generate(); + await createMintV2(provider, {isToken2022: true}, undefined, anotherTokenKeypair); + + // initialize another badge + const configExtension = PDAUtil.getConfigExtension(ctx.program.programId, config).publicKey; + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, config, anotherTokenKeypair.publicKey); + await toTx(ctx, WhirlpoolIx.initializeTokenBadgeIx(ctx.program, { + whirlpoolsConfig: config, + whirlpoolsConfigExtension: configExtension, + funder: provider.wallet.publicKey, + tokenBadgeAuthority: provider.wallet.publicKey, + tokenBadgePda, + tokenMint: anotherTokenKeypair.publicKey, + })).buildAndExecute(); + const badge = fetcher.getTokenBadge(tokenBadgePda.publicKey, IGNORE_CACHE); + assert.ok(badge !== null); + + const fakeAddress = tokenBadgePda.publicKey; + + await assert.rejects( + toTx(ctx, WhirlpoolIx.initializePoolV2Ix(ctx.program, { + ...baseIxParams, + tokenBadgeA: fakeAddress, + })).buildAndExecute(), + /custom program error: 0x7d6/ // ConstraintSeeds + ); + + await assert.rejects( + toTx(ctx, WhirlpoolIx.initializePoolV2Ix(ctx.program, { + ...baseIxParams, + tokenBadgeB: fakeAddress, + })).buildAndExecute(), + /custom program error: 0x7d6/ // ConstraintSeeds + ); + }); + + it("fails when token_badge_a/b address invalid (account owned by WhirlpoolProgram)", async () => { + // use Whirlpool address + const { poolInitInfo } = await initTestPoolV2( + ctx, + {isToken2022: true}, + {isToken2022: true}, + TickSpacing.Standard + ); + + const fakeAddress = poolInitInfo.whirlpoolPda.publicKey; + const whirlpool = fetcher.getPool(fakeAddress); + assert.ok(whirlpool !== null); + + await assert.rejects( + toTx(ctx, WhirlpoolIx.initializePoolV2Ix(ctx.program, { + ...baseIxParams, + tokenBadgeA: fakeAddress, + })).buildAndExecute(), + /custom program error: 0x7d6/ // ConstraintSeeds + ); + + await assert.rejects( + toTx(ctx, WhirlpoolIx.initializePoolV2Ix(ctx.program, { + ...baseIxParams, + tokenBadgeB: fakeAddress, + })).buildAndExecute(), + /custom program error: 0x7d6/ // ConstraintSeeds + ); + }); + }); + }); + + describe("Supported Tokens", () => { + function generate3MintAddress(): [Keypair, Keypair, Keypair] { + const keypairs = [Keypair.generate(), Keypair.generate(), Keypair.generate()].sort((a, b) => PoolUtil.compareMints(a.publicKey, b.publicKey)); + return [keypairs[0], keypairs[1], keypairs[2]]; + } + + async function checkSupported(supported: boolean, whirlpoolsConfig: PublicKey, tokenMintA: PublicKey, tokenMintB: PublicKey, tickSpacing: number, anchorPatch: boolean = false) { + const tokenVaultAKeypair = Keypair.generate(); + const tokenVaultBKeypair = Keypair.generate(); + + const whirlpoolPda = PDAUtil.getWhirlpool(ctx.program.programId, whirlpoolsConfig, tokenMintA, tokenMintB, tickSpacing); + const feeTierKey = PDAUtil.getFeeTier(ctx.program.programId, whirlpoolsConfig, tickSpacing).publicKey; + const tokenBadgeA = PDAUtil.getTokenBadge(ctx.program.programId, whirlpoolsConfig, tokenMintA).publicKey; + const tokenBadgeB = PDAUtil.getTokenBadge(ctx.program.programId, whirlpoolsConfig, tokenMintB).publicKey; + + const tokenProgramA = (await provider.connection.getAccountInfo(tokenMintA))!.owner; + const tokenProgramB = (await provider.connection.getAccountInfo(tokenMintB))!.owner; + + const promise = toTx(ctx, WhirlpoolIx.initializePoolV2Ix(ctx.program, { + tokenVaultAKeypair, + tokenVaultBKeypair, + funder: provider.wallet.publicKey, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(0), + tickSpacing, + tokenMintA, + tokenMintB, + whirlpoolsConfig, + feeTierKey, + tokenBadgeA, + tokenBadgeB, + tokenProgramA, + tokenProgramB, + whirlpoolPda, + })).buildAndExecute(); + + if (supported) { + await promise; + const whirlpoolData = await fetcher.getPool(whirlpoolPda.publicKey, IGNORE_CACHE); + assert.ok(whirlpoolData!.tokenMintA.equals(tokenMintA)); + assert.ok(whirlpoolData!.tokenMintB.equals(tokenMintB)); + } else { + await assert.rejects( + promise, + !anchorPatch + ? /0x179f/ // UnsupportedTokenMint + : /invalid account data for instruction/ // Anchor v0.29 doesn't recognize some new extensions (GroupPointer, Group, MemberPointer, Member) + ); + } + } + + async function runTest(params: { + supported: boolean, + createTokenBadge: boolean, + tokenTrait: TokenTrait, + anchorPatch?: boolean, + }) { + // create tokens + const [tokenA, tokenTarget, tokenB] = generate3MintAddress(); + await createMintV2(provider, {isToken2022: false}, undefined, tokenA); + await createMintV2(provider, {isToken2022: false}, undefined, tokenB); + await createMintV2(provider, params.tokenTrait, undefined, tokenTarget); + + // create config and feetier + const configKeypair = Keypair.generate(); + await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, { + collectProtocolFeesAuthority: provider.wallet.publicKey, + feeAuthority: provider.wallet.publicKey, + rewardEmissionsSuperAuthority: provider.wallet.publicKey, + defaultProtocolFeeRate: 300, + funder: provider.wallet.publicKey, + whirlpoolsConfigKeypair: configKeypair, + })).addSigner(configKeypair).buildAndExecute(); + + const tickSpacing = 64; + await toTx(ctx, WhirlpoolIx.initializeFeeTierIx(ctx.program, { + defaultFeeRate: 3000, + feeAuthority: provider.wallet.publicKey, + funder: provider.wallet.publicKey, + tickSpacing, + whirlpoolsConfig: configKeypair.publicKey, + feeTierPda: PDAUtil.getFeeTier(ctx.program.programId, configKeypair.publicKey, tickSpacing), + })).buildAndExecute(); + + // create token badge if wanted + if (params.createTokenBadge) { + const pda = PDAUtil.getConfigExtension(ctx.program.programId, configKeypair.publicKey); + await toTx(ctx, WhirlpoolIx.initializeConfigExtensionIx(ctx.program, { + feeAuthority: provider.wallet.publicKey, + funder: provider.wallet.publicKey, + whirlpoolsConfig: configKeypair.publicKey, + whirlpoolsConfigExtensionPda: pda, + })).buildAndExecute(); + + const configExtension = PDAUtil.getConfigExtension(ctx.program.programId, configKeypair.publicKey).publicKey; + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, configKeypair.publicKey, tokenTarget.publicKey); + await toTx(ctx, WhirlpoolIx.initializeTokenBadgeIx(ctx.program, { + whirlpoolsConfig: configKeypair.publicKey, + whirlpoolsConfigExtension: configExtension, + funder: provider.wallet.publicKey, + tokenBadgeAuthority: provider.wallet.publicKey, + tokenBadgePda, + tokenMint: tokenTarget.publicKey, + })).buildAndExecute(); + } + + // try to initialize pool + await checkSupported(params.supported, configKeypair.publicKey, tokenA.publicKey, tokenTarget.publicKey, tickSpacing, params.anchorPatch); // as TokenB + await checkSupported(params.supported, configKeypair.publicKey, tokenTarget.publicKey, tokenB.publicKey, tickSpacing, params.anchorPatch); // as TokenA + } + + async function runTestWithNativeMint(params: { + supported: boolean, + createTokenBadge: boolean, + isToken2022NativeMint: boolean, + anchorPatch?: boolean, + }) { + // We need to call this to use NATIVE_MINT_2022 + await initializeNativeMint2022Idempotent(provider); + + // create tokens + const nativeMint = params.isToken2022NativeMint ? NATIVE_MINT_2022 : NATIVE_MINT; + + let tokenA = Keypair.generate(); + while (PoolUtil.compareMints(tokenA.publicKey, nativeMint) >= 0) tokenA = Keypair.generate(); + let tokenB = Keypair.generate(); + while (PoolUtil.compareMints(nativeMint, tokenB.publicKey) >= 0) tokenB = Keypair.generate(); + + assert.ok(PoolUtil.orderMints(tokenA.publicKey, nativeMint)[1].toString() === nativeMint.toString()); + assert.ok(PoolUtil.orderMints(nativeMint, tokenB.publicKey)[0].toString() === nativeMint.toString()); + + await createMintV2(provider, {isToken2022: false}, undefined, tokenA); + await createMintV2(provider, {isToken2022: false}, undefined, tokenB); + + // create config and feetier + const configKeypair = Keypair.generate(); + await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, { + collectProtocolFeesAuthority: provider.wallet.publicKey, + feeAuthority: provider.wallet.publicKey, + rewardEmissionsSuperAuthority: provider.wallet.publicKey, + defaultProtocolFeeRate: 300, + funder: provider.wallet.publicKey, + whirlpoolsConfigKeypair: configKeypair, + })).addSigner(configKeypair).buildAndExecute(); + + const tickSpacing = 64; + await toTx(ctx, WhirlpoolIx.initializeFeeTierIx(ctx.program, { + defaultFeeRate: 3000, + feeAuthority: provider.wallet.publicKey, + funder: provider.wallet.publicKey, + tickSpacing, + whirlpoolsConfig: configKeypair.publicKey, + feeTierPda: PDAUtil.getFeeTier(ctx.program.programId, configKeypair.publicKey, tickSpacing), + })).buildAndExecute(); + + // create token badge if wanted + if (params.createTokenBadge) { + const pda = PDAUtil.getConfigExtension(ctx.program.programId, configKeypair.publicKey); + await toTx(ctx, WhirlpoolIx.initializeConfigExtensionIx(ctx.program, { + feeAuthority: provider.wallet.publicKey, + funder: provider.wallet.publicKey, + whirlpoolsConfig: configKeypair.publicKey, + whirlpoolsConfigExtensionPda: pda, + })).buildAndExecute(); + + const configExtension = PDAUtil.getConfigExtension(ctx.program.programId, configKeypair.publicKey).publicKey; + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, configKeypair.publicKey, nativeMint); + await toTx(ctx, WhirlpoolIx.initializeTokenBadgeIx(ctx.program, { + whirlpoolsConfig: configKeypair.publicKey, + whirlpoolsConfigExtension: configExtension, + funder: provider.wallet.publicKey, + tokenBadgeAuthority: provider.wallet.publicKey, + tokenBadgePda, + tokenMint: nativeMint, + })).buildAndExecute(); + } + + // try to initialize pool + await checkSupported(params.supported, configKeypair.publicKey, tokenA.publicKey, nativeMint, tickSpacing, params.anchorPatch); // as TokenB + await checkSupported(params.supported, configKeypair.publicKey, nativeMint, tokenB.publicKey, tickSpacing, params.anchorPatch); // as TokenA + } + + it("Token: mint without FreezeAuthority", async () => { + await runTest({ + supported: true, + createTokenBadge: false, + tokenTrait: { + isToken2022: false, + } + }); + }); + + it("Token: mint with FreezeAuthority", async () => { + // not good, but allowed for compatibility to initialize_pool + await runTest({ + supported: true, + createTokenBadge: false, + tokenTrait: { + isToken2022: false, + hasFreezeAuthority: true, + } + }); + }); + + it("Token: native mint (WSOL)", async () => { + await runTestWithNativeMint({ + supported: true, + createTokenBadge: false, + isToken2022NativeMint: false, + }); + }); + + it("Token-2022: with TransferFeeConfig", async () => { + await runTest({ + supported: true, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasTransferFeeExtension: true, + } + }); + }); + + it("Token-2022: with MetadataPointer & TokenMetadata", async () => { + await runTest({ + supported: true, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasTokenMetadataExtension: true, + hasMetadataPointerExtension: true, + } + }); + }); + + it("Token-2022: with ConfidentialTransferMint", async () => { + await runTest({ + supported: true, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasConfidentialTransferExtension: true, + } + }); + }); + + it("Token-2022: with TokenBadge with FreezeAuthority", async () => { + await runTest({ + supported: true, + createTokenBadge: true, + tokenTrait: { + isToken2022: true, + hasFreezeAuthority: true, + } + }); + }); + + it("Token-2022: with TokenBadge with PermanentDelegate", async () => { + await runTest({ + supported: true, + createTokenBadge: true, + tokenTrait: { + isToken2022: true, + hasPermanentDelegate: true, + } + }); + }); + + it("Token-2022: with TokenBadge with TransferHook", async () => { + await runTest({ + supported: true, + createTokenBadge: true, + tokenTrait: { + isToken2022: true, + hasTransferHookExtension: true, + } + }); + }); + + it("Token-2022: with TokenBadge with MintCloseAuthority", async () => { + await runTest({ + supported: true, + createTokenBadge: true, + tokenTrait: { + isToken2022: true, + hasMintCloseAuthorityExtension: true, + } + }); + }); + + it("Token-2022: with TokenBadge with DefaultAccountState(Initialized)", async () => { + await runTest({ + supported: true, + createTokenBadge: true, + tokenTrait: { + isToken2022: true, + hasDefaultAccountStateExtension: true, + defaultAccountInitialState: AccountState.Initialized, + } + }); + }); + + it("Token-2022: [FAIL] with TokenBadge with DefaultAccountState(Frozen)", async () => { + await runTest({ + supported: false, + createTokenBadge: true, + tokenTrait: { + isToken2022: true, + hasFreezeAuthority: true, // needed to set initial state to Frozen + hasDefaultAccountStateExtension: true, + defaultAccountInitialState: AccountState.Frozen, + } + }); + }); + + it("Token-2022: [FAIL] without TokenBadge with FreezeAuthority", async () => { + await runTest({ + supported: false, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasFreezeAuthority: true, + } + }); + }); + + it("Token-2022: [FAIL] without TokenBadge with PermanentDelegate", async () => { + await runTest({ + supported: false, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasPermanentDelegate: true, + } + }); + }); + + it("Token-2022: [FAIL] without TokenBadge with TransferHook", async () => { + await runTest({ + supported: false, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasTransferHookExtension: true, + } + }); + }); + + it("Token-2022: [FAIL] without TokenBadge with MintCloseAuthority", async () => { + await runTest({ + supported: false, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasMintCloseAuthorityExtension: true, + } + }); + }); + + it("Token-2022: [FAIL] without TokenBadge with DefaultAccountState(Initialized)", async () => { + await runTest({ + supported: false, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasDefaultAccountStateExtension: true, + defaultAccountInitialState: AccountState.Initialized, + } + }); + }); + + it("Token-2022: [FAIL] without TokenBadge with DefaultAccountState(Frozen)", async () => { + await runTest({ + supported: false, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasFreezeAuthority: true, // needed to set initial state to Frozen + hasDefaultAccountStateExtension: true, + defaultAccountInitialState: AccountState.Frozen, + } + }); + }); + + it("Token-2022: [FAIL] with/without TokenBadge, native mint (WSOL-2022)", async () => { + await runTestWithNativeMint({ + supported: false, + createTokenBadge: false, + isToken2022NativeMint: true, + }); + + await runTestWithNativeMint({ + supported: false, + createTokenBadge: true, + isToken2022NativeMint: true, + }); + }); + + it("Token-2022: [FAIL] with/without TokenBadge with InterestBearingConfig", async () => { + const tokenTrait: TokenTrait = { + isToken2022: true, + hasInterestBearingExtension: true, + }; + await runTest({ supported: false, createTokenBadge: true, tokenTrait }); + await runTest({ supported: false, createTokenBadge: false, tokenTrait }); + }); + + it("Token-2022: [FAIL] with/without TokenBadge with Group", async () => { + assert.ok(false, "[11 Mar, 2024] NOT IMPLEMENTED / I believe this extension is not stable yet"); + /* + const tokenTrait: TokenTrait = { + isToken2022: true, + hasGroupExtension: true, + }; + // TODO: remove anchorPatch: v0.29 doesn't recognize Group + await runTest({ supported: false, createTokenBadge: true, tokenTrait, anchorPatch: true }); + await runTest({ supported: false, createTokenBadge: false, tokenTrait, anchorPatch: true }); + */ + }); + + it("Token-2022: [FAIL] with/without TokenBadge with GroupPointer" , async () => { + const tokenTrait: TokenTrait = { + isToken2022: true, + hasGroupPointerExtension: true, + }; + // TODO: remove anchorPatch: v0.29 doesn't recognize GroupPointer + await runTest({ supported: false, createTokenBadge: true, tokenTrait, anchorPatch: true }); + await runTest({ supported: false, createTokenBadge: false, tokenTrait, anchorPatch: true }); + }); + + it("Token-2022: [FAIL] with/without TokenBadge with Member", async () => { + assert.ok(false, "[11 Mar, 2024] NOT IMPLEMENTED / I believe this extension is not stable yet"); + /* + const tokenTrait: TokenTrait = { + isToken2022: true, + hasGroupMemberExtension: true, + }; + // TODO: remove anchorPatch: v0.29 doesn't recognize Member + await runTest({ supported: false, createTokenBadge: true, tokenTrait, anchorPatch: true }); + await runTest({ supported: false, createTokenBadge: false, tokenTrait, anchorPatch: true }); + */ + }); + + it("Token-2022: [FAIL] with/without TokenBadge with MemberPointer", async () => { + const tokenTrait: TokenTrait = { + isToken2022: true, + hasGroupMemberPointerExtension: true, + }; + // TODO: remove anchorPatch: v0.29 doesn't recognize MemberPointer + await runTest({ supported: false, createTokenBadge: true, tokenTrait, anchorPatch: true }); + await runTest({ supported: false, createTokenBadge: false, tokenTrait, anchorPatch: true }); + }); + + it("Token-2022: [FAIL] with/without TokenBadge with NonTransferable", async () => { + const tokenTrait: TokenTrait = { + isToken2022: true, + hasNonTransferableExtension: true, + }; + await runTest({ supported: false, createTokenBadge: true, tokenTrait }); + await runTest({ supported: false, createTokenBadge: false, tokenTrait }); + }); + }); +}); diff --git a/sdk/tests/integration/v2/initialize_reward_v2.test.ts b/sdk/tests/integration/v2/initialize_reward_v2.test.ts new file mode 100644 index 000000000..a32cb77b3 --- /dev/null +++ b/sdk/tests/integration/v2/initialize_reward_v2.test.ts @@ -0,0 +1,780 @@ +import * as anchor from "@coral-xyz/anchor"; +import * as assert from "assert"; +import { METADATA_PROGRAM_ADDRESS, PDAUtil, toTx, WhirlpoolContext, WhirlpoolData, WhirlpoolIx } from "../../../src"; +import { IGNORE_CACHE } from "../../../src/network/public/fetcher"; +import { ONE_SOL, systemTransferTx, TickSpacing } from "../../utils"; +import { defaultConfirmOptions } from "../../utils/const"; +import { TokenTrait, initTestPoolV2, initializeRewardV2 } from "../../utils/v2/init-utils-v2"; +import { asyncAssertOwnerProgram, createMintV2 } from "../../utils/v2/token-2022"; +import { TEST_TOKEN_2022_PROGRAM_ID, TEST_TOKEN_PROGRAM_ID } from "../../utils"; +import { AccountState } from "@solana/spl-token"; +import { Keypair } from "@solana/web3.js"; + +describe("initialize_reward_v2", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + + describe("v1 parity", () => { + const tokenTraitVariations: { tokenTraitAB: TokenTrait; tokenTraitR: TokenTrait }[] = [ + { tokenTraitAB: { isToken2022: false }, tokenTraitR: { isToken2022: false } }, + { tokenTraitAB: { isToken2022: true }, tokenTraitR: { isToken2022: false } }, + { tokenTraitAB: { isToken2022: false }, tokenTraitR: { isToken2022: true } }, + { tokenTraitAB: { isToken2022: true }, tokenTraitR: { isToken2022: true } }, + ]; + tokenTraitVariations.forEach((tokenTraits) => { + describe(`tokenTraitA/B: ${ + tokenTraits.tokenTraitAB.isToken2022 ? "Token2022" : "Token" + }, tokenTraitReward: ${tokenTraits.tokenTraitR.isToken2022 ? "Token2022" : "Token"}`, () => { + it("successfully initializes reward at index 0", async () => { + const { poolInitInfo, configKeypairs, configExtension } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitAB, + tokenTraits.tokenTraitAB, + TickSpacing.Standard + ); + + const { params } = await initializeRewardV2( + ctx, + tokenTraits.tokenTraitR, + poolInitInfo.whirlpoolsConfig, + configKeypairs.rewardEmissionsSuperAuthorityKeypair, + poolInitInfo.whirlpoolPda.publicKey, + 0, + configExtension.configExtensionKeypairs.tokenBadgeAuthorityKeypair, + ); + + const whirlpool = (await fetcher.getPool( + poolInitInfo.whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + + assert.ok(whirlpool.rewardInfos[0].mint.equals(params.rewardMint)); + assert.ok(whirlpool.rewardInfos[0].vault.equals(params.rewardVaultKeypair.publicKey)); + + await assert.rejects( + initializeRewardV2( + ctx, + tokenTraits.tokenTraitR, + poolInitInfo.whirlpoolsConfig, + configKeypairs.rewardEmissionsSuperAuthorityKeypair, + poolInitInfo.whirlpoolPda.publicKey, + 0, + configExtension.configExtensionKeypairs.tokenBadgeAuthorityKeypair, + ), + /custom program error: 0x178a/ // InvalidRewardIndex + ); + + const { params: params2 } = await initializeRewardV2( + ctx, + tokenTraits.tokenTraitR, + poolInitInfo.whirlpoolsConfig, + configKeypairs.rewardEmissionsSuperAuthorityKeypair, + poolInitInfo.whirlpoolPda.publicKey, + 1, + configExtension.configExtensionKeypairs.tokenBadgeAuthorityKeypair, + ); + + const whirlpool2 = (await fetcher.getPool( + poolInitInfo.whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + + assert.ok(whirlpool2.rewardInfos[0].mint.equals(params.rewardMint)); + assert.ok(whirlpool2.rewardInfos[0].vault.equals(params.rewardVaultKeypair.publicKey)); + await asyncAssertOwnerProgram( + provider, + whirlpool2.rewardInfos[0].vault, + tokenTraits.tokenTraitR.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID + ); + assert.ok(whirlpool2.rewardInfos[1].mint.equals(params2.rewardMint)); + assert.ok(whirlpool2.rewardInfos[1].vault.equals(params2.rewardVaultKeypair.publicKey)); + await asyncAssertOwnerProgram( + provider, + whirlpool2.rewardInfos[1].vault, + tokenTraits.tokenTraitR.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID + ); + assert.ok(whirlpool2.rewardInfos[2].mint.equals(anchor.web3.PublicKey.default)); + assert.ok(whirlpool2.rewardInfos[2].vault.equals(anchor.web3.PublicKey.default)); + }); + + it("succeeds when funder is different than account paying for transaction fee", async () => { + const { poolInitInfo, configKeypairs } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitAB, + tokenTraits.tokenTraitAB, + TickSpacing.Standard + ); + const funderKeypair = anchor.web3.Keypair.generate(); + await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute(); + await initializeRewardV2( + ctx, + tokenTraits.tokenTraitR, + poolInitInfo.whirlpoolsConfig, + configKeypairs.rewardEmissionsSuperAuthorityKeypair, + poolInitInfo.whirlpoolPda.publicKey, + 0, + funderKeypair + ); + }); + + it("fails to initialize reward at index 1", async () => { + const { poolInitInfo, configKeypairs, configExtension } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitAB, + tokenTraits.tokenTraitAB, + TickSpacing.Standard + ); + + await assert.rejects( + initializeRewardV2( + ctx, + tokenTraits.tokenTraitR, + poolInitInfo.whirlpoolsConfig, + configKeypairs.rewardEmissionsSuperAuthorityKeypair, + poolInitInfo.whirlpoolPda.publicKey, + 1, + configExtension.configExtensionKeypairs.tokenBadgeAuthorityKeypair, + ), + /custom program error: 0x178a/ // InvalidRewardIndex + ); + }); + + it("fails to initialize reward at out-of-bound index", async () => { + const { poolInitInfo, configKeypairs, configExtension } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitAB, + tokenTraits.tokenTraitAB, + TickSpacing.Standard + ); + + await assert.rejects( + initializeRewardV2( + ctx, + tokenTraits.tokenTraitR, + poolInitInfo.whirlpoolsConfig, + configKeypairs.rewardEmissionsSuperAuthorityKeypair, + poolInitInfo.whirlpoolPda.publicKey, + 3, + configExtension.configExtensionKeypairs.tokenBadgeAuthorityKeypair, + ) + ); + }); + + it("fails to initialize if authority signature is missing", async () => { + const { poolInitInfo, configKeypairs } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitAB, + tokenTraits.tokenTraitAB, + TickSpacing.Standard + ); + + const rewardMint = await createMintV2(provider, tokenTraits.tokenTraitR); + + const rewardTokenBadgePda = PDAUtil.getTokenBadge( + ctx.program.programId, + poolInitInfo.whirlpoolsConfig, + rewardMint + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializeRewardV2Ix(ctx.program, { + rewardAuthority: configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey, + funder: provider.wallet.publicKey, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + rewardMint, + rewardTokenBadge: rewardTokenBadgePda.publicKey, + rewardTokenProgram: tokenTraits.tokenTraitR.isToken2022 + ? TEST_TOKEN_2022_PROGRAM_ID + : TEST_TOKEN_PROGRAM_ID, + rewardVaultKeypair: anchor.web3.Keypair.generate(), + rewardIndex: 0, + }) + ).buildAndExecute() + ); + }); + }); + }); + }); + + describe("v2 specific accounts", () => { + it("fails when passed reward_token_program is not token program (token-2022 is passed)", async () => { + const { poolInitInfo, configKeypairs } = await initTestPoolV2( + ctx, + {isToken2022: true}, + {isToken2022: true}, + TickSpacing.Standard + ); + + const rewardMint = await createMintV2(provider, {isToken2022: false}); + + const rewardTokenBadgePda = PDAUtil.getTokenBadge( + ctx.program.programId, + poolInitInfo.whirlpoolsConfig, + rewardMint + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializeRewardV2Ix(ctx.program, { + rewardAuthority: configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey, + funder: provider.wallet.publicKey, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + rewardMint, + rewardTokenBadge: rewardTokenBadgePda.publicKey, + rewardTokenProgram: TEST_TOKEN_2022_PROGRAM_ID, + rewardVaultKeypair: anchor.web3.Keypair.generate(), + rewardIndex: 0, + }) + ) + .addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair) + .buildAndExecute(), + /incorrect program id for instruction/ // Anchor will try to create vault account + ); + }); + + it("fails when passed reward_token_program is not token-2022 program (token is passed)", async () => { + const { poolInitInfo, configKeypairs } = await initTestPoolV2( + ctx, + {isToken2022: true}, + {isToken2022: true}, + TickSpacing.Standard + ); + + const rewardMint = await createMintV2(provider, {isToken2022: true}); + + const rewardTokenBadgePda = PDAUtil.getTokenBadge( + ctx.program.programId, + poolInitInfo.whirlpoolsConfig, + rewardMint + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializeRewardV2Ix(ctx.program, { + rewardAuthority: configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey, + funder: provider.wallet.publicKey, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + rewardMint, + rewardTokenBadge: rewardTokenBadgePda.publicKey, + rewardTokenProgram: TEST_TOKEN_PROGRAM_ID, + rewardVaultKeypair: anchor.web3.Keypair.generate(), + rewardIndex: 0, + }) + ) + .addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair) + .buildAndExecute(), + /incorrect program id for instruction/ // Anchor will try to create vault account + ); + }); + + it("fails when passed reward_token_program is token_metadata", async () => { + const { poolInitInfo, configKeypairs } = await initTestPoolV2( + ctx, + {isToken2022: true}, + {isToken2022: true}, + TickSpacing.Standard + ); + + const rewardMint = await createMintV2(provider, {isToken2022: true}); + + const rewardTokenBadgePda = PDAUtil.getTokenBadge( + ctx.program.programId, + poolInitInfo.whirlpoolsConfig, + rewardMint + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializeRewardV2Ix(ctx.program, { + rewardAuthority: configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey, + funder: provider.wallet.publicKey, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + rewardMint, + rewardTokenBadge: rewardTokenBadgePda.publicKey, + rewardTokenProgram: METADATA_PROGRAM_ADDRESS, + rewardVaultKeypair: anchor.web3.Keypair.generate(), + rewardIndex: 0, + }) + ) + .addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair) + .buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + describe("invalid badge account", () => { + it("fails when reward_token_badge address invalid (uninitialized)", async () => { + const { poolInitInfo, configKeypairs } = await initTestPoolV2( + ctx, + {isToken2022: true}, + {isToken2022: true}, + TickSpacing.Standard + ); + + const rewardMint = await createMintV2(provider, {isToken2022: true, hasPermanentDelegate: true}); + const fakeAddress = Keypair.generate().publicKey; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializeRewardV2Ix(ctx.program, { + rewardAuthority: configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey, + funder: provider.wallet.publicKey, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + rewardMint, + rewardTokenBadge: fakeAddress, + rewardTokenProgram: TEST_TOKEN_2022_PROGRAM_ID, + rewardVaultKeypair: anchor.web3.Keypair.generate(), + rewardIndex: 0, + }) + ) + .addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair) + .buildAndExecute(), + /custom program error: 0x7d6/ // ConstraintSeeds + ); + }); + + it("fails when reward_token_badge address invalid (initialized, same config / different mint)", async () => { + const { poolInitInfo, configKeypairs, configExtension } = await initTestPoolV2( + ctx, + {isToken2022: true}, + {isToken2022: true}, + TickSpacing.Standard + ); + + const rewardMint = await createMintV2(provider, {isToken2022: true, hasPermanentDelegate: true}); + const anotherMint = await createMintV2(provider, {isToken2022: true, hasPermanentDelegate: true}); + + // initialize another badge + const config = poolInitInfo.whirlpoolsConfig; + const configExtensionPda = PDAUtil.getConfigExtension(ctx.program.programId, config); + const anotherMintTokenBadgePda = PDAUtil.getTokenBadge( + ctx.program.programId, + config, + anotherMint + ); + const tokenBadgeAuthority = configExtension.configExtensionKeypairs.tokenBadgeAuthorityKeypair; + await toTx(ctx, WhirlpoolIx.initializeTokenBadgeIx(ctx.program, { + whirlpoolsConfig: config, + whirlpoolsConfigExtension: configExtensionPda.publicKey, + funder: provider.wallet.publicKey, + tokenBadgeAuthority: tokenBadgeAuthority.publicKey, + tokenBadgePda: anotherMintTokenBadgePda, + tokenMint: anotherMint, + })).addSigner(tokenBadgeAuthority).buildAndExecute(); + const badge = fetcher.getTokenBadge(anotherMintTokenBadgePda.publicKey, IGNORE_CACHE); + assert.ok(badge !== null); + + const fakeAddress = anotherMintTokenBadgePda.publicKey; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializeRewardV2Ix(ctx.program, { + rewardAuthority: configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey, + funder: provider.wallet.publicKey, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + rewardMint, + rewardTokenBadge: fakeAddress, + rewardTokenProgram: TEST_TOKEN_2022_PROGRAM_ID, + rewardVaultKeypair: anchor.web3.Keypair.generate(), + rewardIndex: 0, + }) + ) + .addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair) + .buildAndExecute(), + /custom program error: 0x7d6/ // ConstraintSeeds + ); + }); + + it("fails when reward_token_badge address invalid (initialized, account owned by WhirlpoolProgram)", async () => { + const { poolInitInfo, configKeypairs } = await initTestPoolV2( + ctx, + {isToken2022: true}, + {isToken2022: true}, + TickSpacing.Standard + ); + + const rewardMint = await createMintV2(provider, {isToken2022: true, hasPermanentDelegate: true}); + const fakeAddress = poolInitInfo.whirlpoolPda.publicKey; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.initializeRewardV2Ix(ctx.program, { + rewardAuthority: configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey, + funder: provider.wallet.publicKey, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + rewardMint, + rewardTokenBadge: fakeAddress, + rewardTokenProgram: TEST_TOKEN_2022_PROGRAM_ID, + rewardVaultKeypair: anchor.web3.Keypair.generate(), + rewardIndex: 0, + }) + ) + .addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair) + .buildAndExecute(), + /custom program error: 0x7d6/ // ConstraintSeeds + ); + }); + }); + }); + + describe("Supported Tokens", () => { + async function runTest(params: { + supported: boolean, + createTokenBadge: boolean, + tokenTrait: TokenTrait, + anchorPatch?: boolean, + }) { + const { poolInitInfo, configKeypairs, configExtension } = await initTestPoolV2( + ctx, + {isToken2022: true}, + {isToken2022: true}, + TickSpacing.Standard + ); + const config = poolInitInfo.whirlpoolsConfig; + + const rewardToken = await createMintV2(provider, params.tokenTrait); + const tokenProgram = (await provider.connection.getAccountInfo(rewardToken))!.owner; + + // create token badge if wanted + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, config, rewardToken); + if (params.createTokenBadge) { + await toTx(ctx, WhirlpoolIx.initializeTokenBadgeIx(ctx.program, { + whirlpoolsConfig: config, + whirlpoolsConfigExtension: configExtension.configExtensionInitInfo.whirlpoolsConfigExtensionPda.publicKey, + funder: provider.wallet.publicKey, + tokenBadgeAuthority: configExtension.configExtensionKeypairs.tokenBadgeAuthorityKeypair.publicKey, + tokenBadgePda, + tokenMint: rewardToken, + })).addSigner(configExtension.configExtensionKeypairs.tokenBadgeAuthorityKeypair).buildAndExecute(); + } + + // try to initialize reward + const promise = toTx( + ctx, + WhirlpoolIx.initializeRewardV2Ix(ctx.program, { + rewardAuthority: configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey, + funder: provider.wallet.publicKey, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + rewardMint: rewardToken, + rewardTokenBadge: tokenBadgePda.publicKey, + rewardTokenProgram: tokenProgram, + rewardVaultKeypair: anchor.web3.Keypair.generate(), + rewardIndex: 0, + }) + ) + .addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair) + .buildAndExecute(); + + if (params.supported) { + await promise; + const whirlpoolData = await fetcher.getPool(poolInitInfo.whirlpoolPda.publicKey, IGNORE_CACHE); + assert.ok(whirlpoolData!.rewardInfos[0].mint.equals(rewardToken)); + } else { + await assert.rejects( + promise, + !params.anchorPatch + ? /0x179f/ // UnsupportedTokenMint + : /invalid account data for instruction/ // Anchor v0.29 doesn't recognize some new extensions (GroupPointer, Group, MemberPointer, Member) + ); + } + } + + it("Token: mint without FreezeAuthority", async () => { + await runTest({ + supported: true, + createTokenBadge: false, + tokenTrait: { + isToken2022: false, + } + }); + }); + + it("Token: mint with FreezeAuthority", async () => { + // not good, but allowed for compatibility to initialize_pool + await runTest({ + supported: true, + createTokenBadge: false, + tokenTrait: { + isToken2022: false, + hasFreezeAuthority: true, + } + }); + }); + + it("Token: native mint (WSOL)", async () => { + await runTest({ + supported: true, + createTokenBadge: false, + tokenTrait: { + isToken2022: false, + isNativeMint: true, + } + }); + }); + + it("Token-2022: with TransferFeeConfig", async () => { + await runTest({ + supported: true, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasTransferFeeExtension: true, + } + }); + }); + + it("Token-2022: with MetadataPointer & TokenMetadata", async () => { + await runTest({ + supported: true, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasTokenMetadataExtension: true, + hasMetadataPointerExtension: true, + } + }); + }); + + it("Token-2022: with ConfidentialTransferMint", async () => { + await runTest({ + supported: true, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasConfidentialTransferExtension: true, + } + }); + }); + + it("Token-2022: with TokenBadge with FreezeAuthority", async () => { + await runTest({ + supported: true, + createTokenBadge: true, + tokenTrait: { + isToken2022: true, + hasFreezeAuthority: true, + } + }); + }); + + it("Token-2022: with TokenBadge with PermanentDelegate", async () => { + await runTest({ + supported: true, + createTokenBadge: true, + tokenTrait: { + isToken2022: true, + hasPermanentDelegate: true, + } + }); + }); + + it("Token-2022: with TokenBadge with TransferHook", async () => { + await runTest({ + supported: true, + createTokenBadge: true, + tokenTrait: { + isToken2022: true, + hasTransferHookExtension: true, + } + }); + }); + + it("Token-2022: with TokenBadge with MintCloseAuthority", async () => { + await runTest({ + supported: true, + createTokenBadge: true, + tokenTrait: { + isToken2022: true, + hasMintCloseAuthorityExtension: true, + } + }); + }); + + it("Token-2022: with TokenBadge with DefaultAccountState(Initialized)", async () => { + await runTest({ + supported: true, + createTokenBadge: true, + tokenTrait: { + isToken2022: true, + hasDefaultAccountStateExtension: true, + defaultAccountInitialState: AccountState.Initialized, + } + }); + }); + + it("Token-2022: [FAIL] with TokenBadge with DefaultAccountState(Frozen)", async () => { + await runTest({ + supported: false, + createTokenBadge: true, + tokenTrait: { + isToken2022: true, + hasFreezeAuthority: true, // needed to set initial state to Frozen + hasDefaultAccountStateExtension: true, + defaultAccountInitialState: AccountState.Frozen, + } + }); + }); + + it("Token-2022: [FAIL] without TokenBadge with FreezeAuthority", async () => { + await runTest({ + supported: false, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasFreezeAuthority: true, + } + }); + }); + + it("Token-2022: [FAIL] without TokenBadge with PermanentDelegate", async () => { + await runTest({ + supported: false, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasPermanentDelegate: true, + } + }); + }); + + it("Token-2022: [FAIL] without TokenBadge with TransferHook", async () => { + await runTest({ + supported: false, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasTransferHookExtension: true, + } + }); + }); + + it("Token-2022: [FAIL] without TokenBadge with MintCloseAuthority", async () => { + await runTest({ + supported: false, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasMintCloseAuthorityExtension: true, + } + }); + }); + + it("Token-2022: [FAIL] without TokenBadge with DefaultAccountState(Initialized)", async () => { + await runTest({ + supported: false, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasDefaultAccountStateExtension: true, + defaultAccountInitialState: AccountState.Initialized, + } + }); + }); + + it("Token-2022: [FAIL] without TokenBadge with DefaultAccountState(Frozen)", async () => { + await runTest({ + supported: false, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + hasFreezeAuthority: true, // needed to set initial state to Frozen + hasDefaultAccountStateExtension: true, + defaultAccountInitialState: AccountState.Frozen, + } + }); + }); + + it("Token-2022: [FAIL] with/without TokenBadge, native mint (WSOL-2022)", async () => { + await runTest({ + supported: false, + createTokenBadge: false, + tokenTrait: { + isToken2022: true, + isNativeMint: true, + } + }); + await runTest({ + supported: false, + createTokenBadge: true, + tokenTrait: { + isToken2022: true, + isNativeMint: true, + } + }); + }); + + it("Token-2022: [FAIL] with/without TokenBadge with InterestBearingConfig", async () => { + const tokenTrait: TokenTrait = { + isToken2022: true, + hasInterestBearingExtension: true, + }; + await runTest({ supported: false, createTokenBadge: true, tokenTrait }); + await runTest({ supported: false, createTokenBadge: false, tokenTrait }); + }); + + it("Token-2022: [FAIL] with/without TokenBadge with Group", async () => { + assert.ok(false, "[11 Mar, 2024] NOT IMPLEMENTED / I believe this extension is not stable yet"); + /* + const tokenTrait: TokenTrait = { + isToken2022: true, + hasGroupExtension: true, + }; + // TODO: remove anchorPatch: v0.29 doesn't recognize Group + await runTest({ supported: false, createTokenBadge: true, tokenTrait, anchorPatch: true }); + await runTest({ supported: false, createTokenBadge: false, tokenTrait, anchorPatch: true }); + */ + }); + + it("Token-2022: [FAIL] with/without TokenBadge with GroupPointer" , async () => { + const tokenTrait: TokenTrait = { + isToken2022: true, + hasGroupPointerExtension: true, + }; + // TODO: remove anchorPatch: v0.29 doesn't recognize GroupPointer + await runTest({ supported: false, createTokenBadge: true, tokenTrait, anchorPatch: true }); + await runTest({ supported: false, createTokenBadge: false, tokenTrait, anchorPatch: true }); + }); + + it("Token-2022: [FAIL] with/without TokenBadge with Member", async () => { + assert.ok(false, "[11 Mar, 2024] NOT IMPLEMENTED / I believe this extension is not stable yet"); + /* + const tokenTrait: TokenTrait = { + isToken2022: true, + hasGroupMemberExtension: true, + }; + // TODO: remove anchorPatch: v0.29 doesn't recognize Member + await runTest({ supported: false, createTokenBadge: true, tokenTrait, anchorPatch: true }); + await runTest({ supported: false, createTokenBadge: false, tokenTrait, anchorPatch: true }); + */ + }); + + it("Token-2022: [FAIL] with/without TokenBadge with MemberPointer", async () => { + const tokenTrait: TokenTrait = { + isToken2022: true, + hasGroupMemberPointerExtension: true, + }; + // TODO: remove anchorPatch: v0.29 doesn't recognize MemberPointer + await runTest({ supported: false, createTokenBadge: true, tokenTrait, anchorPatch: true }); + await runTest({ supported: false, createTokenBadge: false, tokenTrait, anchorPatch: true }); + }); + + it("Token-2022: [FAIL] with/without TokenBadge with NonTransferable", async () => { + const tokenTrait: TokenTrait = { + isToken2022: true, + hasNonTransferableExtension: true, + }; + await runTest({ supported: false, createTokenBadge: true, tokenTrait }); + await runTest({ supported: false, createTokenBadge: false, tokenTrait }); + }); + }); +}); diff --git a/sdk/tests/integration/v2/set_reward_emissions_v2.test.ts b/sdk/tests/integration/v2/set_reward_emissions_v2.test.ts new file mode 100644 index 000000000..5a77d4790 --- /dev/null +++ b/sdk/tests/integration/v2/set_reward_emissions_v2.test.ts @@ -0,0 +1,248 @@ +import * as anchor from "@coral-xyz/anchor"; +import * as assert from "assert"; +import { toTx, WhirlpoolContext, WhirlpoolData, WhirlpoolIx } from "../../../src"; +import { IGNORE_CACHE } from "../../../src/network/public/fetcher"; +import { TickSpacing, ZERO_BN } from "../../utils"; +import { defaultConfirmOptions } from "../../utils/const"; +import { initTestPoolV2, initializeRewardV2 } from "../../utils/v2/init-utils-v2"; +import { TokenTrait } from "../../utils/v2/init-utils-v2"; +import { createAndMintToTokenAccountV2, mintToDestinationV2 } from "../../utils/v2/token-2022"; + +describe("set_reward_emissions_v2", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + + const emissionsPerSecondX64 = new anchor.BN(10_000).shln(64).div(new anchor.BN(60 * 60 * 24)); + + describe("v1 parity", () => { + const tokenTraitVariations: { tokenTraitAB: TokenTrait; tokenTraitR: TokenTrait }[] = [ + { tokenTraitAB: { isToken2022: false }, tokenTraitR: { isToken2022: false } }, + { tokenTraitAB: { isToken2022: true }, tokenTraitR: { isToken2022: false } }, + { tokenTraitAB: { isToken2022: false }, tokenTraitR: { isToken2022: true } }, + { tokenTraitAB: { isToken2022: true }, tokenTraitR: { isToken2022: true } }, + ]; + tokenTraitVariations.forEach((tokenTraits) => { + describe(`tokenTraitA/B: ${ + tokenTraits.tokenTraitAB.isToken2022 ? "Token2022" : "Token" + }, tokenTraitReward: ${tokenTraits.tokenTraitR.isToken2022 ? "Token2022" : "Token"}`, () => { + it("successfully set_reward_emissions", async () => { + const { poolInitInfo, configInitInfo, configKeypairs, configExtension } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitAB, + tokenTraits.tokenTraitAB, + TickSpacing.Standard + ); + + const rewardIndex = 0; + + const { + params: { rewardVaultKeypair, rewardMint }, + } = await initializeRewardV2( + ctx, + tokenTraits.tokenTraitR, + poolInitInfo.whirlpoolsConfig, + configKeypairs.rewardEmissionsSuperAuthorityKeypair, + poolInitInfo.whirlpoolPda.publicKey, + rewardIndex, + configExtension.configExtensionKeypairs.tokenBadgeAuthorityKeypair, + ); + + await mintToDestinationV2( + provider, + tokenTraits.tokenTraitR, + rewardMint, + rewardVaultKeypair.publicKey, + 10000 + ); + + await toTx( + ctx, + WhirlpoolIx.setRewardEmissionsV2Ix(ctx.program, { + rewardAuthority: configInitInfo.rewardEmissionsSuperAuthority, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + rewardIndex, + rewardVaultKey: rewardVaultKeypair.publicKey, + emissionsPerSecondX64, + }) + ) + .addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair) + .buildAndExecute(); + + let whirlpool = (await fetcher.getPool( + poolInitInfo.whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + assert.ok(whirlpool.rewardInfos[0].emissionsPerSecondX64.eq(emissionsPerSecondX64)); + + // Successfuly set emissions back to zero + await toTx( + ctx, + WhirlpoolIx.setRewardEmissionsV2Ix(ctx.program, { + rewardAuthority: configInitInfo.rewardEmissionsSuperAuthority, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + rewardIndex, + rewardVaultKey: rewardVaultKeypair.publicKey, + emissionsPerSecondX64: ZERO_BN, + }) + ) + .addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair) + .buildAndExecute(); + + whirlpool = (await fetcher.getPool( + poolInitInfo.whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + assert.ok(whirlpool.rewardInfos[0].emissionsPerSecondX64.eq(ZERO_BN)); + }); + + it("fails when token vault does not contain at least 1 day of emission runway", async () => { + const { poolInitInfo, configInitInfo, configKeypairs, configExtension } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitAB, + tokenTraits.tokenTraitAB, + TickSpacing.Standard + ); + + const rewardIndex = 0; + + const { + params: { rewardVaultKeypair }, + } = await initializeRewardV2( + ctx, + tokenTraits.tokenTraitR, + poolInitInfo.whirlpoolsConfig, + configKeypairs.rewardEmissionsSuperAuthorityKeypair, + poolInitInfo.whirlpoolPda.publicKey, + rewardIndex, + configExtension.configExtensionKeypairs.tokenBadgeAuthorityKeypair, + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.setRewardEmissionsV2Ix(ctx.program, { + rewardAuthority: configInitInfo.rewardEmissionsSuperAuthority, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + rewardIndex, + rewardVaultKey: rewardVaultKeypair.publicKey, + emissionsPerSecondX64, + }) + ) + .addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair) + .buildAndExecute(), + /0x178b/ // RewardVaultAmountInsufficient + ); + }); + + it("fails if provided reward vault does not match whirlpool reward vault", async () => { + const { poolInitInfo, configInitInfo, configKeypairs, configExtension } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitAB, + tokenTraits.tokenTraitAB, + TickSpacing.Standard + ); + + const rewardIndex = 0; + const { + params: { rewardVaultKeypair, rewardMint }, + } = await initializeRewardV2( + ctx, + tokenTraits.tokenTraitR, + poolInitInfo.whirlpoolsConfig, + configKeypairs.rewardEmissionsSuperAuthorityKeypair, + poolInitInfo.whirlpoolPda.publicKey, + rewardIndex, + configExtension.configExtensionKeypairs.tokenBadgeAuthorityKeypair, + ); + + const fakeVault = await createAndMintToTokenAccountV2( + provider, + tokenTraits.tokenTraitR, + rewardMint, + 10000 + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.setRewardEmissionsV2Ix(ctx.program, { + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + rewardAuthority: configInitInfo.rewardEmissionsSuperAuthority, + rewardVaultKey: fakeVault, + rewardIndex, + emissionsPerSecondX64, + }) + ) + .addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair) + .buildAndExecute(), + /0x7dc/ // An address constraint was violated + ); + }); + + it("cannot set emission for an uninitialized reward", async () => { + const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitAB, + tokenTraits.tokenTraitAB, + TickSpacing.Standard + ); + + const rewardIndex = 0; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.setRewardEmissionsV2Ix(ctx.program, { + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + rewardAuthority: configInitInfo.rewardEmissionsSuperAuthority, + rewardVaultKey: anchor.web3.PublicKey.default, + rewardIndex: rewardIndex, + emissionsPerSecondX64, + }) + ) + .addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair) + .buildAndExecute(), + /0xbbf/ // AccountOwnedByWrongProgram + ); + }); + + it("cannot set emission without the authority's signature", async () => { + const { poolInitInfo, configInitInfo, configKeypairs, configExtension } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitAB, + tokenTraits.tokenTraitAB, + TickSpacing.Standard + ); + + const rewardIndex = 0; + + await initializeRewardV2( + ctx, + tokenTraits.tokenTraitR, + poolInitInfo.whirlpoolsConfig, + configKeypairs.rewardEmissionsSuperAuthorityKeypair, + poolInitInfo.whirlpoolPda.publicKey, + rewardIndex, + configExtension.configExtensionKeypairs.tokenBadgeAuthorityKeypair, + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.setRewardEmissionsV2Ix(ctx.program, { + rewardAuthority: configInitInfo.rewardEmissionsSuperAuthority, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + rewardIndex, + rewardVaultKey: provider.wallet.publicKey, // TODO fix + emissionsPerSecondX64, + }) + ).buildAndExecute(), + /.*signature verification fail.*/i + ); + }); + }); + }); + }); +}); diff --git a/sdk/tests/integration/v2/swap_v2.test.ts b/sdk/tests/integration/v2/swap_v2.test.ts new file mode 100644 index 000000000..f7d47bd00 --- /dev/null +++ b/sdk/tests/integration/v2/swap_v2.test.ts @@ -0,0 +1,2543 @@ +import * as anchor from "@coral-xyz/anchor"; +import { web3 } from "@coral-xyz/anchor"; +import { MathUtil, Percentage } from "@orca-so/common-sdk"; +import * as assert from "assert"; +import { BN } from "bn.js"; +import Decimal from "decimal.js"; +import { + MAX_SQRT_PRICE, + METADATA_PROGRAM_ADDRESS, + MIN_SQRT_PRICE, + PDAUtil, + PriceMath, + SwapUtils, + SwapV2Params, + TICK_ARRAY_SIZE, + TickArrayData, + TickUtil, + WhirlpoolContext, + WhirlpoolData, + WhirlpoolIx, + buildWhirlpoolClient, + swapQuoteWithParams, + toTx, +} from "../../../src"; +import { IGNORE_CACHE } from "../../../src/network/public/fetcher"; +import { + MAX_U64, + TEST_TOKEN_2022_PROGRAM_ID, + TEST_TOKEN_PROGRAM_ID, + TickSpacing, + ZERO_BN, + getTokenBalance, +} from "../../utils"; +import { defaultConfirmOptions } from "../../utils/const"; +import { initTickArrayRange } from "../../utils/init-utils"; +import { + FundedPositionV2Params, + TokenTrait, + fundPositionsV2, + initTestPoolV2, + initTestPoolWithLiquidityV2, + initTestPoolWithTokensV2, + withdrawPositionsV2, +} from "../../utils/v2/init-utils-v2"; +import { createMintV2 } from "../../utils/v2/token-2022"; +import { TokenExtensionUtil } from "../../../src/utils/public/token-extension-util"; + +describe("swap_v2", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + const client = buildWhirlpoolClient(ctx); + + describe("v1 parity", () => { + const tokenTraitVariations: { + tokenTraitA: TokenTrait; + tokenTraitB: TokenTrait; + tokenTraitR: TokenTrait; + }[] = [ + { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + tokenTraitR: { isToken2022: false }, + }, + { + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: false }, + tokenTraitR: { isToken2022: false }, + }, + { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: true }, + tokenTraitR: { isToken2022: true }, + }, + { + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tokenTraitR: { isToken2022: true }, + }, + ]; + tokenTraitVariations.forEach((tokenTraits) => { + describe(`tokenTraitA: ${ + tokenTraits.tokenTraitA.isToken2022 ? "Token2022" : "Token" + }, tokenTraitB: ${ + tokenTraits.tokenTraitB.isToken2022 ? "Token2022" : "Token" + }, tokenTraitR: ${tokenTraits.tokenTraitR.isToken2022 ? "Token2022" : "Token"}`, () => { + it("fail on token vault mint a does not match whirlpool token a", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const { poolInitInfo: anotherPoolInitInfo } = await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Stable + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: anotherPoolInitInfo.tokenVaultAKeypair.publicKey, // invalid + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fail on token vault mint b does not match whirlpool token b", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const { poolInitInfo: anotherPoolInitInfo } = await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Stable + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: anotherPoolInitInfo.tokenVaultBKeypair.publicKey, // invalid + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fail on token owner account a does not match vault a mint", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountB } = await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const { tokenAccountA: anotherTokenAccountA } = await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Stable + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: anotherTokenAccountA, // invalid + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + + it("fail on token owner account b does not match vault b mint", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA } = await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const { tokenAccountB: anotherTokenAccountB } = await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Stable + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: anotherTokenAccountB, // invalid + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + + it("fails to swap with incorrect token authority", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + + const otherTokenAuthority = web3.Keypair.generate(); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: otherTokenAuthority.publicKey, // invalid + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ) + .addSigner(otherTokenAuthority) + .buildAndExecute(), + /0x4/ // OwnerMismatch + ); + }); + + it("fails on passing in the wrong tick-array", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard, + MathUtil.toX64(new Decimal(0.0242).sqrt()) + ); // Negative Tick + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: PriceMath.tickIndexToSqrtPriceX64(-50000), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(), + /0x1787/ // InvalidTickArraySequence + ); + }); + + it("fails on passing in the wrong whirlpool", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const { poolInitInfo: anotherPoolInitInfo } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: anotherPoolInitInfo.whirlpoolPda.publicKey, // invalid + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress at token_mint_a (V1: 0x7d3 (ConstraaintRaw) at token_owner_account_a) + ); + }); + + it("fails on passing in the tick-arrays from another whirlpool", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const { poolInitInfo: anotherPoolInitInfo } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + anotherPoolInitInfo.whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, // invalid + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(), + /0x7d1/ // ConstraintHasOne + ); + }); + + it("fails on passing in an account of another type for the oracle", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: tickArrays[0].publicKey, // invalid + }) + ).buildAndExecute(), + /0x7d6/ // ConstraintSeeds + ); + }); + + it("fails on passing in an incorrectly hashed oracle PDA", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const { poolInitInfo: anotherPoolInitInfo } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + + const anotherOraclePda = PDAUtil.getOracle( + ctx.program.programId, + anotherPoolInitInfo.whirlpoolPda.publicKey + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: anotherOraclePda.publicKey, // invalid + }) + ).buildAndExecute(), + /0x7d6/ // ConstraintSeeds + ); + }); + + it("fail on passing in zero tradable amount", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 33792, + 3, + TickSpacing.Standard, + false + ); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(0), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(), + /0x1793/ // ZeroTradableAmount + ); + }); + + it("swaps across one tick array", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + const aToB = false; + await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, // to 33792 + 3, + TickSpacing.Standard, + aToB + ); + + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + + await fundPositionsV2(ctx, poolInitInfo, tokenAccountA, tokenAccountB, fundParams); + + const tokenVaultABefore = new anchor.BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey) + ); + const tokenVaultBBefore = new anchor.BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey) + ); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey; + const whirlpoolData = (await fetcher.getPool( + whirlpoolKey, + IGNORE_CACHE + )) as WhirlpoolData; + /* replaceed by swapQuoteWithParams to avoid using whirlpool client + const quote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + new BN(100000), + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + */ + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: false, + tokenAmount: new BN(100000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(false), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + false, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quote, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + assert.equal( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + tokenVaultABefore.sub(quote.estimatedAmountOut).toString() + ); + assert.equal( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + tokenVaultBBefore.add(quote.estimatedAmountIn).toString() + ); + }); + + it("swaps aToB across initialized tick with no movement", async () => { + const startingTick = 91720; + const tickSpacing = TickSpacing.Stable; + const startingTickArrayStartIndex = TickUtil.getStartTickIndex(startingTick, tickSpacing); + const aToB = true; + const startSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(startingTick); + const initialLiquidity = new anchor.BN(10_000_000); + const additionalLiquidity = new anchor.BN(2_000_000); + + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Stable, + startSqrtPrice + ); + await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + startingTickArrayStartIndex + TICK_ARRAY_SIZE * tickSpacing * 2, + 5, + TickSpacing.Stable, + aToB + ); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + const initialParams: FundedPositionV2Params[] = [ + { + liquidityAmount: initialLiquidity, + tickLowerIndex: startingTickArrayStartIndex + tickSpacing, + tickUpperIndex: + startingTickArrayStartIndex + TICK_ARRAY_SIZE * tickSpacing * 2 - tickSpacing, + }, + ]; + + await fundPositionsV2(ctx, poolInitInfo, tokenAccountA, tokenAccountB, initialParams); + + const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey; + let whirlpoolData = (await fetcher.getPool(whirlpoolKey, IGNORE_CACHE)) as WhirlpoolData; + + // Position covers the current price, so liquidity should be equal to the initial funded position + assert.ok(whirlpoolData.liquidity.eq(new anchor.BN(10_000_000))); + + const nextParams: FundedPositionV2Params[] = [ + { + liquidityAmount: additionalLiquidity, + tickLowerIndex: startingTick - tickSpacing * 2, + tickUpperIndex: startingTick, + }, + ]; + + await fundPositionsV2(ctx, poolInitInfo, tokenAccountA, tokenAccountB, nextParams); + + whirlpoolData = (await fetcher.getPool(whirlpoolKey, IGNORE_CACHE)) as WhirlpoolData; + // Whirlpool.currentTick is 91720, so the newly funded position's upper tick is not + // strictly less than 91720 so the liquidity is not added. + assert.ok(whirlpoolData.liquidity.eq(initialLiquidity)); + assert.ok(whirlpoolData.sqrtPrice.eq(startSqrtPrice)); + assert.equal(whirlpoolData.tickCurrentIndex, startingTick); + + /* replaced by swapQuoteWithParams to avoid using whirlpool client + const quote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + new BN(1), + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + */ + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: true, + tokenAmount: new BN(1), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(true), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + true, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quote, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + whirlpoolData = (await fetcher.getPool(whirlpoolKey, IGNORE_CACHE)) as WhirlpoolData; + + // After the above swap, since the amount is so low, it is completely taken by fees + // thus, the sqrt price will remain the same, the starting tick will decrement since it + // is an aToB swap ending on initialized tick, and since the tick is crossed, + // the liquidity will be added + assert.equal(whirlpoolData.tickCurrentIndex, startingTick - 1); + assert.ok(whirlpoolData.sqrtPrice.eq(startSqrtPrice)); + assert.ok(whirlpoolData.liquidity.eq(initialLiquidity.add(additionalLiquidity))); + + /* replaced by swapQuoteWithParams to avoid using whirlpool client + const quote2 = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + new BN(1), + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + */ + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: true, + tokenAmount: new BN(1), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(true), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + true, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quote2, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + whirlpoolData = (await fetcher.getPool(whirlpoolKey, IGNORE_CACHE)) as WhirlpoolData; + + // After the above swap, since the amount is so low, it is completely taken by fees + // thus, the sqrt price will remaing the same, the starting tick will not decrement + // since it is an aToB swap ending on an uninitialized tick, no tick is crossed + assert.equal(whirlpoolData.tickCurrentIndex, startingTick - 1); + assert.ok(whirlpoolData.sqrtPrice.eq(startSqrtPrice)); + assert.ok(whirlpoolData.liquidity.eq(initialLiquidity.add(additionalLiquidity))); + }); + + it("swaps aToB with small remainder across initialized tick", async () => { + const startingTick = 91728; + const tickSpacing = TickSpacing.Stable; + const startingTickArrayStartIndex = TickUtil.getStartTickIndex(startingTick, tickSpacing); + const aToB = true; + const startSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(startingTick); + const initialLiquidity = new anchor.BN(10_000_000); + const additionalLiquidity = new anchor.BN(2_000_000); + + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Stable, + startSqrtPrice + ); + await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + startingTickArrayStartIndex + TICK_ARRAY_SIZE * tickSpacing * 2, + 5, + TickSpacing.Stable, + aToB + ); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + const initialParams: FundedPositionV2Params[] = [ + { + liquidityAmount: initialLiquidity, + tickLowerIndex: startingTickArrayStartIndex + tickSpacing, + tickUpperIndex: + startingTickArrayStartIndex + TICK_ARRAY_SIZE * tickSpacing * 2 - tickSpacing, + }, + ]; + + await fundPositionsV2(ctx, poolInitInfo, tokenAccountA, tokenAccountB, initialParams); + + const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey; + let whirlpoolData = (await fetcher.getPool(whirlpoolKey, IGNORE_CACHE)) as WhirlpoolData; + + // Position covers the current price, so liquidity should be equal to the initial funded position + assert.ok(whirlpoolData.liquidity.eq(new anchor.BN(10_000_000))); + + const nextParams: FundedPositionV2Params[] = [ + { + liquidityAmount: additionalLiquidity, + tickLowerIndex: startingTick - tickSpacing * 3, + tickUpperIndex: startingTick - tickSpacing, + }, + ]; + + await fundPositionsV2(ctx, poolInitInfo, tokenAccountA, tokenAccountB, nextParams); + + whirlpoolData = (await fetcher.getPool(whirlpoolKey, IGNORE_CACHE)) as WhirlpoolData; + // Whirlpool.currentTick is 91720, so the newly funded position's upper tick is not + // strictly less than 91720 so the liquidity is not added. + assert.ok(whirlpoolData.liquidity.eq(initialLiquidity)); + assert.ok(whirlpoolData.sqrtPrice.eq(startSqrtPrice)); + assert.equal(whirlpoolData.tickCurrentIndex, startingTick); + + /* replaced by swapQuoteWithParams to avoid using whirlpool client + const quote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + new BN(1), + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + */ + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: true, + tokenAmount: new BN(1), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(true), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + true, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quote, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + whirlpoolData = (await fetcher.getPool(whirlpoolKey, IGNORE_CACHE)) as WhirlpoolData; + + // After the above swap, since the amount is so low, it is completely taken by fees + // thus, the sqrt price will remain the same, the starting tick will stay the same since it + // is an aToB swap ending on initialized tick and no tick is crossed + assert.equal(whirlpoolData.tickCurrentIndex, startingTick); + assert.ok(whirlpoolData.sqrtPrice.eq(startSqrtPrice)); + assert.ok(whirlpoolData.liquidity.eq(initialLiquidity)); + + /* replaced by swapQuoteWithParams to avoid using whirlpool client + const quote2 = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + new BN(43), + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + */ + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: true, + tokenAmount: new BN(43), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(true), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + true, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quote2, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + whirlpoolData = (await fetcher.getPool(whirlpoolKey, IGNORE_CACHE)) as WhirlpoolData; + + // After the above swap, there will be a small amount remaining that crosses + // an initialized tick index, but isn't enough to move the sqrt price. + assert.equal(whirlpoolData.tickCurrentIndex, startingTick - tickSpacing - 1); + assert.ok( + whirlpoolData.sqrtPrice.eq( + PriceMath.tickIndexToSqrtPriceX64(startingTick - tickSpacing) + ) + ); + assert.ok(whirlpoolData.liquidity.eq(initialLiquidity.add(additionalLiquidity))); + }); + + it("swaps across three tick arrays", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Stable, + PriceMath.tickIndexToSqrtPriceX64(27500) + ); + + const aToB = false; + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 27456, // to 28160, 28864 + 5, + TickSpacing.Stable, + false + ); + + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(100_000_000), + tickLowerIndex: 27456, + tickUpperIndex: 27840, + }, + { + liquidityAmount: new anchor.BN(100_000_000), + tickLowerIndex: 28864, + tickUpperIndex: 28928, + }, + { + liquidityAmount: new anchor.BN(100_000_000), + tickLowerIndex: 27712, + tickUpperIndex: 28928, + }, + ]; + + await fundPositionsV2(ctx, poolInitInfo, tokenAccountA, tokenAccountB, fundParams); + + assert.equal( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + "1977429" + ); + assert.equal( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + "869058" + ); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + // Tick + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(7051000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: PriceMath.tickIndexToSqrtPriceX64(28500), + amountSpecifiedIsInput: true, + aToB: aToB, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[1].publicKey, + tickArray2: tickArrays[2].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + assert.equal( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + "1535201" + ); + assert.equal( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + "7920058" + ); + + // TODO: Verify fees and other whirlpool params + }); + + it("Error on passing in uninitialized tick-array", async () => { + const { poolInitInfo, tokenAccountA, tokenAccountB, tickArrays } = + await initTestPoolWithLiquidityV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB + ); + const whirlpool = poolInitInfo.whirlpoolPda.publicKey; + + const uninitializedTickArrayPda = PDAUtil.getTickArray( + ctx.program.programId, + whirlpool, + 0 + ); + + const oraclePda = PDAUtil.getOracle( + ctx.program.programId, + poolInitInfo.whirlpoolPda.publicKey + ); + + const params: SwapV2Params = { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4294886578)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpool, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: uninitializedTickArrayPda.publicKey, + tickArray2: tickArrays[2].publicKey, + oracle: oraclePda.publicKey, + }; + + try { + await toTx(ctx, WhirlpoolIx.swapV2Ix(ctx.program, params)).buildAndExecute(); + assert.fail("should fail if a tick-array is uninitialized"); + } catch (e) { + const error = e as Error; + assert.match(error.message, /0xbbf/); // AccountOwnedByWrongProgram + } + }); + + it("Error if sqrt_price_limit exceeds max", async () => { + const { poolInitInfo, tokenAccountA, tokenAccountB, tickArrays } = + await initTestPoolWithLiquidityV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB + ); + const whirlpool = poolInitInfo.whirlpoolPda.publicKey; + + const oraclePda = PDAUtil.getOracle( + ctx.program.programId, + poolInitInfo.whirlpoolPda.publicKey + ); + + const params: SwapV2Params = { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: new anchor.BN(MAX_SQRT_PRICE).add(new anchor.BN(1)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpool, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[1].publicKey, + tickArray2: tickArrays[2].publicKey, + oracle: oraclePda.publicKey, + }; + + try { + await toTx(ctx, WhirlpoolIx.swapV2Ix(ctx.program, params)).buildAndExecute(); + assert.fail("should fail if sqrt_price exceeds maximum"); + } catch (e) { + const error = e as Error; + assert.match(error.message, /0x177b/); // SqrtPriceOutOfBounds + } + }); + + it("Error if sqrt_price_limit subceed min", async () => { + const { poolInitInfo, tokenAccountA, tokenAccountB, tickArrays } = + await initTestPoolWithLiquidityV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB + ); + const whirlpool = poolInitInfo.whirlpoolPda.publicKey; + + const oraclePda = PDAUtil.getOracle( + ctx.program.programId, + poolInitInfo.whirlpoolPda.publicKey + ); + + const params: SwapV2Params = { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: new anchor.BN(MIN_SQRT_PRICE).sub(new anchor.BN(1)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpool, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[1].publicKey, + tickArray2: tickArrays[2].publicKey, + oracle: oraclePda.publicKey, + }; + + try { + await toTx(ctx, WhirlpoolIx.swapV2Ix(ctx.program, params)).buildAndExecute(); + assert.fail("should fail if sqrt_price subceeds minimum"); + } catch (e) { + const error = e as Error; + assert.match(error.message, /0x177b/); // SqrtPriceOutOfBounds + } + }); + + it("Error if a to b swap below minimum output", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, // to 33792 + 3, + TickSpacing.Standard, + false + ); + + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(100_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + + await fundPositionsV2(ctx, poolInitInfo, tokenAccountA, tokenAccountB, fundParams); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + const params = { + amount: new BN(10), + otherAmountThreshold: MAX_U64, + sqrtPriceLimit: new anchor.BN(MIN_SQRT_PRICE), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }; + + try { + await toTx(ctx, WhirlpoolIx.swapV2Ix(ctx.program, params)).buildAndExecute(); + assert.fail("should fail if amount out is below threshold"); + } catch (e) { + const error = e as Error; + assert.match(error.message, /0x1794/); // AmountOutBelowMinimum + } + }); + + it("Error if b to a swap below minimum output", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, // to 33792 + 3, + TickSpacing.Standard, + false + ); + + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(100_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + + await fundPositionsV2(ctx, poolInitInfo, tokenAccountA, tokenAccountB, fundParams); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + const params: SwapV2Params = { + amount: new BN(10), + otherAmountThreshold: MAX_U64, + sqrtPriceLimit: new anchor.BN(MAX_SQRT_PRICE), + amountSpecifiedIsInput: true, + aToB: false, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }; + + try { + await toTx(ctx, WhirlpoolIx.swapV2Ix(ctx.program, params)).buildAndExecute(); + assert.fail("should fail if amount out is below threshold"); + } catch (e) { + const error = e as Error; + assert.match(error.message, /0x1794/); // AmountOutBelowMinimum + } + }); + + it("Error if a to b swap above maximum input", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, // to 33792 + 3, + TickSpacing.Standard, + false + ); + + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(100_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + + await fundPositionsV2(ctx, poolInitInfo, tokenAccountA, tokenAccountB, fundParams); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + const params: SwapV2Params = { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: new anchor.BN(MIN_SQRT_PRICE), + amountSpecifiedIsInput: false, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }; + + try { + await toTx(ctx, WhirlpoolIx.swapV2Ix(ctx.program, params)).buildAndExecute(); + assert.fail("should fail if amount out is below threshold"); + } catch (e) { + const error = e as Error; + assert.match(error.message, /0x1795/); // AmountInAboveMaximum + } + }); + + it("Error if b to a swap below maximum input", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, // to 33792 + 3, + TickSpacing.Standard, + false + ); + + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(100_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + + await fundPositionsV2(ctx, poolInitInfo, tokenAccountA, tokenAccountB, fundParams); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + const params: SwapV2Params = { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: new anchor.BN(MAX_SQRT_PRICE), + amountSpecifiedIsInput: false, + aToB: false, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }; + + try { + await toTx(ctx, WhirlpoolIx.swapV2Ix(ctx.program, params)).buildAndExecute(); + assert.fail("should fail if amount out is below threshold"); + } catch (e) { + const error = e as Error; + assert.match(error.message, /0x1795/); // AmountInAboveMaximum + } + }); + + it("swaps across ten tick arrays", async () => { + const { + poolInitInfo, + configInitInfo, + configKeypairs, + whirlpoolPda, + tokenAccountA, + tokenAccountB, + } = await initTestPoolWithTokensV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Stable, + PriceMath.tickIndexToSqrtPriceX64(27500) + ); + + const aToB = false; + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 27456, // to 30528 + 3, + TickSpacing.Stable, + aToB + ); + + // tick array range: 27658 to 29386 + // tick arrays: (27456, 28152), (28160, 28856), (28864, 29,560) + // current tick: 27727 + // initialized ticks: + // 27712, 27736, 27840, 28288, 28296, 28304, 28416, 28576, 28736, 29112, 29120, 29240, 29360 + + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 27712, + tickUpperIndex: 29360, + }, + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 27736, + tickUpperIndex: 29240, + }, + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 27840, + tickUpperIndex: 29120, + }, + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 28288, + tickUpperIndex: 29112, + }, + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 28416, + tickUpperIndex: 29112, + }, + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 28288, + tickUpperIndex: 28304, + }, + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 28296, + tickUpperIndex: 29112, + }, + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 28576, + tickUpperIndex: 28736, + }, + ]; + + const positionInfos = await fundPositionsV2( + ctx, + poolInitInfo, + tokenAccountA, + tokenAccountB, + fundParams + ); + + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey)); + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey)); + + ( + await Promise.all( + tickArrays.map((tickArray) => fetcher.getTickArray(tickArray.publicKey)) + ) + ).map((tickArray) => { + const ta = tickArray as TickArrayData; + ta.ticks.forEach((tick, index) => { + if (!tick.initialized) { + return; + } + + /* + console.log( + ta.startTickIndex + index * TickSpacing.Stable, + tick.feeGrowthOutsideA.toString(), + tick.feeGrowthOutsideB.toString() + ); + */ + }); + }); + + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + // Tick + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(829996), + otherAmountThreshold: MAX_U64, + sqrtPriceLimit: PriceMath.tickIndexToSqrtPriceX64(29240), + amountSpecifiedIsInput: false, + aToB, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[1].publicKey, + tickArray2: tickArrays[2].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey)); + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey)); + + ( + await Promise.all( + tickArrays.map((tickArray) => fetcher.getTickArray(tickArray.publicKey)) + ) + ).map((tickArray) => { + const ta = tickArray as TickArrayData; + ta.ticks.forEach((tick, index) => { + if (!tick.initialized) { + return; + } + + /* + console.log( + ta.startTickIndex + index * TickSpacing.Stable, + tick.feeGrowthOutsideA.toString(), + tick.feeGrowthOutsideB.toString() + ); + */ + }); + }); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(14538074), + otherAmountThreshold: MAX_U64, + sqrtPriceLimit: PriceMath.tickIndexToSqrtPriceX64(27712), + amountSpecifiedIsInput: false, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[2].publicKey, + tickArray1: tickArrays[1].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey)); + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey)); + + ( + await Promise.all( + tickArrays.map((tickArray) => fetcher.getTickArray(tickArray.publicKey)) + ) + ).map((tickArray) => { + const ta = tickArray as TickArrayData; + ta.ticks.forEach((tick, index) => { + if (!tick.initialized) { + return; + } + + /* + console.log( + ta.startTickIndex + index * TickSpacing.Stable, + tick.feeGrowthOutsideA.toString(), + tick.feeGrowthOutsideB.toString() + ); + */ + }); + }); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(829996), + otherAmountThreshold: MAX_U64, + sqrtPriceLimit: PriceMath.tickIndexToSqrtPriceX64(29240), + amountSpecifiedIsInput: false, + aToB, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[1].publicKey, + tickArray2: tickArrays[2].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey)); + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey)); + + ( + await Promise.all( + tickArrays.map((tickArray) => fetcher.getTickArray(tickArray.publicKey)) + ) + ).map((tickArray) => { + const ta = tickArray as TickArrayData; + ta.ticks.forEach((tick, index) => { + if (!tick.initialized) { + return; + } + + /* + console.log( + ta.startTickIndex + index * TickSpacing.Stable, + tick.feeGrowthOutsideA.toString(), + tick.feeGrowthOutsideB.toString() + ); + */ + }); + }); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(14538074), + otherAmountThreshold: MAX_U64, + sqrtPriceLimit: PriceMath.tickIndexToSqrtPriceX64(27712), + amountSpecifiedIsInput: false, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[2].publicKey, + tickArray1: tickArrays[1].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey)); + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey)); + + ( + await Promise.all( + tickArrays.map((tickArray) => fetcher.getTickArray(tickArray.publicKey)) + ) + ).map((tickArray) => { + const ta = tickArray as TickArrayData; + ta.ticks.forEach((tick, index) => { + if (!tick.initialized) { + return; + } + + /* + console.log( + ta.startTickIndex + index * TickSpacing.Stable, + tick.feeGrowthOutsideA.toString(), + tick.feeGrowthOutsideB.toString() + ); + */ + }); + }); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(829996), + otherAmountThreshold: MAX_U64, + sqrtPriceLimit: PriceMath.tickIndexToSqrtPriceX64(29240), + amountSpecifiedIsInput: false, + aToB, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[1].publicKey, + tickArray2: tickArrays[2].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey)); + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey)); + + ( + await Promise.all( + tickArrays.map((tickArray) => fetcher.getTickArray(tickArray.publicKey)) + ) + ).map((tickArray) => { + const ta = tickArray as TickArrayData; + ta.ticks.forEach((tick, index) => { + if (!tick.initialized) { + return; + } + + /* + console.log( + ta.startTickIndex + index * TickSpacing.Stable, + tick.feeGrowthOutsideA.toString(), + tick.feeGrowthOutsideB.toString() + ); + */ + }); + }); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(14538074), + otherAmountThreshold: MAX_U64, + sqrtPriceLimit: PriceMath.tickIndexToSqrtPriceX64(27712), + amountSpecifiedIsInput: false, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[2].publicKey, + tickArray1: tickArrays[1].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey)); + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey)); + + ( + await Promise.all( + tickArrays.map((tickArray) => fetcher.getTickArray(tickArray.publicKey)) + ) + ).map((tickArray) => { + const ta = tickArray as TickArrayData; + ta.ticks.forEach((tick, index) => { + if (!tick.initialized) { + return; + } + + /* + console.log( + ta.startTickIndex + index * TickSpacing.Stable, + tick.feeGrowthOutsideA.toString(), + tick.feeGrowthOutsideB.toString() + ); + */ + }); + }); + + await withdrawPositionsV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + positionInfos, + tokenAccountA, + tokenAccountB + ); + + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey)); + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey)); + + ( + await Promise.all( + tickArrays.map((tickArray) => fetcher.getTickArray(tickArray.publicKey)) + ) + ).map((tickArray) => { + const ta = tickArray as TickArrayData; + ta.ticks.forEach((tick, index) => { + if (!tick.initialized) { + return; + } + + /* + console.log( + ta.startTickIndex + index * TickSpacing.Stable, + tick.feeGrowthOutsideA.toString(), + tick.feeGrowthOutsideB.toString() + ); + */ + }); + }); + + await toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: poolInitInfo.whirlpoolsConfig, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + collectProtocolFeesAuthority: + configKeypairs.collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + }) + ) + .addSigner(configKeypairs.collectProtocolFeesAuthorityKeypair) + .buildAndExecute(); + + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey)); + //console.log(await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey)); + }); + }); + }); + }); + + describe("v2 specific accounts", () => { + it("fails when passed token_mint_a does not match whirlpool's token_mint_a", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + { isToken2022: true }, + { isToken2022: true }, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + const otherTokenPublicKey = await createMintV2(provider, { isToken2022: true }); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: otherTokenPublicKey, // invalid + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_mint_b does not match whirlpool's token_mint_b", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + { isToken2022: true }, + { isToken2022: true }, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + const otherTokenPublicKey = await createMintV2(provider, { isToken2022: true }); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: otherTokenPublicKey, // invalid + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_a is not token program (token-2022 is passed)", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + { isToken2022: false }, + { isToken2022: false }, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + assert.ok(poolInitInfo.tokenProgramA.equals(TEST_TOKEN_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: TEST_TOKEN_2022_PROGRAM_ID, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_a is not token-2022 program (token is passed)", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + { isToken2022: true }, + { isToken2022: true }, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + assert.ok(poolInitInfo.tokenProgramA.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: TEST_TOKEN_PROGRAM_ID, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_a is token_metadata", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + { isToken2022: true }, + { isToken2022: true }, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + assert.ok(poolInitInfo.tokenProgramA.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: METADATA_PROGRAM_ADDRESS, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("fails when passed token_program_b is not token program (token-2022 is passed)", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + { isToken2022: false }, + { isToken2022: false }, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + assert.ok(poolInitInfo.tokenProgramB.equals(TEST_TOKEN_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: TEST_TOKEN_2022_PROGRAM_ID, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_b is not token-2022 program (token is passed)", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + { isToken2022: true }, + { isToken2022: true }, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + assert.ok(poolInitInfo.tokenProgramB.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: TEST_TOKEN_PROGRAM_ID, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails when passed token_program_b is token_metadata", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + { isToken2022: true }, + { isToken2022: true }, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + assert.ok(poolInitInfo.tokenProgramB.equals(TEST_TOKEN_2022_PROGRAM_ID)); + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(10), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: METADATA_PROGRAM_ADDRESS, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("fails when passed memo_program is token_metadata", async () => { + const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + ctx, + { isToken2022: true }, + { isToken2022: true }, + TickSpacing.Standard + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, + 3, + TickSpacing.Standard, + false + ); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + const invalidMemoProgram = METADATA_PROGRAM_ADDRESS; + + await assert.rejects( + toTx(ctx, { + cleanupInstructions: [], + signers: [], + instructions: [ + ctx.program.instruction.swapV2( + new BN(10), // amount + ZERO_BN, // otherAmountThreshold + MathUtil.toX64(new Decimal(4.95)), // sqrtPriceLimit + true, // amountSpecifiedIsInput + true, // aToB + { slices: [] }, + { + accounts: { + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrays[0].publicKey, + tickArray1: tickArrays[0].publicKey, + tickArray2: tickArrays[0].publicKey, + oracle: oraclePda.publicKey, + memoProgram: invalidMemoProgram, + }, + } + ), + ], + }).buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + }); +}); diff --git a/sdk/tests/integration/v2/token-badge/delete_token_badge.test.ts b/sdk/tests/integration/v2/token-badge/delete_token_badge.test.ts new file mode 100644 index 000000000..168a66b3d --- /dev/null +++ b/sdk/tests/integration/v2/token-badge/delete_token_badge.test.ts @@ -0,0 +1,453 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Keypair, LAMPORTS_PER_SOL, PublicKey, TransactionInstruction } from "@solana/web3.js"; +import * as assert from "assert"; +import { + IGNORE_CACHE, + PDAUtil, + toTx, + WhirlpoolContext, + WhirlpoolIx +} from "../../../../src"; +import { defaultConfirmOptions } from "../../../utils/const"; +import { DeleteTokenBadgeParams, InitializeTokenBadgeParams } from "../../../../src/instructions"; +import { createMintV2 } from "../../../utils/v2/token-2022"; +import { TokenTrait } from "../../../utils/v2/init-utils-v2"; + +describe("delete_token_badge", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + + const collectProtocolFeesAuthorityKeypair = Keypair.generate(); + const feeAuthorityKeypair = Keypair.generate(); + const rewardEmissionsSuperAuthorityKeypair = Keypair.generate(); + const initialConfigExtensionAuthorityKeypair = feeAuthorityKeypair; + const initialTokenBadgeAuthorityKeypair = feeAuthorityKeypair; + const updatedTokenBadgeAuthorityKeypair = Keypair.generate(); + + async function createOtherWallet(): Promise { + const keypair = Keypair.generate(); + const signature = await provider.connection.requestAirdrop(keypair.publicKey, 100 * LAMPORTS_PER_SOL); + await provider.connection.confirmTransaction(signature, "confirmed"); + return keypair; + } + + async function initializeWhirlpoolsConfig(configKeypair: Keypair) { + return toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, { + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + feeAuthority: feeAuthorityKeypair.publicKey, + rewardEmissionsSuperAuthority: rewardEmissionsSuperAuthorityKeypair.publicKey, + defaultProtocolFeeRate: 300, + funder: provider.wallet.publicKey, + whirlpoolsConfigKeypair: configKeypair, + })).addSigner(configKeypair).buildAndExecute(); + } + + async function initializeWhirlpoolsConfigExtension(config: PublicKey) { + const pda = PDAUtil.getConfigExtension(ctx.program.programId, config); + return toTx(ctx, WhirlpoolIx.initializeConfigExtensionIx(ctx.program, { + feeAuthority: feeAuthorityKeypair.publicKey, + funder: provider.wallet.publicKey, + whirlpoolsConfig: config, + whirlpoolsConfigExtensionPda: pda, + })).addSigner(feeAuthorityKeypair).buildAndExecute(); + } + + async function initializeTokenBadge(config: PublicKey, mint: PublicKey, overwrite: Partial, signers: Keypair[] = [initialTokenBadgeAuthorityKeypair]) { + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, config).publicKey; + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, config, mint); + const tx = toTx(ctx, WhirlpoolIx.initializeTokenBadgeIx(ctx.program, { + whirlpoolsConfig: config, + whirlpoolsConfigExtension, + funder: provider.wallet.publicKey, + tokenBadgeAuthority: initialTokenBadgeAuthorityKeypair.publicKey, + tokenBadgePda, + tokenMint: mint, + ...overwrite, + })); + signers.forEach((signer) => tx.addSigner(signer)); + return tx.buildAndExecute(); + } + + async function updateTokenBadgeAuthority(config: PublicKey, authority: Keypair, newAuthority: PublicKey) { + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, config).publicKey; + return toTx(ctx, WhirlpoolIx.setTokenBadgeAuthorityIx(ctx.program, { + whirlpoolsConfig: config, + whirlpoolsConfigExtension, + configExtensionAuthority: authority.publicKey, + newTokenBadgeAuthority: newAuthority, + })).addSigner(authority).buildAndExecute(); + } + + async function deleteTokenBadge(config: PublicKey, mint: PublicKey, overwrite: Partial, signers: Keypair[] = [initialTokenBadgeAuthorityKeypair]) { + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, config).publicKey; + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, config, mint); + const tx = toTx(ctx, WhirlpoolIx.deleteTokenBadgeIx(ctx.program, { + whirlpoolsConfig: config, + whirlpoolsConfigExtension, + tokenBadgeAuthority: initialTokenBadgeAuthorityKeypair.publicKey, + tokenMint: mint, + tokenBadge: tokenBadgePda.publicKey, + receiver: provider.wallet.publicKey, + ...overwrite, + })); + signers.forEach((signer) => tx.addSigner(signer)); + return tx.buildAndExecute(); + } + + describe("successfully delete token badge", () => { + const tokenTraits: TokenTrait[] = [{isToken2022: true}, {isToken2022: false}]; + + tokenTraits.forEach((tokenTrait) => { + it(`Mint TokenProgram: ${tokenTrait.isToken2022 ? "Token-2022" : "Token"}`, async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const mint = await createMintV2(provider, tokenTrait); + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, whirlpoolsConfigKeypair.publicKey, mint); + const tokenBadgeData = await fetcher.getTokenBadge(tokenBadgePda.publicKey, IGNORE_CACHE); + assert.ok(tokenBadgeData!.whirlpoolsConfig.equals(whirlpoolsConfigKeypair.publicKey)); + assert.ok(tokenBadgeData!.tokenMint.equals(mint)); + + const preBalance = await provider.connection.getBalance(provider.wallet.publicKey); + + await deleteTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + const tokenBadgeDataRemoved = await fetcher.getTokenBadge(tokenBadgePda.publicKey, IGNORE_CACHE); + assert.ok(tokenBadgeDataRemoved === null); + + const postBalance = await provider.connection.getBalance(provider.wallet.publicKey); + + // wallet paid network fee, but receive rent. so balance should be increased. + assert.ok(postBalance > preBalance); + }); + }); + }); + + it("successfully delete when receiver is different than account paying for transaction fee", async () => { + const otherWallet = await createOtherWallet(); + + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const mint = await createMintV2(provider, {isToken2022: true}); + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, whirlpoolsConfigKeypair.publicKey, mint); + const tokenBadgeData = await fetcher.getTokenBadge(tokenBadgePda.publicKey, IGNORE_CACHE); + assert.ok(tokenBadgeData!.whirlpoolsConfig.equals(whirlpoolsConfigKeypair.publicKey)); + assert.ok(tokenBadgeData!.tokenMint.equals(mint)); + + const preBalance = await provider.connection.getBalance(otherWallet.publicKey); + const rent = await provider.connection.getBalance(tokenBadgePda.publicKey); + + await deleteTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, { + receiver: otherWallet.publicKey, + }); + const tokenBadgeDataRemoved = await fetcher.getTokenBadge(tokenBadgePda.publicKey, IGNORE_CACHE); + assert.ok(tokenBadgeDataRemoved === null); + + const postBalance = await provider.connection.getBalance(otherWallet.publicKey); + + assert.equal(postBalance, preBalance + rent); + }); + + it("should be failed: already deleted", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const mint = await createMintV2(provider, {isToken2022: true}); + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, whirlpoolsConfigKeypair.publicKey, mint); + const tokenBadgeData = await fetcher.getTokenBadge(tokenBadgePda.publicKey, IGNORE_CACHE); + assert.ok(tokenBadgeData!.whirlpoolsConfig.equals(whirlpoolsConfigKeypair.publicKey)); + assert.ok(tokenBadgeData!.tokenMint.equals(mint)); + + await deleteTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + const tokenBadgeDataRemoved = await fetcher.getTokenBadge(tokenBadgePda.publicKey, IGNORE_CACHE); + assert.ok(tokenBadgeDataRemoved === null); + + // delete again + await assert.rejects( + deleteTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}), + /0xbc4/ // AccountNotInitialized + ); + }); + + describe("invalid input account", () => { + it("should be failed: invalid whirlpools_config", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const mint = await createMintV2(provider, {isToken2022: true}); + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + + // config not initialized + const anotherWhirlpoolsConfigKeypair = Keypair.generate(); + await assert.rejects( + deleteTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mint, { + whirlpoolsConfig: anotherWhirlpoolsConfigKeypair.publicKey, + } + ), + /0xbc4/ // AccountNotInitialized + ); + + // config initialized, but not match to whirlpools_config_extension + await initializeWhirlpoolsConfig(anotherWhirlpoolsConfigKeypair); + await assert.rejects( + deleteTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mint, { + whirlpoolsConfig: anotherWhirlpoolsConfigKeypair.publicKey, + } + ), + /0x7d1/ // ConstraintHasOne + ); + }); + + it("should be failed: invalid whirlpools_config_extension", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const mint = await createMintV2(provider, {isToken2022: true}); + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + + const anotherWhirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(anotherWhirlpoolsConfigKeypair); + + // config_extension not initialized + await assert.rejects( + deleteTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mint, { + whirlpoolsConfigExtension: PDAUtil.getConfigExtension(ctx.program.programId, anotherWhirlpoolsConfigKeypair.publicKey).publicKey, + } + ), + /0xbc4/ // AccountNotInitialized + ); + + // initialized, but fake config_extension + await initializeWhirlpoolsConfigExtension(anotherWhirlpoolsConfigKeypair.publicKey); + await assert.rejects( + deleteTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mint, { + whirlpoolsConfigExtension: PDAUtil.getConfigExtension(ctx.program.programId, anotherWhirlpoolsConfigKeypair.publicKey).publicKey, + } + ), + /0x7d1/ // ConstraintHasOne + ); + }); + + it("should be failed: invalid token_badge_authority", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const mint = await createMintV2(provider, {isToken2022: true}); + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + + const fakeAuthority = Keypair.generate(); + await assert.rejects( + deleteTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mint, { + tokenBadgeAuthority: fakeAuthority.publicKey, + }, [ + fakeAuthority, + ] + ), + /0x7dc/ // ConstraintAddress + ); + }); + + + it("should be failed: config_extension_authority is passed as token_badge_authority", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + const mint = await createMintV2(provider, {isToken2022: true}); + + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + + // update authority from provider.wallet + await updateTokenBadgeAuthority(whirlpoolsConfigKeypair.publicKey, initialConfigExtensionAuthorityKeypair, updatedTokenBadgeAuthorityKeypair.publicKey); + const extension = await fetcher.getConfigExtension(whirlpoolsConfigExtension, IGNORE_CACHE); + assert.ok(extension?.tokenBadgeAuthority.equals(updatedTokenBadgeAuthorityKeypair.publicKey)); + + const fakeAuthority = initialConfigExtensionAuthorityKeypair; + await assert.rejects( + deleteTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mint, { + tokenBadgeAuthority: fakeAuthority.publicKey, + }, [ + fakeAuthority, + ] + ), + /0x7dc/ // ConstraintAddress + ); + }); + + it("should be failed: token_badge_authority is not signer", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const mint = await createMintV2(provider, {isToken2022: true}); + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + + // update authority from provider.wallet + await updateTokenBadgeAuthority(whirlpoolsConfigKeypair.publicKey, initialConfigExtensionAuthorityKeypair, updatedTokenBadgeAuthorityKeypair.publicKey); + const extension = await fetcher.getConfigExtension(whirlpoolsConfigExtension, IGNORE_CACHE); + assert.ok(extension?.tokenBadgeAuthority.equals(updatedTokenBadgeAuthorityKeypair.publicKey)); + + const ix: TransactionInstruction = program.instruction.deleteTokenBadge({ + accounts: { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension, + tokenBadgeAuthority: updatedTokenBadgeAuthorityKeypair.publicKey, + tokenMint: mint, + tokenBadge: PDAUtil.getTokenBadge(ctx.program.programId, whirlpoolsConfigKeypair.publicKey, mint).publicKey, + receiver: ctx.wallet.publicKey, + }, + }) + + assert.equal(ix.keys.length, 6); + assert.ok(ix.keys[2].pubkey.equals(updatedTokenBadgeAuthorityKeypair.publicKey)); + + // unset signer flag + ix.keys[2].isSigner = false; + + const tx = toTx(ctx, { + instructions: [ix], + cleanupInstructions: [], + signers: [], // no updatedTokenBadgeAuthorityKeypair + }) + + await assert.rejects( + tx.buildAndExecute(), + /0xbc2/ // AccountNotSigner + ); + }); + + it("should be failed: invalid token_mint", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const mint = await createMintV2(provider, {isToken2022: true}); + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + + // mint is not uninitialized + const uninitializedMint = Keypair.generate().publicKey; + await assert.rejects( + deleteTokenBadge( + whirlpoolsConfigKeypair.publicKey, + uninitializedMint, + {}, + ), + /0xbc4/ // AccountNotInitialized + ); + + // different mint + const anotherMint = await createMintV2(provider, {isToken2022: true}); + await assert.rejects( + initializeTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mint, + { + tokenMint: anotherMint, + }, + ), + /0x7d6/ // ConstraintSeeds (token_badge (PDA) is not valid) + ); + }); + + it("should be failed: invalid token_badge", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const mint = await createMintV2(provider, {isToken2022: true}); + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + + // different mint (PDA not initialized) + const anotherMint = await createMintV2(provider, {isToken2022: true}); + const pdaForAnotherMint = PDAUtil.getTokenBadge( + ctx.program.programId, + whirlpoolsConfigKeypair.publicKey, + anotherMint, + ); + await assert.rejects( + deleteTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mint, + { + tokenBadge: pdaForAnotherMint.publicKey, + }, + ), + /0xbc4/ // AccountNotInitialized + ); + + // different mint (PDA initialized) + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, anotherMint, {}); + await assert.rejects( + deleteTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mint, + { + tokenBadge: pdaForAnotherMint.publicKey, + }, + ), + /0x7d6/ // ConstraintSeeds (token_badge (PDA) is not valid) + ); + }); + }); + + describe("lifecycle", () => { + it("initialize / delete / (re)initialize / (re)delete", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const mint = await createMintV2(provider, {isToken2022: true}); + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, whirlpoolsConfigKeypair.publicKey, mint); + + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + const tokenBadgeData1 = await fetcher.getTokenBadge(tokenBadgePda.publicKey, IGNORE_CACHE); + assert.ok(tokenBadgeData1!.whirlpoolsConfig.equals(whirlpoolsConfigKeypair.publicKey)); + assert.ok(tokenBadgeData1!.tokenMint.equals(mint)); + + await deleteTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + const tokenBadgeDataRemoved1 = await fetcher.getTokenBadge(tokenBadgePda.publicKey, IGNORE_CACHE); + assert.ok(tokenBadgeDataRemoved1 === null); + + // re-initialize + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + const tokenBadgeData2 = await fetcher.getTokenBadge(tokenBadgePda.publicKey, IGNORE_CACHE); + assert.ok(tokenBadgeData2!.whirlpoolsConfig.equals(whirlpoolsConfigKeypair.publicKey)); + assert.ok(tokenBadgeData2!.tokenMint.equals(mint)); + + // re-delete + await deleteTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + const tokenBadgeDataRemoved2 = await fetcher.getTokenBadge(tokenBadgePda.publicKey, IGNORE_CACHE); + assert.ok(tokenBadgeDataRemoved2 === null); + }); + }); +}); diff --git a/sdk/tests/integration/v2/token-badge/initialize_config_extension.test.ts b/sdk/tests/integration/v2/token-badge/initialize_config_extension.test.ts new file mode 100644 index 000000000..7a909cdae --- /dev/null +++ b/sdk/tests/integration/v2/token-badge/initialize_config_extension.test.ts @@ -0,0 +1,256 @@ +import * as anchor from "@coral-xyz/anchor"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram, TransactionInstruction } from "@solana/web3.js"; +import * as assert from "assert"; +import { + PDAUtil, + toTx, + WhirlpoolContext, + WhirlpoolIx +} from "../../../../src"; +import { defaultConfirmOptions } from "../../../utils/const"; +import { InitConfigExtensionParams } from "../../../../src/instructions"; + +describe("initialize_config_extension", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + + const collectProtocolFeesAuthorityKeypair = Keypair.generate(); + const feeAuthorityKeypair = Keypair.generate(); + const rewardEmissionsSuperAuthorityKeypair = Keypair.generate(); + + async function createOtherWallet(): Promise { + const keypair = Keypair.generate(); + const signature = await provider.connection.requestAirdrop(keypair.publicKey, 100 * LAMPORTS_PER_SOL); + await provider.connection.confirmTransaction(signature, "confirmed"); + return keypair; + } + + async function initializeWhirlpoolsConfig(configKeypair: Keypair) { + return toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, { + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + feeAuthority: feeAuthorityKeypair.publicKey, + rewardEmissionsSuperAuthority: rewardEmissionsSuperAuthorityKeypair.publicKey, + defaultProtocolFeeRate: 300, + funder: provider.wallet.publicKey, + whirlpoolsConfigKeypair: configKeypair, + })).addSigner(configKeypair).buildAndExecute(); + } + + async function initializeWhirlpoolsConfigExtension(config: PublicKey, overwrite: Partial, signers: Keypair[] = [feeAuthorityKeypair]) { + const pda = PDAUtil.getConfigExtension(ctx.program.programId, config); + const tx = toTx(ctx, WhirlpoolIx.initializeConfigExtensionIx(ctx.program, { + feeAuthority: feeAuthorityKeypair.publicKey, + funder: provider.wallet.publicKey, + whirlpoolsConfig: config, + whirlpoolsConfigExtensionPda: pda, + ...overwrite, + })); + signers.forEach((signer) => tx.addSigner(signer)); + return tx.buildAndExecute(); + } + + it("successfully initialize config extension and verify initialized account contents", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + + const configExtensionPubkey = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + await initializeWhirlpoolsConfigExtension( + whirlpoolsConfigKeypair.publicKey, + {} + ); + + const configExtension = await fetcher.getConfigExtension(configExtensionPubkey); + + assert.ok(configExtension!.whirlpoolsConfig.equals(whirlpoolsConfigKeypair.publicKey)); + assert.ok(configExtension!.configExtensionAuthority.equals(feeAuthorityKeypair.publicKey)); + assert.ok(configExtension!.tokenBadgeAuthority.equals(feeAuthorityKeypair.publicKey)); + }); + + it("successfully initialize when funder is different than account paying for transaction fee", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + + const preBalance = await ctx.connection.getBalance(ctx.wallet.publicKey); + const otherWallet = await createOtherWallet(); + + const configExtensionPubkey = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + await initializeWhirlpoolsConfigExtension( + whirlpoolsConfigKeypair.publicKey, + { + funder: otherWallet.publicKey, + }, + [ + feeAuthorityKeypair, + otherWallet, + ] + ); + + const postBalance = await ctx.connection.getBalance(ctx.wallet.publicKey); + const diffBalance = preBalance - postBalance; + const minRent = await ctx.connection.getMinimumBalanceForRentExemption(0); + assert.ok(diffBalance < minRent); // ctx.wallet didn't pay any rent + + const configExtension = await fetcher.getConfigExtension(configExtensionPubkey); + + assert.ok(configExtension!.whirlpoolsConfig.equals(whirlpoolsConfigKeypair.publicKey)); + assert.ok(configExtension!.configExtensionAuthority.equals(feeAuthorityKeypair.publicKey)); + assert.ok(configExtension!.tokenBadgeAuthority.equals(feeAuthorityKeypair.publicKey)); + }); + + it("WhirlpoolsConfigExtension account has reserved space", async () => { + const whirlpoolsConfigExtensionAccountSizeIncludingReserve = 8 + 32 + 32 + 32 + 512; + + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + + const configExtensionPubkey = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + await initializeWhirlpoolsConfigExtension( + whirlpoolsConfigKeypair.publicKey, + {} + ); + + const account = await ctx.connection.getAccountInfo(configExtensionPubkey, "confirmed"); + assert.equal(account!.data.length, whirlpoolsConfigExtensionAccountSizeIncludingReserve); + }); + + it("should be failed: already initialized", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + + const configExtensionPubkey = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + await initializeWhirlpoolsConfigExtension( + whirlpoolsConfigKeypair.publicKey, + {} + ); + + // initialized + const configExtension = await fetcher.getConfigExtension(configExtensionPubkey); + assert.ok(configExtension!.whirlpoolsConfig.equals(whirlpoolsConfigKeypair.publicKey)); + + // re-initialize + await assert.rejects( + initializeWhirlpoolsConfigExtension( + whirlpoolsConfigKeypair.publicKey, + {} + ), + (err) => { return JSON.stringify(err).includes("already in use") } + ); + }); + + describe("invalid input account", () => { + it("should be failed: invalid config", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + + // config not initialized + + await assert.rejects( + initializeWhirlpoolsConfigExtension( + whirlpoolsConfigKeypair.publicKey, + {} + ), + /0xbc4/ // AccountNotInitialized + ); + }); + + it("should be failed: invalid config_extension address", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + + const invalidPda = PDAUtil.getFeeTier(ctx.program.programId, whirlpoolsConfigKeypair.publicKey, 64); + await assert.rejects( + initializeWhirlpoolsConfigExtension( + whirlpoolsConfigKeypair.publicKey, + { + whirlpoolsConfigExtensionPda: invalidPda, + } + ), + /0x7d6/ // ConstraintSeeds + ); + }); + + it("should be failed: funder is not signer", async () => { + const otherWallet = await createOtherWallet(); + + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + + const whirlpoolsConfigExtensionPda = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey); + const ix: TransactionInstruction = program.instruction.initializeConfigExtension({ + accounts: { + config: whirlpoolsConfigKeypair.publicKey, + configExtension: whirlpoolsConfigExtensionPda.publicKey, + funder: otherWallet.publicKey, + feeAuthority: feeAuthorityKeypair.publicKey, + systemProgram: SystemProgram.programId, + }, + }) + + assert.equal(ix.keys.length, 5); + assert.ok(ix.keys[2].pubkey.equals(otherWallet.publicKey)); + + // unset signer flag + ix.keys[2].isSigner = false; + + const tx = toTx(ctx, { + instructions: [ix], + cleanupInstructions: [], + signers: [feeAuthorityKeypair], // no otherWallet + }) + + await assert.rejects( + tx.buildAndExecute(), + /0xbc2/ // AccountNotSigner + ); + }); + + it("should be failed: invalid fee_authority", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + + const invalidAuthorityKeypair = Keypair.generate(); + await assert.rejects( + initializeWhirlpoolsConfigExtension( + whirlpoolsConfigKeypair.publicKey, + { + feeAuthority: invalidAuthorityKeypair.publicKey, + }, + [invalidAuthorityKeypair], + ), + /0x7dc/ // ConstraintAddress + ); + }); + + it("should be failed: invalid system program", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + + const invalidSystemProgram = TOKEN_PROGRAM_ID; + + const whirlpoolsConfigExtensionPda = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey); + const ix: TransactionInstruction = program.instruction.initializeConfigExtension({ + accounts: { + config: whirlpoolsConfigKeypair.publicKey, + configExtension: whirlpoolsConfigExtensionPda.publicKey, + funder: ctx.wallet.publicKey, + feeAuthority: feeAuthorityKeypair.publicKey, + systemProgram: invalidSystemProgram, + }, + }) + + const tx = toTx(ctx, { + instructions: [ix], + cleanupInstructions: [], + signers: [feeAuthorityKeypair], + }) + + await assert.rejects( + tx.buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + }); +}); diff --git a/sdk/tests/integration/v2/token-badge/initialize_token_badge.test.ts b/sdk/tests/integration/v2/token-badge/initialize_token_badge.test.ts new file mode 100644 index 000000000..63e3d58e8 --- /dev/null +++ b/sdk/tests/integration/v2/token-badge/initialize_token_badge.test.ts @@ -0,0 +1,465 @@ +import * as anchor from "@coral-xyz/anchor"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram, TransactionInstruction } from "@solana/web3.js"; +import * as assert from "assert"; +import { + IGNORE_CACHE, + PDAUtil, + toTx, + WhirlpoolContext, + WhirlpoolIx +} from "../../../../src"; +import { defaultConfirmOptions } from "../../../utils/const"; +import { InitializeTokenBadgeParams } from "../../../../src/instructions"; +import { createMintV2 } from "../../../utils/v2/token-2022"; +import { TokenTrait } from "../../../utils/v2/init-utils-v2"; + +describe("initialize_token_badge", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + + const collectProtocolFeesAuthorityKeypair = Keypair.generate(); + const feeAuthorityKeypair = Keypair.generate(); + const rewardEmissionsSuperAuthorityKeypair = Keypair.generate(); + const initialConfigExtensionAuthorityKeypair = feeAuthorityKeypair; + const initialTokenBadgeAuthorityKeypair = feeAuthorityKeypair; + const updatedTokenBadgeAuthorityKeypair = Keypair.generate(); + + async function createOtherWallet(): Promise { + const keypair = Keypair.generate(); + const signature = await provider.connection.requestAirdrop(keypair.publicKey, 100 * LAMPORTS_PER_SOL); + await provider.connection.confirmTransaction(signature, "confirmed"); + return keypair; + } + + async function initializeWhirlpoolsConfig(configKeypair: Keypair) { + return toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, { + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + feeAuthority: feeAuthorityKeypair.publicKey, + rewardEmissionsSuperAuthority: rewardEmissionsSuperAuthorityKeypair.publicKey, + defaultProtocolFeeRate: 300, + funder: provider.wallet.publicKey, + whirlpoolsConfigKeypair: configKeypair, + })).addSigner(configKeypair).buildAndExecute(); + } + + async function initializeWhirlpoolsConfigExtension(config: PublicKey) { + const pda = PDAUtil.getConfigExtension(ctx.program.programId, config); + return toTx(ctx, WhirlpoolIx.initializeConfigExtensionIx(ctx.program, { + feeAuthority: feeAuthorityKeypair.publicKey, + funder: provider.wallet.publicKey, + whirlpoolsConfig: config, + whirlpoolsConfigExtensionPda: pda, + })).addSigner(feeAuthorityKeypair).buildAndExecute(); + } + + async function initializeTokenBadge(config: PublicKey, mint: PublicKey, overwrite: Partial, signers: Keypair[] = [initialTokenBadgeAuthorityKeypair]) { + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, config).publicKey; + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, config, mint); + const tx = toTx(ctx, WhirlpoolIx.initializeTokenBadgeIx(ctx.program, { + whirlpoolsConfig: config, + whirlpoolsConfigExtension, + funder: provider.wallet.publicKey, + tokenBadgeAuthority: initialTokenBadgeAuthorityKeypair.publicKey, + tokenBadgePda, + tokenMint: mint, + ...overwrite, + })); + signers.forEach((signer) => tx.addSigner(signer)); + return tx.buildAndExecute(); + } + + async function updateTokenBadgeAuthority(config: PublicKey, authority: Keypair, newAuthority: PublicKey) { + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, config).publicKey; + return toTx(ctx, WhirlpoolIx.setTokenBadgeAuthorityIx(ctx.program, { + whirlpoolsConfig: config, + whirlpoolsConfigExtension, + configExtensionAuthority: authority.publicKey, + newTokenBadgeAuthority: newAuthority, + })).addSigner(authority).buildAndExecute(); + } + + describe("successfully initialize token badge and verify initialized account contents", () => { + const tokenTraits: TokenTrait[] = [{isToken2022: true}, {isToken2022: false}]; + + tokenTraits.forEach((tokenTrait) => { + it(`Mint TokenProgram: ${tokenTrait.isToken2022 ? "Token-2022" : "Token"}`, async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const mint = await createMintV2(provider, tokenTrait); + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, whirlpoolsConfigKeypair.publicKey, mint); + const tokenBadgeData = await fetcher.getTokenBadge(tokenBadgePda.publicKey, IGNORE_CACHE); + assert.ok(tokenBadgeData!.whirlpoolsConfig.equals(whirlpoolsConfigKeypair.publicKey)); + assert.ok(tokenBadgeData!.tokenMint.equals(mint)); + }); + }) + }); + + it("successfully initialize when funder is different than account paying for transaction fee", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const mint = await createMintV2(provider, {isToken2022: true}); + + const preBalance = await ctx.connection.getBalance(ctx.wallet.publicKey); + const otherWallet = await createOtherWallet(); + + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, { + funder: otherWallet.publicKey, + }, [ + initialTokenBadgeAuthorityKeypair, + otherWallet + ]); + + const postBalance = await ctx.connection.getBalance(ctx.wallet.publicKey); + const diffBalance = preBalance - postBalance; + const minRent = await ctx.connection.getMinimumBalanceForRentExemption(0); + assert.ok(diffBalance < minRent); // ctx.wallet didn't pay any rent + + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, whirlpoolsConfigKeypair.publicKey, mint); + const tokenBadgeData = await fetcher.getTokenBadge(tokenBadgePda.publicKey, IGNORE_CACHE); + assert.ok(tokenBadgeData!.whirlpoolsConfig.equals(whirlpoolsConfigKeypair.publicKey)); + assert.ok(tokenBadgeData!.tokenMint.equals(mint)); + }); + + it("TokenBadge account has reserved space", async () => { + const tokenBadgeAccountSizeIncludingReserve = 8 + 32 + 32 + 128; + + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const mint = await createMintV2(provider, {isToken2022: true}); + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, whirlpoolsConfigKeypair.publicKey, mint); + + const account = await ctx.connection.getAccountInfo(tokenBadgePda.publicKey, "confirmed"); + assert.equal(account!.data.length, tokenBadgeAccountSizeIncludingReserve); + }); + + it("should be failed: already initialized", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const mint = await createMintV2(provider, {isToken2022: true}); + + // initialized + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}); + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, whirlpoolsConfigKeypair.publicKey, mint); + const tokenBadgeData = await fetcher.getTokenBadge(tokenBadgePda.publicKey, IGNORE_CACHE); + assert.ok(tokenBadgeData !== null); + + // re-initialize + await assert.rejects( + initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, {}), + (err) => { return JSON.stringify(err).includes("already in use") } + ); + }); + + describe("invalid input account", () => { + it("should be failed: invalid whirlpools_config", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const mint = await createMintV2(provider, {isToken2022: true}); + + // config not initialized + const anotherWhirlpoolsConfigKeypair = Keypair.generate(); + await assert.rejects( + initializeTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mint, { + whirlpoolsConfig: anotherWhirlpoolsConfigKeypair.publicKey, + } + ), + /0xbc4/ // AccountNotInitialized + ); + + // config initialized, but not match to whirlpools_config_extension + await initializeWhirlpoolsConfig(anotherWhirlpoolsConfigKeypair); + await assert.rejects( + initializeTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mint, { + whirlpoolsConfig: anotherWhirlpoolsConfigKeypair.publicKey, + } + ), + /0x7d6/ // ConstraintSeeds (token_badge (PDA) is not valid) + ); + + // with fake PDA + await assert.rejects( + initializeTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mint, { + whirlpoolsConfig: anotherWhirlpoolsConfigKeypair.publicKey, + tokenBadgePda: PDAUtil.getTokenBadge(ctx.program.programId, anotherWhirlpoolsConfigKeypair.publicKey, mint), + } + ), + /0x7d1/ // ConstraintHasOne + ); + }); + + it("should be failed: invalid whirlpools_config_extension", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + const mint = await createMintV2(provider, {isToken2022: true}); + + // config_extension not initialized + await assert.rejects( + initializeTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mint, { + whirlpoolsConfigExtension: PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey, + } + ), + /0xbc4/ // AccountNotInitialized + ); + + // initialized, but fake config_extension + const anotherWhirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(anotherWhirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(anotherWhirlpoolsConfigKeypair.publicKey); + await assert.rejects( + initializeTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mint, { + whirlpoolsConfigExtension: PDAUtil.getConfigExtension(ctx.program.programId, anotherWhirlpoolsConfigKeypair.publicKey).publicKey, + } + ), + /0x7d1/ // ConstraintHasOne + ); + }); + + it("should be failed: invalid token_badge_authority", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + const mint = await createMintV2(provider, {isToken2022: true}); + + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const fakeAuthority = Keypair.generate(); + await assert.rejects( + initializeTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mint, { + tokenBadgeAuthority: fakeAuthority.publicKey, + }, [ + fakeAuthority, + ] + ), + /0x7dc/ // ConstraintAddress + ); + }); + + it("should be failed: token_badge_authority is not signer", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + const mint = await createMintV2(provider, {isToken2022: true}); + + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + + // update authority from provider.wallet + await updateTokenBadgeAuthority(whirlpoolsConfigKeypair.publicKey, initialConfigExtensionAuthorityKeypair, updatedTokenBadgeAuthorityKeypair.publicKey); + const extension = await fetcher.getConfigExtension(whirlpoolsConfigExtension, IGNORE_CACHE); + assert.ok(extension?.tokenBadgeAuthority.equals(updatedTokenBadgeAuthorityKeypair.publicKey)); + + const ix: TransactionInstruction = program.instruction.initializeTokenBadge({ + accounts: { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension, + tokenBadgeAuthority: updatedTokenBadgeAuthorityKeypair.publicKey, + tokenMint: mint, + tokenBadge: PDAUtil.getTokenBadge(ctx.program.programId, whirlpoolsConfigKeypair.publicKey, mint).publicKey, + funder: ctx.wallet.publicKey, + systemProgram: SystemProgram.programId, + }, + }) + + assert.equal(ix.keys.length, 7); + assert.ok(ix.keys[2].pubkey.equals(updatedTokenBadgeAuthorityKeypair.publicKey)); + + // unset signer flag + ix.keys[2].isSigner = false; + + const tx = toTx(ctx, { + instructions: [ix], + cleanupInstructions: [], + signers: [], // no updatedTokenBadgeAuthorityKeypair + }) + + await assert.rejects( + tx.buildAndExecute(), + /0xbc2/ // AccountNotSigner + ); + }); + + it("should be failed: config_extension_authority is passed as token_badge_authority", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + const mint = await createMintV2(provider, {isToken2022: true}); + + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + + // update authority from provider.wallet + await updateTokenBadgeAuthority(whirlpoolsConfigKeypair.publicKey, initialConfigExtensionAuthorityKeypair, updatedTokenBadgeAuthorityKeypair.publicKey); + const extension = await fetcher.getConfigExtension(whirlpoolsConfigExtension, IGNORE_CACHE); + assert.ok(extension?.tokenBadgeAuthority.equals(updatedTokenBadgeAuthorityKeypair.publicKey)); + + const fakeAuthority = initialConfigExtensionAuthorityKeypair; + await assert.rejects( + initializeTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mint, { + tokenBadgeAuthority: fakeAuthority.publicKey, + }, [ + fakeAuthority, + ] + ), + /0x7dc/ // ConstraintAddress + ); + }); + + it("should be failed: invalid token_mint", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + // mint is not uninitialized + const uninitializedMint = Keypair.generate().publicKey; + await assert.rejects( + initializeTokenBadge( + whirlpoolsConfigKeypair.publicKey, + uninitializedMint, + {}, + ), + /0xbc4/ // AccountNotInitialized + ); + + // different mint + const mintA = await createMintV2(provider, {isToken2022: true}); + const mintB = await createMintV2(provider, {isToken2022: true}); + await assert.rejects( + initializeTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mintA, + { + tokenMint: mintB, + }, + ), + /0x7d6/ // ConstraintSeeds (token_badge (PDA) is not valid) + ); + }); + + it("should be failed: invalid token_badge", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + // different mint + const mintA = await createMintV2(provider, {isToken2022: true}); + const mintB = await createMintV2(provider, {isToken2022: true}); + const pdaForMintB = PDAUtil.getTokenBadge( + ctx.program.programId, + whirlpoolsConfigKeypair.publicKey, + mintB, + ); + await assert.rejects( + initializeTokenBadge( + whirlpoolsConfigKeypair.publicKey, + mintA, + { + tokenBadgePda: pdaForMintB, + }, + ), + /0x7d6/ // ConstraintSeeds (token_badge (PDA) is not valid) + ); + }); + + it("should be failed: funder is not signer", async () => { + const otherWallet = await createOtherWallet(); + + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + const mint = await createMintV2(provider, {isToken2022: true}); + + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + + const ix: TransactionInstruction = program.instruction.initializeTokenBadge({ + accounts: { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension, + tokenBadgeAuthority: initialTokenBadgeAuthorityKeypair.publicKey, + tokenMint: mint, + tokenBadge: PDAUtil.getTokenBadge(ctx.program.programId, whirlpoolsConfigKeypair.publicKey, mint).publicKey, + funder: otherWallet.publicKey, + systemProgram: SystemProgram.programId, + }, + }) + + assert.equal(ix.keys.length, 7); + assert.ok(ix.keys[5].pubkey.equals(otherWallet.publicKey)); + + // unset signer flag + ix.keys[5].isSigner = false; + + const tx = toTx(ctx, { + instructions: [ix], + cleanupInstructions: [], + signers: [initialTokenBadgeAuthorityKeypair], // no otherWallet + }) + + await assert.rejects( + tx.buildAndExecute(), + /0xbc2/ // AccountNotSigner + ); + }); + + it("should be failed: invalid system program", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + const mint = await createMintV2(provider, {isToken2022: true}); + + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + + const invalidSystemProgram = TOKEN_PROGRAM_ID; + const ix: TransactionInstruction = program.instruction.initializeTokenBadge({ + accounts: { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension, + tokenBadgeAuthority: initialTokenBadgeAuthorityKeypair.publicKey, + tokenMint: mint, + tokenBadge: PDAUtil.getTokenBadge(ctx.program.programId, whirlpoolsConfigKeypair.publicKey, mint).publicKey, + funder: ctx.wallet.publicKey, + systemProgram: invalidSystemProgram, + }, + }) + + const tx = toTx(ctx, { + instructions: [ix], + cleanupInstructions: [], + signers: [initialTokenBadgeAuthorityKeypair], + }) + + await assert.rejects( + tx.buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + }); +}); diff --git a/sdk/tests/integration/v2/token-badge/set_config_extension_authority.test.ts b/sdk/tests/integration/v2/token-badge/set_config_extension_authority.test.ts new file mode 100644 index 000000000..52fa17743 --- /dev/null +++ b/sdk/tests/integration/v2/token-badge/set_config_extension_authority.test.ts @@ -0,0 +1,212 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Keypair, PublicKey, TransactionInstruction } from "@solana/web3.js"; +import * as assert from "assert"; +import { + IGNORE_CACHE, + PDAUtil, + toTx, + WhirlpoolContext, + WhirlpoolIx +} from "../../../../src"; +import { defaultConfirmOptions } from "../../../utils/const"; + +describe("set_config_extension_authority", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + + const collectProtocolFeesAuthorityKeypair = Keypair.generate(); + const feeAuthorityKeypair = Keypair.generate(); + const rewardEmissionsSuperAuthorityKeypair = Keypair.generate(); + const initialConfigExtensionAuthorityKeypair = feeAuthorityKeypair; + const initialTokenBadgeAuthorityKeypair = feeAuthorityKeypair; + const updatedConfigExtensionAuthorityKeypair = Keypair.generate(); + + async function initializeWhirlpoolsConfig(configKeypair: Keypair) { + return toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, { + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + feeAuthority: feeAuthorityKeypair.publicKey, + rewardEmissionsSuperAuthority: rewardEmissionsSuperAuthorityKeypair.publicKey, + defaultProtocolFeeRate: 300, + funder: provider.wallet.publicKey, + whirlpoolsConfigKeypair: configKeypair, + })).addSigner(configKeypair).buildAndExecute(); + } + + async function initializeWhirlpoolsConfigExtension(config: PublicKey) { + const pda = PDAUtil.getConfigExtension(ctx.program.programId, config); + return toTx(ctx, WhirlpoolIx.initializeConfigExtensionIx(ctx.program, { + feeAuthority: feeAuthorityKeypair.publicKey, + funder: provider.wallet.publicKey, + whirlpoolsConfig: config, + whirlpoolsConfigExtensionPda: pda, + })).addSigner(feeAuthorityKeypair).buildAndExecute(); + } + + async function setConfigExtensionAuthority(config: PublicKey, configExtensionAuthority: Keypair, newAuthority: PublicKey) { + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, config).publicKey; + return toTx(ctx, WhirlpoolIx.setConfigExtensionAuthorityIx(ctx.program, { + whirlpoolsConfig: config, + whirlpoolsConfigExtension, + configExtensionAuthority: configExtensionAuthority.publicKey, + newConfigExtensionAuthority: newAuthority, + })).addSigner(configExtensionAuthority).buildAndExecute(); + } + + it("successfully set config extension authority and verify updated account contents", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + const extensionData = await fetcher.getConfigExtension(whirlpoolsConfigExtension, IGNORE_CACHE); + assert.ok(extensionData!.configExtensionAuthority.equals(initialConfigExtensionAuthorityKeypair.publicKey)); + assert.ok(extensionData!.tokenBadgeAuthority.equals(initialTokenBadgeAuthorityKeypair.publicKey)); + + assert.ok(!initialConfigExtensionAuthorityKeypair.publicKey.equals(updatedConfigExtensionAuthorityKeypair.publicKey)); + await setConfigExtensionAuthority( + whirlpoolsConfigKeypair.publicKey, + initialConfigExtensionAuthorityKeypair, + updatedConfigExtensionAuthorityKeypair.publicKey + ); + + const updatedExtensionData = await fetcher.getConfigExtension(whirlpoolsConfigExtension, IGNORE_CACHE); + assert.ok(updatedExtensionData!.configExtensionAuthority.equals(updatedConfigExtensionAuthorityKeypair.publicKey)); + assert.ok(updatedExtensionData!.tokenBadgeAuthority.equals(initialTokenBadgeAuthorityKeypair.publicKey)); + + // set back to initialConfigExtension with updateConfigExtensionAuthority + await setConfigExtensionAuthority( + whirlpoolsConfigKeypair.publicKey, + updatedConfigExtensionAuthorityKeypair, + initialConfigExtensionAuthorityKeypair.publicKey, + ); + + const backExtensionData = await fetcher.getConfigExtension(whirlpoolsConfigExtension, IGNORE_CACHE); + assert.ok(backExtensionData!.configExtensionAuthority.equals(initialConfigExtensionAuthorityKeypair.publicKey)); + }); + + describe("invalid input account", () => { + it("should be failed: invalid whirlpools_config", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + + // config not initialized + const anotherWhirlpoolsConfigKeypair = Keypair.generate(); + await assert.rejects( + toTx(ctx, WhirlpoolIx.setConfigExtensionAuthorityIx(ctx.program, { + whirlpoolsConfig: anotherWhirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension, + configExtensionAuthority: initialConfigExtensionAuthorityKeypair.publicKey, + newConfigExtensionAuthority: updatedConfigExtensionAuthorityKeypair.publicKey, + })).addSigner(initialConfigExtensionAuthorityKeypair).buildAndExecute(), + /0xbc4/ // AccountNotInitialized + ); + + // config initialized, but not match to whirlpools_config_extension + await initializeWhirlpoolsConfig(anotherWhirlpoolsConfigKeypair); + await assert.rejects( + toTx(ctx, WhirlpoolIx.setConfigExtensionAuthorityIx(ctx.program, { + whirlpoolsConfig: anotherWhirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension, + configExtensionAuthority: initialConfigExtensionAuthorityKeypair.publicKey, + newConfigExtensionAuthority: updatedConfigExtensionAuthorityKeypair.publicKey, + })).addSigner(initialConfigExtensionAuthorityKeypair).buildAndExecute(), + /0x7d1/ // ConstraintHasOne + ); + }); + + it("should be failed: invalid whirlpools_config_extension", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + + // config_extension not initialized + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + await assert.rejects( + toTx(ctx, WhirlpoolIx.setConfigExtensionAuthorityIx(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension, + configExtensionAuthority: initialConfigExtensionAuthorityKeypair.publicKey, + newConfigExtensionAuthority: updatedConfigExtensionAuthorityKeypair.publicKey, + })).addSigner(initialConfigExtensionAuthorityKeypair).buildAndExecute(), + /0xbc4/ // AccountNotInitialized + ); + + // initialized, but fake config_extension + const anotherWhirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(anotherWhirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(anotherWhirlpoolsConfigKeypair.publicKey); + const anotherWhirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, anotherWhirlpoolsConfigKeypair.publicKey).publicKey; + await assert.rejects( + toTx(ctx, WhirlpoolIx.setConfigExtensionAuthorityIx(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension: anotherWhirlpoolsConfigExtension, + configExtensionAuthority: initialConfigExtensionAuthorityKeypair.publicKey, + newConfigExtensionAuthority: updatedConfigExtensionAuthorityKeypair.publicKey, + })).addSigner(initialConfigExtensionAuthorityKeypair).buildAndExecute(), + /0x7d1/ // ConstraintHasOne + ); + }); + + it("should be failed: invalid config_extension_authority", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + + const fakeAuthority = Keypair.generate(); + await assert.rejects( + toTx(ctx, WhirlpoolIx.setConfigExtensionAuthorityIx(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension, + configExtensionAuthority: fakeAuthority.publicKey, + newConfigExtensionAuthority: updatedConfigExtensionAuthorityKeypair.publicKey, + })).addSigner(fakeAuthority).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("should be failed: config_extension_authority is not signer", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + + // update authority from provider.wallet + await setConfigExtensionAuthority(whirlpoolsConfigKeypair.publicKey, initialConfigExtensionAuthorityKeypair, updatedConfigExtensionAuthorityKeypair.publicKey); + const extension = await fetcher.getConfigExtension(whirlpoolsConfigExtension, IGNORE_CACHE); + assert.ok(extension?.configExtensionAuthority.equals(updatedConfigExtensionAuthorityKeypair.publicKey)); + + const ix: TransactionInstruction = program.instruction.setConfigExtensionAuthority({ + accounts: { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension, + configExtensionAuthority: updatedConfigExtensionAuthorityKeypair.publicKey, + newConfigExtensionAuthority: Keypair.generate().publicKey, + }, + }) + + assert.equal(ix.keys.length, 4); + assert.ok(ix.keys[2].pubkey.equals(updatedConfigExtensionAuthorityKeypair.publicKey)); + + // unset signer flag + ix.keys[2].isSigner = false; + + const tx = toTx(ctx, { + instructions: [ix], + cleanupInstructions: [], + signers: [], // no updatedConfigExtensionAuthorityKeypair + }) + + await assert.rejects( + tx.buildAndExecute(), + /0xbc2/ // AccountNotSigner + ); + }); + }); +}); diff --git a/sdk/tests/integration/v2/token-badge/set_token_badge_authority.test.ts b/sdk/tests/integration/v2/token-badge/set_token_badge_authority.test.ts new file mode 100644 index 000000000..65e193bbb --- /dev/null +++ b/sdk/tests/integration/v2/token-badge/set_token_badge_authority.test.ts @@ -0,0 +1,261 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Keypair, PublicKey, TransactionInstruction } from "@solana/web3.js"; +import * as assert from "assert"; +import { + IGNORE_CACHE, + PDAUtil, + toTx, + WhirlpoolContext, + WhirlpoolIx +} from "../../../../src"; +import { defaultConfirmOptions } from "../../../utils/const"; +import { InitializeTokenBadgeParams } from "../../../../src/instructions"; +import { createMintV2 } from "../../../utils/v2/token-2022"; + +describe("set_token_badge_authority", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + + const collectProtocolFeesAuthorityKeypair = Keypair.generate(); + const feeAuthorityKeypair = Keypair.generate(); + const rewardEmissionsSuperAuthorityKeypair = Keypair.generate(); + const initialConfigExtensionAuthorityKeypair = feeAuthorityKeypair; + const initialTokenBadgeAuthorityKeypair = feeAuthorityKeypair; + const updatedTokenBadgeAuthorityKeypair = Keypair.generate(); + + async function initializeWhirlpoolsConfig(configKeypair: Keypair) { + return toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, { + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + feeAuthority: feeAuthorityKeypair.publicKey, + rewardEmissionsSuperAuthority: rewardEmissionsSuperAuthorityKeypair.publicKey, + defaultProtocolFeeRate: 300, + funder: provider.wallet.publicKey, + whirlpoolsConfigKeypair: configKeypair, + })).addSigner(configKeypair).buildAndExecute(); + } + + async function initializeWhirlpoolsConfigExtension(config: PublicKey) { + const pda = PDAUtil.getConfigExtension(ctx.program.programId, config); + return toTx(ctx, WhirlpoolIx.initializeConfigExtensionIx(ctx.program, { + feeAuthority: feeAuthorityKeypair.publicKey, + funder: provider.wallet.publicKey, + whirlpoolsConfig: config, + whirlpoolsConfigExtensionPda: pda, + })).addSigner(feeAuthorityKeypair).buildAndExecute(); + } + + async function initializeTokenBadge(config: PublicKey, mint: PublicKey, overwrite: Partial, signers: Keypair[] = [initialTokenBadgeAuthorityKeypair]) { + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, config).publicKey; + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, config, mint); + const tx = toTx(ctx, WhirlpoolIx.initializeTokenBadgeIx(ctx.program, { + whirlpoolsConfig: config, + whirlpoolsConfigExtension, + funder: provider.wallet.publicKey, + tokenBadgeAuthority: initialTokenBadgeAuthorityKeypair.publicKey, + tokenBadgePda, + tokenMint: mint, + ...overwrite, + })); + signers.forEach((signer) => tx.addSigner(signer)); + return tx.buildAndExecute(); + } + + async function setTokenBadgeAuthority(config: PublicKey, configExtensionAuthority: Keypair, newAuthority: PublicKey) { + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, config).publicKey; + return toTx(ctx, WhirlpoolIx.setTokenBadgeAuthorityIx(ctx.program, { + whirlpoolsConfig: config, + whirlpoolsConfigExtension, + configExtensionAuthority: configExtensionAuthority.publicKey, + newTokenBadgeAuthority: newAuthority, + })).addSigner(configExtensionAuthority).buildAndExecute(); + } + + it("successfully set token badge authority and verify updated account contents", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + const extensionData = await fetcher.getConfigExtension(whirlpoolsConfigExtension, IGNORE_CACHE); + assert.ok(extensionData!.tokenBadgeAuthority.equals(initialTokenBadgeAuthorityKeypair.publicKey)); + + assert.ok(!initialTokenBadgeAuthorityKeypair.publicKey.equals(updatedTokenBadgeAuthorityKeypair.publicKey)); + await setTokenBadgeAuthority( + whirlpoolsConfigKeypair.publicKey, + initialConfigExtensionAuthorityKeypair, + updatedTokenBadgeAuthorityKeypair.publicKey + ); + + const updatedExtensionData = await fetcher.getConfigExtension(whirlpoolsConfigExtension, IGNORE_CACHE); + assert.ok(updatedExtensionData!.tokenBadgeAuthority.equals(updatedTokenBadgeAuthorityKeypair.publicKey)); + + // initialize TokenBadge with updated authority + const mint = await createMintV2(provider, {isToken2022: true}); + await initializeTokenBadge(whirlpoolsConfigKeypair.publicKey, mint, { + tokenBadgeAuthority: updatedTokenBadgeAuthorityKeypair.publicKey, + }, [ + updatedTokenBadgeAuthorityKeypair + ]); + + const tokenBadgePda = PDAUtil.getTokenBadge(ctx.program.programId, whirlpoolsConfigKeypair.publicKey, mint); + const tokenBadgeData = await fetcher.getTokenBadge(tokenBadgePda.publicKey, IGNORE_CACHE); + assert.ok(tokenBadgeData!.whirlpoolsConfig.equals(whirlpoolsConfigKeypair.publicKey)); + assert.ok(tokenBadgeData!.tokenMint.equals(mint)); + }); + + describe("invalid input account", () => { + it("should be failed: invalid whirlpools_config", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + + // config not initialized + const anotherWhirlpoolsConfigKeypair = Keypair.generate(); + await assert.rejects( + toTx(ctx, WhirlpoolIx.setTokenBadgeAuthorityIx(ctx.program, { + whirlpoolsConfig: anotherWhirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension, + configExtensionAuthority: initialConfigExtensionAuthorityKeypair.publicKey, + newTokenBadgeAuthority: updatedTokenBadgeAuthorityKeypair.publicKey, + })).addSigner(initialTokenBadgeAuthorityKeypair).buildAndExecute(), + /0xbc4/ // AccountNotInitialized + ); + + // config initialized, but not match to whirlpools_config_extension + await initializeWhirlpoolsConfig(anotherWhirlpoolsConfigKeypair); + await assert.rejects( + toTx(ctx, WhirlpoolIx.setTokenBadgeAuthorityIx(ctx.program, { + whirlpoolsConfig: anotherWhirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension, + configExtensionAuthority: initialConfigExtensionAuthorityKeypair.publicKey, + newTokenBadgeAuthority: updatedTokenBadgeAuthorityKeypair.publicKey, + })).addSigner(initialTokenBadgeAuthorityKeypair).buildAndExecute(), + /0x7d1/ // ConstraintHasOne + ); + }); + + it("should be failed: invalid whirlpools_config_extension", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + + // config_extension not initialized + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + await assert.rejects( + toTx(ctx, WhirlpoolIx.setTokenBadgeAuthorityIx(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension, + configExtensionAuthority: initialConfigExtensionAuthorityKeypair.publicKey, + newTokenBadgeAuthority: updatedTokenBadgeAuthorityKeypair.publicKey, + })).addSigner(initialTokenBadgeAuthorityKeypair).buildAndExecute(), + /0xbc4/ // AccountNotInitialized + ); + + // initialized, but fake config_extension + const anotherWhirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(anotherWhirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(anotherWhirlpoolsConfigKeypair.publicKey); + const anotherWhirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, anotherWhirlpoolsConfigKeypair.publicKey).publicKey; + await assert.rejects( + toTx(ctx, WhirlpoolIx.setTokenBadgeAuthorityIx(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension: anotherWhirlpoolsConfigExtension, + configExtensionAuthority: initialConfigExtensionAuthorityKeypair.publicKey, + newTokenBadgeAuthority: updatedTokenBadgeAuthorityKeypair.publicKey, + })).addSigner(initialTokenBadgeAuthorityKeypair).buildAndExecute(), + /0x7d1/ // ConstraintHasOne + ); + }); + + it("should be failed: invalid config_extension_authority", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + + const fakeAuthority = Keypair.generate(); + await assert.rejects( + toTx(ctx, WhirlpoolIx.setTokenBadgeAuthorityIx(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension, + configExtensionAuthority: fakeAuthority.publicKey, + newTokenBadgeAuthority: updatedTokenBadgeAuthorityKeypair.publicKey, + })).addSigner(fakeAuthority).buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("should be failed: token_badge_authority != config_extension_authority", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + const extensionData = await fetcher.getConfigExtension(whirlpoolsConfigExtension, IGNORE_CACHE); + assert.ok(extensionData!.tokenBadgeAuthority.equals(initialTokenBadgeAuthorityKeypair.publicKey)); + + assert.ok(!initialTokenBadgeAuthorityKeypair.publicKey.equals(updatedTokenBadgeAuthorityKeypair.publicKey)); + await setTokenBadgeAuthority( + whirlpoolsConfigKeypair.publicKey, + initialConfigExtensionAuthorityKeypair, + updatedTokenBadgeAuthorityKeypair.publicKey + ); + + const updatedExtensionData = await fetcher.getConfigExtension(whirlpoolsConfigExtension, IGNORE_CACHE); + assert.ok(updatedExtensionData!.tokenBadgeAuthority.equals(updatedTokenBadgeAuthorityKeypair.publicKey)); + + assert.ok(!updatedTokenBadgeAuthorityKeypair.publicKey.equals(initialConfigExtensionAuthorityKeypair.publicKey)); + await assert.rejects( + setTokenBadgeAuthority( + whirlpoolsConfigKeypair.publicKey, + updatedTokenBadgeAuthorityKeypair, + Keypair.generate().publicKey, + ), + /0x7dc/ // ConstraintAddress + ); + }); + + it("should be failed: config_extension_authority is not signer", async () => { + const whirlpoolsConfigKeypair = Keypair.generate(); + await initializeWhirlpoolsConfig(whirlpoolsConfigKeypair); + + await initializeWhirlpoolsConfigExtension(whirlpoolsConfigKeypair.publicKey); + const whirlpoolsConfigExtension = PDAUtil.getConfigExtension(ctx.program.programId, whirlpoolsConfigKeypair.publicKey).publicKey; + + // update authority from provider.wallet + await setTokenBadgeAuthority(whirlpoolsConfigKeypair.publicKey, initialTokenBadgeAuthorityKeypair, updatedTokenBadgeAuthorityKeypair.publicKey); + const extension = await fetcher.getConfigExtension(whirlpoolsConfigExtension, IGNORE_CACHE); + assert.ok(extension?.tokenBadgeAuthority.equals(updatedTokenBadgeAuthorityKeypair.publicKey)); + + const ix: TransactionInstruction = program.instruction.setTokenBadgeAuthority({ + accounts: { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension, + configExtensionAuthority: updatedTokenBadgeAuthorityKeypair.publicKey, + newTokenBadgeAuthority: Keypair.generate().publicKey, + }, + }) + + assert.equal(ix.keys.length, 4); + assert.ok(ix.keys[2].pubkey.equals(updatedTokenBadgeAuthorityKeypair.publicKey)); + + // unset signer flag + ix.keys[2].isSigner = false; + + const tx = toTx(ctx, { + instructions: [ix], + cleanupInstructions: [], + signers: [], // no updatedTokenBadgeAuthorityKeypair + }) + + await assert.rejects( + tx.buildAndExecute(), + /0xbc2/ // AccountNotSigner + ); + }); + }); +}); diff --git a/sdk/tests/integration/v2/token-extensions/memo-transfer.test.ts b/sdk/tests/integration/v2/token-extensions/memo-transfer.test.ts new file mode 100644 index 000000000..4b0c824ca --- /dev/null +++ b/sdk/tests/integration/v2/token-extensions/memo-transfer.test.ts @@ -0,0 +1,1327 @@ +import * as anchor from "@coral-xyz/anchor"; +import { BN } from "@coral-xyz/anchor"; +import { MathUtil, PDA, Percentage } from "@orca-so/common-sdk"; +import * as assert from "assert"; +import Decimal from "decimal.js"; +import { + buildWhirlpoolClient, + collectRewardsQuote, + DecreaseLiquidityQuote, + decreaseLiquidityQuoteByLiquidityWithParams, + InitPoolV2Params, + NUM_REWARDS, + PDAUtil, + PositionData, + SwapQuote, + swapQuoteWithParams, + SwapUtils, + toTx, + twoHopSwapQuoteFromSwapQuotes, + TwoHopSwapV2Params, + WhirlpoolContext, + WhirlpoolData, + WhirlpoolIx, +} from "../../../../src"; +import { IGNORE_CACHE } from "../../../../src/network/public/fetcher"; +import { + getTokenBalance, + sleep, + TickSpacing, + ZERO_BN, +} from "../../../utils"; +import { defaultConfirmOptions } from "../../../utils/const"; +import { WhirlpoolTestFixtureV2 } from "../../../utils/v2/fixture-v2"; +import { + FundedPositionV2Params, + fundPositionsV2, + initTestPoolWithTokensV2, +} from "../../../utils/v2/init-utils-v2"; +import { + createTokenAccountV2, + disableRequiredMemoTransfers, + enableRequiredMemoTransfers, + isRequiredMemoTransfersEnabled, +} from "../../../utils/v2/token-2022"; +import { PublicKey } from "@solana/web3.js"; +import { initTickArrayRange } from "../../../utils/init-utils"; +import { + InitAquariumV2Params, + buildTestAquariumsV2, + getDefaultAquariumV2, + getTokenAccsForPoolsV2, +} from "../../../utils/v2/aquarium-v2"; +import { TokenExtensionUtil } from "../../../../src/utils/public/token-extension-util"; + +describe("TokenExtension/MemoTransfer", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + const client = buildWhirlpoolClient(ctx); + + const MEMO_TRANSFER_COLLECT_FEES = "Orca CollectFees"; + const MEMO_TRANSFER_COLLECT_PROTOCOL_FEES = "Orca CollectProtocolFees"; + const MEMO_TRANSFER_COLLECT_REWARD = "Orca CollectReward"; + const MEMO_TRANSFER_DECREASE_LIQUIDITY = "Orca Withdraw"; + const MEMO_TRANSFER_SWAP = "Orca Trade"; + + describe("collect_fees_v2, collect_protocol_fees_v2", () => { + let fixture: WhirlpoolTestFixtureV2; + let feeAccountA: PublicKey; + let feeAccountB: PublicKey; + + beforeEach(async () => { + // In same tick array - start index 22528 + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + + const tickSpacing = TickSpacing.Standard; + fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }, // In range position + { tickLowerIndex: 0, tickUpperIndex: 128, liquidityAmount: new anchor.BN(1_000_000) }, // Out of range position + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + const tickArrayPda = PDAUtil.getTickArray( + ctx.program.programId, + whirlpoolPda.publicKey, + 22528 + ); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + // Accrue fees in token A + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + // Accrue fees in token B + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(5)), + amountSpecifiedIsInput: true, + aToB: false, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + }) + ).buildAndExecute(); + + await toTx( + ctx, + WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + position: positions[0].publicKey, + tickArrayLower: tickArrayPda.publicKey, + tickArrayUpper: tickArrayPda.publicKey, + }) + ).buildAndExecute(); + + const whirlpoolData = (await fetcher.getPool(whirlpoolPda.publicKey, IGNORE_CACHE))!; + assert.ok(!whirlpoolData.protocolFeeOwedA.isZero()); + assert.ok(!whirlpoolData.protocolFeeOwedB.isZero()); + + const positionBeforeCollect = (await fetcher.getPosition( + positions[0].publicKey, + IGNORE_CACHE + )) as PositionData; + assert.ok(!positionBeforeCollect.feeOwedA.isZero()); + assert.ok(!positionBeforeCollect.feeOwedB.isZero()); + + feeAccountA = await createTokenAccountV2( + provider, + { isToken2022: true }, + tokenMintA, + provider.wallet.publicKey + ); + feeAccountB = await createTokenAccountV2( + provider, + { isToken2022: true }, + tokenMintB, + provider.wallet.publicKey + ); + }); + + it("collect_fees_v2: without memo", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + assert.ok(!(await isRequiredMemoTransfersEnabled(provider, feeAccountA))); + assert.ok(!(await isRequiredMemoTransfersEnabled(provider, feeAccountB))); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(); + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(feeBalanceA).gtn(0)); + assert.ok(new BN(feeBalanceB).gtn(0)); + + const memoCount = await countMemoLog(provider, sig, MEMO_TRANSFER_COLLECT_FEES); + assert.equal(memoCount, 0); + }); + + it("collect_fees_v2: with memo", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + await enableRequiredMemoTransfers(provider, feeAccountA); + await enableRequiredMemoTransfers(provider, feeAccountB); + + assert.ok(await isRequiredMemoTransfersEnabled(provider, feeAccountA)); + assert.ok(await isRequiredMemoTransfersEnabled(provider, feeAccountB)); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(); + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(feeBalanceA).gtn(0)); + assert.ok(new BN(feeBalanceB).gtn(0)); + + const memoCount = await countMemoLog(provider, sig, MEMO_TRANSFER_COLLECT_FEES); + assert.equal(memoCount, 2); + }); + + it("collect_fees_v2: without memo (has extension, but disabled)", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + await enableRequiredMemoTransfers(provider, feeAccountA); + await enableRequiredMemoTransfers(provider, feeAccountB); + assert.ok(await isRequiredMemoTransfersEnabled(provider, feeAccountA)); + assert.ok(await isRequiredMemoTransfersEnabled(provider, feeAccountB)); + + await disableRequiredMemoTransfers(provider, feeAccountA); + await disableRequiredMemoTransfers(provider, feeAccountB); + assert.ok(!(await isRequiredMemoTransfersEnabled(provider, feeAccountA))); + assert.ok(!(await isRequiredMemoTransfersEnabled(provider, feeAccountB))); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(); + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(feeBalanceA).gtn(0)); + assert.ok(new BN(feeBalanceB).gtn(0)); + + const memoCount = await countMemoLog(provider, sig, MEMO_TRANSFER_COLLECT_FEES); + assert.equal(memoCount, 0); + }); + + it("collect_protocol_fees_v2: without memo", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKeypair }, + } = fixture.getInfos(); + + assert.ok(!(await isRequiredMemoTransfersEnabled(provider, feeAccountA))); + assert.ok(!(await isRequiredMemoTransfersEnabled(provider, feeAccountB))); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(); + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(feeBalanceA).gtn(0)); + assert.ok(new BN(feeBalanceB).gtn(0)); + + const memoCount = await countMemoLog(provider, sig, MEMO_TRANSFER_COLLECT_PROTOCOL_FEES); + assert.equal(memoCount, 0); + }); + + it("collect_protocol_fees_v2: with memo", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKeypair }, + } = fixture.getInfos(); + + await enableRequiredMemoTransfers(provider, feeAccountA); + await enableRequiredMemoTransfers(provider, feeAccountB); + + assert.ok(await isRequiredMemoTransfersEnabled(provider, feeAccountA)); + assert.ok(await isRequiredMemoTransfersEnabled(provider, feeAccountB)); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(); + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(feeBalanceA).gtn(0)); + assert.ok(new BN(feeBalanceB).gtn(0)); + + const memoCount = await countMemoLog(provider, sig, MEMO_TRANSFER_COLLECT_PROTOCOL_FEES); + assert.equal(memoCount, 2); + }); + + it("collect_protocol_fees_v2: without memo (has extension, but disabled)", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKeypair }, + } = fixture.getInfos(); + + await enableRequiredMemoTransfers(provider, feeAccountA); + await enableRequiredMemoTransfers(provider, feeAccountB); + assert.ok(await isRequiredMemoTransfersEnabled(provider, feeAccountA)); + assert.ok(await isRequiredMemoTransfersEnabled(provider, feeAccountB)); + + await disableRequiredMemoTransfers(provider, feeAccountA); + await disableRequiredMemoTransfers(provider, feeAccountB); + assert.ok(!(await isRequiredMemoTransfersEnabled(provider, feeAccountA))); + assert.ok(!(await isRequiredMemoTransfersEnabled(provider, feeAccountB))); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(); + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(feeBalanceA).gtn(0)); + assert.ok(new BN(feeBalanceB).gtn(0)); + + const memoCount = await countMemoLog(provider, sig, MEMO_TRANSFER_COLLECT_PROTOCOL_FEES); + assert.equal(memoCount, 0); + }); + }); + + describe("collect_reward_v2", () => { + let fixture: WhirlpoolTestFixtureV2; + let rewardAccounts: PublicKey[]; + + beforeEach(async () => { + const vaultStartBalance = 1_000_000; + const lowerTickIndex = -1280, + upperTickIndex = 1280, + tickSpacing = TickSpacing.Standard; + fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: tickSpacing, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: lowerTickIndex, + tickUpperIndex: upperTickIndex, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: { isToken2022: true }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: { isToken2022: true }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: { isToken2022: true }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + // accrue rewards + await sleep(3000); + + await toTx( + ctx, + WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + position: positions[0].publicKey, + tickArrayLower: positions[0].tickArrayLower, + tickArrayUpper: positions[0].tickArrayUpper, + }) + ).buildAndExecute(); + + // Generate collect reward expectation + const whirlpoolData = (await fetcher.getPool(whirlpoolPda.publicKey)) as WhirlpoolData; + const positionPreCollect = await client.getPosition(positions[0].publicKey, IGNORE_CACHE); + + // Lock the collectRewards quote to the last time we called updateFeesAndRewards + const expectation = collectRewardsQuote({ + whirlpool: whirlpoolData, + position: positionPreCollect.getData(), + tickLower: positionPreCollect.getLowerTickData(), + tickUpper: positionPreCollect.getUpperTickData(), + timeStampInSeconds: whirlpoolData.rewardLastUpdatedTimestamp, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }); + + // Check that the expectation is not zero + for (let i = 0; i < NUM_REWARDS; i++) { + assert.ok(!expectation.rewardOwed[i]!.isZero()); + } + + rewardAccounts = await Promise.all( + rewards.map((reward) => { + return createTokenAccountV2( + provider, + { isToken2022: true }, + reward.rewardMint, + provider.wallet.publicKey + ); + }) + ); + }); + + it("collect_reward_v2: without memo", async () => { + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + for (let i = 0; i < NUM_REWARDS; i++) { + assert.ok(!(await isRequiredMemoTransfersEnabled(provider, rewardAccounts[i]))); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[i].rewardMint, + rewardTokenProgram: rewards[i].tokenProgram, + rewardOwnerAccount: rewardAccounts[i], + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + rewardIndex: i, + }) + ).buildAndExecute(); + const rewardBalance = await getTokenBalance(provider, rewardAccounts[i]); + assert.ok(new BN(rewardBalance).gtn(0)); + + const memoCount = await countMemoLog(provider, sig, MEMO_TRANSFER_COLLECT_REWARD); + assert.equal(memoCount, 0); + } + }); + + it("collect_reward_v2: with memo", async () => { + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + for (let i = 0; i < NUM_REWARDS; i++) { + await enableRequiredMemoTransfers(provider, rewardAccounts[i]); + assert.ok(await isRequiredMemoTransfersEnabled(provider, rewardAccounts[i])); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[i].rewardMint, + rewardTokenProgram: rewards[i].tokenProgram, + rewardOwnerAccount: rewardAccounts[i], + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + rewardIndex: i, + }) + ).buildAndExecute(); + const rewardBalance = await getTokenBalance(provider, rewardAccounts[i]); + assert.ok(new BN(rewardBalance).gtn(0)); + + const memoCount = await countMemoLog(provider, sig, MEMO_TRANSFER_COLLECT_REWARD); + assert.equal(memoCount, 1); + } + }); + + it("collect_reward_v2: without memo (has extension, but disabled)", async () => { + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + for (let i = 0; i < NUM_REWARDS; i++) { + await enableRequiredMemoTransfers(provider, rewardAccounts[i]); + assert.ok(await isRequiredMemoTransfersEnabled(provider, rewardAccounts[i])); + await disableRequiredMemoTransfers(provider, rewardAccounts[i]); + assert.ok(!(await isRequiredMemoTransfersEnabled(provider, rewardAccounts[i]))); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[i].rewardMint, + rewardTokenProgram: rewards[i].tokenProgram, + rewardOwnerAccount: rewardAccounts[i], + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + rewardIndex: i, + }) + ).buildAndExecute(); + const rewardBalance = await getTokenBalance(provider, rewardAccounts[i]); + assert.ok(new BN(rewardBalance).gtn(0)); + + const memoCount = await countMemoLog(provider, sig, MEMO_TRANSFER_COLLECT_REWARD); + assert.equal(memoCount, 0); + } + }); + }); + + describe("decrease_liquidity_v2", () => { + let fixture: WhirlpoolTestFixtureV2; + let removalQuote: DecreaseLiquidityQuote; + let destAccountA: PublicKey; + let destAccountB: PublicKey; + + beforeEach(async () => { + const liquidityAmount = new anchor.BN(1_250_000); + const tickLower = 7168, + tickUpper = 8960; + fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1.48)), + positions: [{ tickLowerIndex: tickLower, tickUpperIndex: tickUpper, liquidityAmount }], + }); + const { poolInitInfo } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const poolBefore = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + + removalQuote = decreaseLiquidityQuoteByLiquidityWithParams({ + liquidity: new anchor.BN(1_000_000), + sqrtPrice: poolBefore.sqrtPrice, + slippageTolerance: Percentage.fromFraction(1, 100), + tickCurrentIndex: poolBefore.tickCurrentIndex, + tickLowerIndex: tickLower, + tickUpperIndex: tickUpper, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolBefore, IGNORE_CACHE), + }); + assert.ok(!removalQuote.tokenEstA.isZero()); + assert.ok(!removalQuote.tokenEstB.isZero()); + + destAccountA = await createTokenAccountV2( + provider, + { isToken2022: true }, + poolInitInfo.tokenMintA, + provider.wallet.publicKey + ); + destAccountB = await createTokenAccountV2( + provider, + { isToken2022: true }, + poolInitInfo.tokenMintB, + provider.wallet.publicKey + ); + }); + + it("decrease_liquidity_v2: without memo", async () => { + const { poolInitInfo, positions } = fixture.getInfos(); + + const sig = await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + ...removalQuote, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: destAccountA, + tokenOwnerAccountB: destAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positions[0].tickArrayLower, + tickArrayUpper: positions[0].tickArrayUpper, + }) + ).buildAndExecute(); + const destBalanceA = await getTokenBalance(provider, destAccountA); + const destBalanceB = await getTokenBalance(provider, destAccountB); + assert.ok(new BN(destBalanceA).gtn(0)); + assert.ok(new BN(destBalanceB).gtn(0)); + + const memoCount = await countMemoLog(provider, sig, MEMO_TRANSFER_DECREASE_LIQUIDITY); + assert.equal(memoCount, 0); + }); + + it("decrease_liquidity_v2: with memo", async () => { + const { poolInitInfo, positions } = fixture.getInfos(); + + await enableRequiredMemoTransfers(provider, destAccountA); + await enableRequiredMemoTransfers(provider, destAccountB); + assert.ok(await isRequiredMemoTransfersEnabled(provider, destAccountA)); + assert.ok(await isRequiredMemoTransfersEnabled(provider, destAccountB)); + + const sig = await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + ...removalQuote, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: destAccountA, + tokenOwnerAccountB: destAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positions[0].tickArrayLower, + tickArrayUpper: positions[0].tickArrayUpper, + }) + ).buildAndExecute(); + const destBalanceA = await getTokenBalance(provider, destAccountA); + const destBalanceB = await getTokenBalance(provider, destAccountB); + assert.ok(new BN(destBalanceA).gtn(0)); + assert.ok(new BN(destBalanceB).gtn(0)); + + const memoCount = await countMemoLog(provider, sig, MEMO_TRANSFER_DECREASE_LIQUIDITY); + assert.equal(memoCount, 2); + }); + + it("decrease_liquidity_v2: without memo (has extension, but disabled)", async () => { + const { poolInitInfo, positions } = fixture.getInfos(); + + await enableRequiredMemoTransfers(provider, destAccountA); + await enableRequiredMemoTransfers(provider, destAccountB); + assert.ok(await isRequiredMemoTransfersEnabled(provider, destAccountA)); + assert.ok(await isRequiredMemoTransfersEnabled(provider, destAccountB)); + + await disableRequiredMemoTransfers(provider, destAccountA); + await disableRequiredMemoTransfers(provider, destAccountB); + assert.ok(!(await isRequiredMemoTransfersEnabled(provider, destAccountA))); + assert.ok(!(await isRequiredMemoTransfersEnabled(provider, destAccountB))); + + const sig = await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + ...removalQuote, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: destAccountA, + tokenOwnerAccountB: destAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positions[0].tickArrayLower, + tickArrayUpper: positions[0].tickArrayUpper, + }) + ).buildAndExecute(); + const destBalanceA = await getTokenBalance(provider, destAccountA); + const destBalanceB = await getTokenBalance(provider, destAccountB); + assert.ok(new BN(destBalanceA).gtn(0)); + assert.ok(new BN(destBalanceB).gtn(0)); + + const memoCount = await countMemoLog(provider, sig, MEMO_TRANSFER_DECREASE_LIQUIDITY); + assert.equal(memoCount, 0); + }); + }); + + describe("swap_v2", () => { + let poolInitInfo: InitPoolV2Params; + let whirlpoolPda: PDA; + let tokenAccountA: PublicKey; + let tokenAccountB: PublicKey; + let oraclePubkey: PublicKey; + let quoteAToB: SwapQuote; + let quoteBToA: SwapQuote; + + beforeEach(async () => { + const init = await initTestPoolWithTokensV2( + ctx, + { isToken2022: true }, + { isToken2022: true }, + TickSpacing.Standard + ); + poolInitInfo = init.poolInitInfo; + whirlpoolPda = init.whirlpoolPda; + tokenAccountA = init.tokenAccountA; + tokenAccountB = init.tokenAccountB; + + const aToB = false; + await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, // to 33792 + 3, + TickSpacing.Standard, + aToB + ); + + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + + await fundPositionsV2(ctx, poolInitInfo, tokenAccountA, tokenAccountB, fundParams); + + oraclePubkey = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey).publicKey; + + const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey; + const whirlpoolData = (await fetcher.getPool(whirlpoolKey, IGNORE_CACHE)) as WhirlpoolData; + + quoteAToB = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: true, + tokenAmount: new BN(100000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(true), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + true, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }, + Percentage.fromFraction(100, 100) // 100% slippage + ); + + quoteBToA = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: false, + tokenAmount: new BN(100000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(false), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + false, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }, + Percentage.fromFraction(100, 100) // 100% slippage + ); + }); + + it("swap_v2: without memo", async () => { + const balanceA0 = new BN(await getTokenBalance(provider, tokenAccountA)); + const balanceB0 = new BN(await getTokenBalance(provider, tokenAccountB)); + + const sigBToA = await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteBToA, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }) + ).buildAndExecute(); + + const balanceA1 = new BN(await getTokenBalance(provider, tokenAccountA)); + const balanceB1 = new BN(await getTokenBalance(provider, tokenAccountB)); + assert.ok(balanceB1.lt(balanceB0)); + assert.ok(balanceA1.gt(balanceA0)); + + const memoCountBToA = await countMemoLog(provider, sigBToA, MEMO_TRANSFER_SWAP); + assert.equal(memoCountBToA, 0); + + const sigAToB = await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteAToB, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }) + ).buildAndExecute(); + + const balanceA2 = new BN(await getTokenBalance(provider, tokenAccountA)); + const balanceB2 = new BN(await getTokenBalance(provider, tokenAccountB)); + assert.ok(balanceA2.lt(balanceA1)); + assert.ok(balanceB2.gt(balanceB1)); + + const memoCountAToB = await countMemoLog(provider, sigAToB, MEMO_TRANSFER_SWAP); + assert.equal(memoCountAToB, 0); + }); + + it("swap_v2: with memo", async () => { + await enableRequiredMemoTransfers(provider, tokenAccountA); + await enableRequiredMemoTransfers(provider, tokenAccountB); + assert.ok(await isRequiredMemoTransfersEnabled(provider, tokenAccountA)); + assert.ok(await isRequiredMemoTransfersEnabled(provider, tokenAccountB)); + + const balanceA0 = new BN(await getTokenBalance(provider, tokenAccountA)); + const balanceB0 = new BN(await getTokenBalance(provider, tokenAccountB)); + + const sigBToA = await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteBToA, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }) + ).buildAndExecute(); + + const balanceA1 = new BN(await getTokenBalance(provider, tokenAccountA)); + const balanceB1 = new BN(await getTokenBalance(provider, tokenAccountB)); + assert.ok(balanceB1.lt(balanceB0)); + assert.ok(balanceA1.gt(balanceA0)); + + const memoCountBToA = await countMemoLog(provider, sigBToA, MEMO_TRANSFER_SWAP); + assert.equal(memoCountBToA, 1); + + const sigAToB = await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteAToB, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }) + ).buildAndExecute(); + + const balanceA2 = new BN(await getTokenBalance(provider, tokenAccountA)); + const balanceB2 = new BN(await getTokenBalance(provider, tokenAccountB)); + assert.ok(balanceA2.lt(balanceA1)); + assert.ok(balanceB2.gt(balanceB1)); + + const memoCountAToB = await countMemoLog(provider, sigAToB, MEMO_TRANSFER_SWAP); + assert.equal(memoCountAToB, 1); + }); + + it("swap_v2: without memo (has extension, but disabled", async () => { + await enableRequiredMemoTransfers(provider, tokenAccountA); + await enableRequiredMemoTransfers(provider, tokenAccountB); + assert.ok(await isRequiredMemoTransfersEnabled(provider, tokenAccountA)); + assert.ok(await isRequiredMemoTransfersEnabled(provider, tokenAccountB)); + + await disableRequiredMemoTransfers(provider, tokenAccountA); + await disableRequiredMemoTransfers(provider, tokenAccountB); + assert.ok(!(await isRequiredMemoTransfersEnabled(provider, tokenAccountA))); + assert.ok(!(await isRequiredMemoTransfersEnabled(provider, tokenAccountB))); + + const balanceA0 = new BN(await getTokenBalance(provider, tokenAccountA)); + const balanceB0 = new BN(await getTokenBalance(provider, tokenAccountB)); + + const sigBToA = await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteBToA, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }) + ).buildAndExecute(); + + const balanceA1 = new BN(await getTokenBalance(provider, tokenAccountA)); + const balanceB1 = new BN(await getTokenBalance(provider, tokenAccountB)); + assert.ok(balanceB1.lt(balanceB0)); + assert.ok(balanceA1.gt(balanceA0)); + + const memoCountBToA = await countMemoLog(provider, sigBToA, MEMO_TRANSFER_SWAP); + assert.equal(memoCountBToA, 0); + + const sigAToB = await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteAToB, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }) + ).buildAndExecute(); + + const balanceA2 = new BN(await getTokenBalance(provider, tokenAccountA)); + const balanceB2 = new BN(await getTokenBalance(provider, tokenAccountB)); + assert.ok(balanceA2.lt(balanceA1)); + assert.ok(balanceB2.gt(balanceB1)); + + const memoCountAToB = await countMemoLog(provider, sigAToB, MEMO_TRANSFER_SWAP); + assert.equal(memoCountAToB, 0); + }); + }); + + describe("two_hop_swap", () => { + let aqConfig: InitAquariumV2Params; + let baseIxParams: TwoHopSwapV2Params; + let tokenAccountIn: PublicKey; + let tokenAccountOut: PublicKey; + + beforeEach(async () => { + aqConfig = getDefaultAquariumV2(); + // Add a third token and account and a second pool + aqConfig.initMintParams = [ + { tokenTrait: { isToken2022: true } }, + { tokenTrait: { isToken2022: true } }, + { tokenTrait: { isToken2022: true } }, + ]; + aqConfig.initTokenAccParams.push({ mintIndex: 2 }); + aqConfig.initPoolParams.push({ mintIndices: [1, 2], tickSpacing: TickSpacing.Standard }); + + // Add tick arrays and positions + const aToB = false; + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 0, + startTickIndex: 22528, + arrayCount: 3, + aToB, + }); + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 1, + startTickIndex: 22528, + arrayCount: 3, + aToB, + }); + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + aqConfig.initPositionParams.push({ poolIndex: 0, fundParams }); + aqConfig.initPositionParams.push({ poolIndex: 1, fundParams }); + + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [inputToken, intermediaryToken, _outputToken] = mintKeys; + const aToBOne = whirlpoolDataOne.tokenMintA.equals(inputToken); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(intermediaryToken); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: quote.estimatedAmountOut, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const tokenAccKeys = getTokenAccsForPoolsV2(pools, tokenAccounts); + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + baseIxParams = { + ...twoHopQuote, + tokenAuthority: ctx.wallet.publicKey, + whirlpoolOne: pools[0].whirlpoolPda.publicKey, + whirlpoolTwo: pools[1].whirlpoolPda.publicKey, + tokenMintInput: twoHopQuote.aToBOne ? pools[0].tokenMintA : pools[0].tokenMintB, + tokenMintIntermediate: twoHopQuote.aToBOne ? pools[0].tokenMintB : pools[0].tokenMintA, + tokenMintOutput: twoHopQuote.aToBTwo ? pools[1].tokenMintB : pools[1].tokenMintA, + tokenProgramInput: twoHopQuote.aToBOne ? pools[0].tokenProgramA : pools[0].tokenProgramB, + tokenProgramIntermediate: twoHopQuote.aToBOne ? pools[0].tokenProgramB : pools[0].tokenProgramA, + tokenProgramOutput: twoHopQuote.aToBTwo ? pools[1].tokenProgramB : pools[1].tokenProgramA, + tokenOwnerAccountInput: twoHopQuote.aToBOne ? tokenAccKeys[0] : tokenAccKeys[1], + tokenOwnerAccountOutput: twoHopQuote.aToBTwo ? tokenAccKeys[3] : tokenAccKeys[2], + tokenVaultOneInput: twoHopQuote.aToBOne ? pools[0].tokenVaultAKeypair.publicKey : pools[0].tokenVaultBKeypair.publicKey, + tokenVaultOneIntermediate: twoHopQuote.aToBOne ? pools[0].tokenVaultBKeypair.publicKey : pools[0].tokenVaultAKeypair.publicKey, + tokenVaultTwoIntermediate: twoHopQuote.aToBTwo ? pools[1].tokenVaultAKeypair.publicKey : pools[1].tokenVaultBKeypair.publicKey, + tokenVaultTwoOutput: twoHopQuote.aToBTwo ? pools[1].tokenVaultBKeypair.publicKey : pools[1].tokenVaultAKeypair.publicKey, + oracleOne: PDAUtil.getOracle(ctx.program.programId, pools[0].whirlpoolPda.publicKey) + .publicKey, + oracleTwo: PDAUtil.getOracle(ctx.program.programId, pools[1].whirlpoolPda.publicKey) + .publicKey, + }; + + tokenAccountIn = baseIxParams.tokenOwnerAccountInput; + tokenAccountOut = baseIxParams.tokenOwnerAccountOutput; + }); + + it("two_hop_swap_v2: without memo", async () => { + const preBalanceIn = new BN(await getTokenBalance(provider, tokenAccountIn)); + const preBalanceOut = new BN(await getTokenBalance(provider, tokenAccountOut)); + + const sig = await toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, baseIxParams) + ).buildAndExecute(); + + const postBalanceIn = new BN(await getTokenBalance(provider, tokenAccountIn)); + const postBalanceOut = new BN(await getTokenBalance(provider, tokenAccountOut)); + assert.ok(postBalanceIn.lt(preBalanceIn)); + assert.ok(postBalanceOut.gt(preBalanceOut)); + + const memoCount = await countMemoLog(provider, sig, MEMO_TRANSFER_SWAP); + assert.equal(memoCount, 0); + }); + + it("two_hop_swap_v2: with memo", async () => { + await enableRequiredMemoTransfers(provider, tokenAccountIn); + await enableRequiredMemoTransfers(provider, tokenAccountOut); + assert.ok(await isRequiredMemoTransfersEnabled(provider, tokenAccountIn)); + assert.ok(await isRequiredMemoTransfersEnabled(provider, tokenAccountOut)); + + const preBalanceIn = new BN(await getTokenBalance(provider, tokenAccountIn)); + const preBalanceOut = new BN(await getTokenBalance(provider, tokenAccountOut)); + + const sig = await toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, baseIxParams) + ).buildAndExecute(); + + const postBalanceIn = new BN(await getTokenBalance(provider, tokenAccountIn)); + const postBalanceOut = new BN(await getTokenBalance(provider, tokenAccountOut)); + assert.ok(postBalanceIn.lt(preBalanceIn)); + assert.ok(postBalanceOut.gt(preBalanceOut)); + + const memoCount = await countMemoLog(provider, sig, MEMO_TRANSFER_SWAP); + assert.equal(memoCount, 1); // mid token uses vault to vault transfer, so no memo + }); + + it("two_hop_swap_v2: without memo (has extension, but disabled", async () => { + await enableRequiredMemoTransfers(provider, tokenAccountIn); + await enableRequiredMemoTransfers(provider, tokenAccountOut); + assert.ok(await isRequiredMemoTransfersEnabled(provider, tokenAccountIn)); + assert.ok(await isRequiredMemoTransfersEnabled(provider, tokenAccountOut)); + + await disableRequiredMemoTransfers(provider, tokenAccountIn); + await disableRequiredMemoTransfers(provider, tokenAccountOut); + assert.ok(!(await isRequiredMemoTransfersEnabled(provider, tokenAccountIn))); + assert.ok(!(await isRequiredMemoTransfersEnabled(provider, tokenAccountOut))); + + const preBalanceIn = new BN(await getTokenBalance(provider, tokenAccountIn)); + const preBalanceOut = new BN(await getTokenBalance(provider, tokenAccountOut)); + + const sig = await toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, baseIxParams) + ).buildAndExecute(); + + const postBalanceIn = new BN(await getTokenBalance(provider, tokenAccountIn)); + const postBalanceOut = new BN(await getTokenBalance(provider, tokenAccountOut)); + assert.ok(postBalanceIn.lt(preBalanceIn)); + assert.ok(postBalanceOut.gt(preBalanceOut)); + + const memoCount = await countMemoLog(provider, sig, MEMO_TRANSFER_SWAP); + assert.equal(memoCount, 0); + }); + }); +}); + +async function countMemoLog( + provider: anchor.AnchorProvider, + signature: string, + logMessage: string +): Promise { + const logLen = logMessage.length; + const logFormat = `Program log: Memo (len ${logLen}): "${logMessage}"`; + + const tx = await provider.connection.getParsedTransaction(signature, { + maxSupportedTransactionVersion: 0, + }); + const memos = tx?.meta?.logMessages?.filter((msg) => msg === logFormat); + return memos!.length; +} diff --git a/sdk/tests/integration/v2/token-extensions/non-confidential-transfer.test.ts b/sdk/tests/integration/v2/token-extensions/non-confidential-transfer.test.ts new file mode 100644 index 000000000..735787226 --- /dev/null +++ b/sdk/tests/integration/v2/token-extensions/non-confidential-transfer.test.ts @@ -0,0 +1,846 @@ +import * as anchor from "@coral-xyz/anchor"; +import { BN } from "@coral-xyz/anchor"; +import { MathUtil, PDA, Percentage } from "@orca-so/common-sdk"; +import * as assert from "assert"; +import Decimal from "decimal.js"; +import { + buildWhirlpoolClient, + collectRewardsQuote, + DecreaseLiquidityQuote, + decreaseLiquidityQuoteByLiquidityWithParams, + InitPoolV2Params, + NUM_REWARDS, + PDAUtil, + PoolUtil, + PositionData, + PriceMath, + SwapQuote, + swapQuoteWithParams, + SwapUtils, + toTokenAmount, + toTx, + twoHopSwapQuoteFromSwapQuotes, + TwoHopSwapV2Params, + WhirlpoolContext, + WhirlpoolData, + WhirlpoolIx, +} from "../../../../src"; +import { IGNORE_CACHE } from "../../../../src/network/public/fetcher"; +import { + getTokenBalance, + sleep, + TickSpacing, + ZERO_BN, +} from "../../../utils"; +import { defaultConfirmOptions } from "../../../utils/const"; +import { WhirlpoolTestFixtureV2 } from "../../../utils/v2/fixture-v2"; +import { + FundedPositionV2Params, + fundPositionsV2, + initTestPoolWithTokensV2, + useMaxCU, +} from "../../../utils/v2/init-utils-v2"; +import { + createTokenAccountV2, +} from "../../../utils/v2/token-2022"; +import { AccountMeta, ComputeBudgetProgram, PublicKey } from "@solana/web3.js"; +import { initTickArrayRange } from "../../../utils/init-utils"; +import { + InitAquariumV2Params, + buildTestAquariumsV2, + getDefaultAquariumV2, + getTokenAccsForPoolsV2, +} from "../../../utils/v2/aquarium-v2"; +import { getExtraAccountMetasForTestTransferHookProgram, getTestTransferHookCounter, updateTransferHookProgram } from "../../../utils/v2/test-transfer-hook-program"; +import { hasConfidentialTransferMintExtension } from "../../../utils/v2/confidential-transfer"; +import { TokenExtensionUtil } from "../../../../src/utils/public/token-extension-util"; + +describe("TokenExtension/ConfidentialTransfer (NON confidential transfer only)", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + const client = buildWhirlpoolClient(ctx); + + describe("collect_fees_v2, collect_protocol_fees_v2", () => { + let fixture: WhirlpoolTestFixtureV2; + let feeAccountA: PublicKey; + let feeAccountB: PublicKey; + + beforeEach(async () => { + // In same tick array - start index 22528 + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + + const tickSpacing = TickSpacing.Standard; + fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true, hasConfidentialTransferExtension: true }, + tokenTraitB: { isToken2022: true, hasConfidentialTransferExtension: true }, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }, // In range position + { tickLowerIndex: 0, tickUpperIndex: 128, liquidityAmount: new anchor.BN(1_000_000) }, // Out of range position + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + const tickArrayPda = PDAUtil.getTickArray( + ctx.program.programId, + whirlpoolPda.publicKey, + 22528 + ); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + // Accrue fees in token A + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + }) + ).prependInstruction(useMaxCU()).buildAndExecute(); + + // Accrue fees in token B + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(5)), + amountSpecifiedIsInput: true, + aToB: false, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + }) + ).prependInstruction(useMaxCU()).buildAndExecute(); + + await toTx( + ctx, + WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + position: positions[0].publicKey, + tickArrayLower: tickArrayPda.publicKey, + tickArrayUpper: tickArrayPda.publicKey, + }) + ).buildAndExecute(); + + const whirlpoolData = (await fetcher.getPool(whirlpoolPda.publicKey, IGNORE_CACHE))!; + assert.ok(!whirlpoolData.protocolFeeOwedA.isZero()); + assert.ok(!whirlpoolData.protocolFeeOwedB.isZero()); + + const positionBeforeCollect = (await fetcher.getPosition( + positions[0].publicKey, + IGNORE_CACHE + )) as PositionData; + assert.ok(!positionBeforeCollect.feeOwedA.isZero()); + assert.ok(!positionBeforeCollect.feeOwedB.isZero()); + + feeAccountA = await createTokenAccountV2( + provider, + { isToken2022: true }, + tokenMintA, + provider.wallet.publicKey + ); + feeAccountB = await createTokenAccountV2( + provider, + { isToken2022: true }, + tokenMintB, + provider.wallet.publicKey + ); + }); + + it("collect_fees_v2: non confidential transfer", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + assert.ok(await hasConfidentialTransferMintExtension(provider, tokenMintA)); + assert.ok(await hasConfidentialTransferMintExtension(provider, tokenMintB)); + + await toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }) + ).buildAndExecute(); + + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(feeBalanceA).gtn(0)); + assert.ok(new BN(feeBalanceB).gtn(0)); + }); + + it("collect_protocol_fees_v2: non confidential transfer", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKeypair }, + } = fixture.getInfos(); + + assert.ok(await hasConfidentialTransferMintExtension(provider, tokenMintA)); + assert.ok(await hasConfidentialTransferMintExtension(provider, tokenMintB)); + + await toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(); + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(feeBalanceA).gtn(0)); + assert.ok(new BN(feeBalanceB).gtn(0)); + }); + }); + + describe("collect_reward_v2", () => { + let fixture: WhirlpoolTestFixtureV2; + let rewardAccounts: PublicKey[]; + + beforeEach(async () => { + const vaultStartBalance = 1_000_000; + const lowerTickIndex = -1280, + upperTickIndex = 1280, + tickSpacing = TickSpacing.Standard; + fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: tickSpacing, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: lowerTickIndex, + tickUpperIndex: upperTickIndex, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: { isToken2022: true, hasConfidentialTransferExtension: true }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: { isToken2022: true, hasConfidentialTransferExtension: true }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: { isToken2022: true, hasConfidentialTransferExtension: true }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + // accrue rewards + await sleep(3000); + + await toTx( + ctx, + WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + position: positions[0].publicKey, + tickArrayLower: positions[0].tickArrayLower, + tickArrayUpper: positions[0].tickArrayUpper, + }) + ).buildAndExecute(); + + // Generate collect reward expectation + const whirlpoolData = (await fetcher.getPool(whirlpoolPda.publicKey)) as WhirlpoolData; + const positionPreCollect = await client.getPosition(positions[0].publicKey, IGNORE_CACHE); + + // Lock the collectRewards quote to the last time we called updateFeesAndRewards + const expectation = collectRewardsQuote({ + whirlpool: whirlpoolData, + position: positionPreCollect.getData(), + tickLower: positionPreCollect.getLowerTickData(), + tickUpper: positionPreCollect.getUpperTickData(), + timeStampInSeconds: whirlpoolData.rewardLastUpdatedTimestamp, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }); + + // Check that the expectation is not zero + for (let i = 0; i < NUM_REWARDS; i++) { + assert.ok(!expectation.rewardOwed[i]!.isZero()); + } + + rewardAccounts = await Promise.all( + rewards.map((reward) => { + return createTokenAccountV2( + provider, + { isToken2022: true }, + reward.rewardMint, + provider.wallet.publicKey + ); + }) + ); + }); + + it("collect_reward_v2: non confidential transfer", async () => { + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + for (let i = 0; i < NUM_REWARDS; i++) { + assert.ok(hasConfidentialTransferMintExtension(provider, rewards[i].rewardMint)); + + await toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[i].rewardMint, + rewardTokenProgram: rewards[i].tokenProgram, + rewardOwnerAccount: rewardAccounts[i], + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + rewardIndex: i, + }) + ).buildAndExecute(); + const rewardBalance = await getTokenBalance(provider, rewardAccounts[i]); + assert.ok(new BN(rewardBalance).gtn(0)); + } + }); + }); + + describe("increase_liquidity_v2", () => { + const tickLowerIndex = 7168; + const tickUpperIndex = 8960; + const currTick = Math.round((tickLowerIndex + tickUpperIndex) / 2); + + let fixture: WhirlpoolTestFixtureV2; + + beforeEach(async () => { + fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true, hasConfidentialTransferExtension: true}, + tokenTraitB: { isToken2022: true, hasConfidentialTransferExtension: true}, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + }); + }); + + it("increase_liquidity_v2: non confidential transfer", async () => { + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 1_000_000); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + assert.ok(await hasConfidentialTransferMintExtension(provider, poolInitInfo.tokenMintA)); + assert.ok(await hasConfidentialTransferMintExtension(provider, poolInitInfo.tokenMintB)); + + const preVaultBalanceA = await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey); + const preVaultBalanceB = await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey); + + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }) + ).buildAndExecute(); + + const postVaultBalanceA = await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey); + const postVaultBalanceB = await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey); + assert.ok(new BN(postVaultBalanceA).gt(new BN(preVaultBalanceA))); + assert.ok(new BN(postVaultBalanceB).gt(new BN(preVaultBalanceB))); + }); + }); + + describe("decrease_liquidity_v2", () => { + let fixture: WhirlpoolTestFixtureV2; + let removalQuote: DecreaseLiquidityQuote; + let destAccountA: PublicKey; + let destAccountB: PublicKey; + + beforeEach(async () => { + const liquidityAmount = new anchor.BN(1_250_000); + const tickLower = 7168, + tickUpper = 8960; + fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true, hasConfidentialTransferExtension: true }, + tokenTraitB: { isToken2022: true, hasConfidentialTransferExtension: true }, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1.48)), + positions: [{ tickLowerIndex: tickLower, tickUpperIndex: tickUpper, liquidityAmount }], + }); + const { poolInitInfo } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const poolBefore = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + + removalQuote = decreaseLiquidityQuoteByLiquidityWithParams({ + liquidity: new anchor.BN(1_000_000), + sqrtPrice: poolBefore.sqrtPrice, + slippageTolerance: Percentage.fromFraction(1, 100), + tickCurrentIndex: poolBefore.tickCurrentIndex, + tickLowerIndex: tickLower, + tickUpperIndex: tickUpper, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolBefore, IGNORE_CACHE), + }); + assert.ok(!removalQuote.tokenEstA.isZero()); + assert.ok(!removalQuote.tokenEstB.isZero()); + + destAccountA = await createTokenAccountV2( + provider, + { isToken2022: true }, + poolInitInfo.tokenMintA, + provider.wallet.publicKey + ); + destAccountB = await createTokenAccountV2( + provider, + { isToken2022: true }, + poolInitInfo.tokenMintB, + provider.wallet.publicKey + ); + }); + + it("decrease_liquidity_v2: non confidential transfer", async () => { + const { poolInitInfo, positions } = fixture.getInfos(); + + assert.ok(await hasConfidentialTransferMintExtension(provider, poolInitInfo.tokenMintA)); + assert.ok(await hasConfidentialTransferMintExtension(provider, poolInitInfo.tokenMintB)); + + await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + ...removalQuote, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: destAccountA, + tokenOwnerAccountB: destAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positions[0].tickArrayLower, + tickArrayUpper: positions[0].tickArrayUpper, + }) + ).buildAndExecute(); + const destBalanceA = await getTokenBalance(provider, destAccountA); + const destBalanceB = await getTokenBalance(provider, destAccountB); + assert.ok(new BN(destBalanceA).gtn(0)); + assert.ok(new BN(destBalanceB).gtn(0)); + }); + }); + + describe("swap_v2", () => { + let poolInitInfo: InitPoolV2Params; + let whirlpoolPda: PDA; + let tokenAccountA: PublicKey; + let tokenAccountB: PublicKey; + let oraclePubkey: PublicKey; + let quoteAToB: SwapQuote; + let quoteBToA: SwapQuote; + + beforeEach(async () => { + const init = await initTestPoolWithTokensV2( + ctx, + { isToken2022: true, hasConfidentialTransferExtension: true }, + { isToken2022: true, hasConfidentialTransferExtension: true }, + TickSpacing.Standard + ); + poolInitInfo = init.poolInitInfo; + whirlpoolPda = init.whirlpoolPda; + tokenAccountA = init.tokenAccountA; + tokenAccountB = init.tokenAccountB; + + const aToB = false; + await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, // to 33792 + 3, + TickSpacing.Standard, + aToB + ); + + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + + await fundPositionsV2(ctx, poolInitInfo, tokenAccountA, tokenAccountB, fundParams); + + oraclePubkey = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey).publicKey; + + const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey; + const whirlpoolData = (await fetcher.getPool(whirlpoolKey, IGNORE_CACHE)) as WhirlpoolData; + + quoteAToB = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: true, + tokenAmount: new BN(100000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(true), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + true, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }, + Percentage.fromFraction(100, 100) // 100% slippage + ); + + quoteBToA = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: false, + tokenAmount: new BN(100000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(false), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + false, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }, + Percentage.fromFraction(100, 100) // 100% slippage + ); + }); + + it("swap_v2: non confidential transfer, a to b", async () => { + assert.ok(await hasConfidentialTransferMintExtension(provider, poolInitInfo.tokenMintA)); + assert.ok(await hasConfidentialTransferMintExtension(provider, poolInitInfo.tokenMintB)); + + const preBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const preBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteAToB, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }) + ).buildAndExecute(); + + const postBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const postBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + assert.ok(postBalanceA.lt(preBalanceA)); + assert.ok(postBalanceB.gt(preBalanceB)); + }); + + it("swap_v2: non confidential transfer, b to a", async () => { + assert.ok(await hasConfidentialTransferMintExtension(provider, poolInitInfo.tokenMintA)); + assert.ok(await hasConfidentialTransferMintExtension(provider, poolInitInfo.tokenMintB)); + + const preBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const preBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteBToA, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }) + ).buildAndExecute(); + + const postBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const postBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + assert.ok(postBalanceA.gt(preBalanceA)); + assert.ok(postBalanceB.lt(preBalanceB)); + }); + }); + + describe("two_hop_swap", () => { + let aqConfig: InitAquariumV2Params; + let baseIxParams: TwoHopSwapV2Params; + let tokenAccountIn: PublicKey; + let tokenAccountOut: PublicKey; + + beforeEach(async () => { + aqConfig = getDefaultAquariumV2(); + // Add a third token and account and a second pool + aqConfig.initMintParams = [ + { tokenTrait: { isToken2022: true, hasConfidentialTransferExtension: true } }, + { tokenTrait: { isToken2022: true, hasConfidentialTransferExtension: true } }, + { tokenTrait: { isToken2022: true, hasConfidentialTransferExtension: true } }, + ]; + aqConfig.initTokenAccParams.push({ mintIndex: 2 }); + aqConfig.initPoolParams.push({ mintIndices: [1, 2], tickSpacing: TickSpacing.Standard }); + + // Add tick arrays and positions + const aToB = false; + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 0, + startTickIndex: 22528, + arrayCount: 3, + aToB, + }); + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 1, + startTickIndex: 22528, + arrayCount: 3, + aToB, + }); + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + aqConfig.initPositionParams.push({ poolIndex: 0, fundParams }); + aqConfig.initPositionParams.push({ poolIndex: 1, fundParams }); + + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [inputToken, intermediaryToken, _outputToken] = mintKeys; + const aToBOne = whirlpoolDataOne.tokenMintA.equals(inputToken); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(intermediaryToken); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: quote.estimatedAmountOut, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const tokenAccKeys = getTokenAccsForPoolsV2(pools, tokenAccounts); + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + baseIxParams = { + ...twoHopQuote, + tokenAuthority: ctx.wallet.publicKey, + whirlpoolOne: pools[0].whirlpoolPda.publicKey, + whirlpoolTwo: pools[1].whirlpoolPda.publicKey, + tokenMintInput: twoHopQuote.aToBOne ? pools[0].tokenMintA : pools[0].tokenMintB, + tokenMintIntermediate: twoHopQuote.aToBOne ? pools[0].tokenMintB : pools[0].tokenMintA, + tokenMintOutput: twoHopQuote.aToBTwo ? pools[1].tokenMintB : pools[1].tokenMintA, + tokenProgramInput: twoHopQuote.aToBOne ? pools[0].tokenProgramA : pools[0].tokenProgramB, + tokenProgramIntermediate: twoHopQuote.aToBOne ? pools[0].tokenProgramB : pools[0].tokenProgramA, + tokenProgramOutput: twoHopQuote.aToBTwo ? pools[1].tokenProgramB : pools[1].tokenProgramA, + tokenOwnerAccountInput: twoHopQuote.aToBOne ? tokenAccKeys[0] : tokenAccKeys[1], + tokenOwnerAccountOutput: twoHopQuote.aToBTwo ? tokenAccKeys[3] : tokenAccKeys[2], + tokenVaultOneInput: twoHopQuote.aToBOne ? pools[0].tokenVaultAKeypair.publicKey : pools[0].tokenVaultBKeypair.publicKey, + tokenVaultOneIntermediate: twoHopQuote.aToBOne ? pools[0].tokenVaultBKeypair.publicKey : pools[0].tokenVaultAKeypair.publicKey, + tokenVaultTwoIntermediate: twoHopQuote.aToBTwo ? pools[1].tokenVaultAKeypair.publicKey : pools[1].tokenVaultBKeypair.publicKey, + tokenVaultTwoOutput: twoHopQuote.aToBTwo ? pools[1].tokenVaultBKeypair.publicKey : pools[1].tokenVaultAKeypair.publicKey, + oracleOne: PDAUtil.getOracle(ctx.program.programId, pools[0].whirlpoolPda.publicKey) + .publicKey, + oracleTwo: PDAUtil.getOracle(ctx.program.programId, pools[1].whirlpoolPda.publicKey) + .publicKey, + }; + + tokenAccountIn = baseIxParams.tokenOwnerAccountInput; + tokenAccountOut = baseIxParams.tokenOwnerAccountOutput; + }); + + it("two_hop_swap_v2: non confidential transfer", async () => { + const preBalanceIn = new BN(await getTokenBalance(provider, tokenAccountIn)); + const preBalanceOut = new BN(await getTokenBalance(provider, tokenAccountOut)); + + await toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, baseIxParams) + ).buildAndExecute(); + + const postBalanceIn = new BN(await getTokenBalance(provider, tokenAccountIn)); + const postBalanceOut = new BN(await getTokenBalance(provider, tokenAccountOut)); + assert.ok(postBalanceIn.lt(preBalanceIn)); + assert.ok(postBalanceOut.gt(preBalanceOut)); + }); + }); + +}); diff --git a/sdk/tests/integration/v2/token-extensions/transfer-fee.test.ts b/sdk/tests/integration/v2/token-extensions/transfer-fee.test.ts new file mode 100644 index 000000000..5bfbfe3f6 --- /dev/null +++ b/sdk/tests/integration/v2/token-extensions/transfer-fee.test.ts @@ -0,0 +1,7259 @@ +import * as anchor from "@coral-xyz/anchor"; +import { BN } from "@coral-xyz/anchor"; +import { MathUtil, MintWithTokenProgram, PDA, Percentage, U64_MAX } from "@orca-so/common-sdk"; +import * as assert from "assert"; +import Decimal from "decimal.js"; +import { + buildWhirlpoolClient, + collectRewardsQuote, + DecreaseLiquidityV2Params, + IncreaseLiquidityV2Params, + InitPoolV2Params, + MEMO_PROGRAM_ADDRESS, + NUM_REWARDS, + PDAUtil, + PoolUtil, + PositionData, + PriceMath, + swapQuoteWithParams, + SwapUtils, + TickUtil, + toTokenAmount, + toTx, + twoHopSwapQuoteFromSwapQuotes, + TwoHopSwapV2Params, + WhirlpoolContext, + WhirlpoolData, + WhirlpoolIx, +} from "../../../../src"; +import { IGNORE_CACHE } from "../../../../src/network/public/fetcher"; +import { + getTokenBalance, + sleep, + TEST_TOKEN_2022_PROGRAM_ID, + TEST_TOKEN_PROGRAM_ID, + TickSpacing, + ZERO_BN, +} from "../../../utils"; +import { defaultConfirmOptions } from "../../../utils/const"; +import { WhirlpoolTestFixtureV2 } from "../../../utils/v2/fixture-v2"; +import { + FundedPositionV2Params, + TokenTrait, + fundPositionsV2, + initTestPoolWithTokensV2, + useMaxCU, +} from "../../../utils/v2/init-utils-v2"; +import { + calculateTransferFeeExcludedAmount, + calculateTransferFeeIncludedAmount, + createTokenAccountV2, +} from "../../../utils/v2/token-2022"; +import { PublicKey } from "@solana/web3.js"; +import { initTickArrayRange } from "../../../utils/init-utils"; +import { + InitAquariumV2Params, + TestAquarium, + buildTestAquariumsV2, + getDefaultAquariumV2, + getTokenAccsForPoolsV2, +} from "../../../utils/v2/aquarium-v2"; +import { + MAX_FEE_BASIS_POINTS, + TransferFee, + TransferFeeConfig, + getAccount, + getEpochFee, + getMint, + getTransferFeeAmount, + getTransferFeeConfig, +} from "@solana/spl-token"; +import { createSetTransferFeeInstruction } from "../../../utils/v2/transfer-fee"; +import { TokenExtensionContext, TokenExtensionUtil } from "../../../../src/utils/public/token-extension-util"; + +describe("TokenExtension/TransferFee", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + const client = buildWhirlpoolClient(ctx); + + const dummyTokenMintWithProgram: MintWithTokenProgram = { + address: PublicKey.default, + decimals: 0, + freezeAuthority: null, + isInitialized: true, + mintAuthority: null, + supply: 1_000_000_000n, + tlvData: Buffer.from([]), + tokenProgram: TEST_TOKEN_PROGRAM_ID, + } + + const withNoExtension: TokenExtensionContext = { + currentEpoch: 100, + tokenMintWithProgramA: dummyTokenMintWithProgram, + tokenMintWithProgramB: dummyTokenMintWithProgram, + rewardTokenMintsWithProgram: [ + dummyTokenMintWithProgram, + dummyTokenMintWithProgram, + dummyTokenMintWithProgram, + ], + }; + + async function getTransferFee(mint: PublicKey): Promise { + const mintData = await getMint( + provider.connection, + mint, + undefined, + TEST_TOKEN_2022_PROGRAM_ID, + ); + const transferFeeConfig = getTransferFeeConfig(mintData); + assert.ok(transferFeeConfig !== null); + + const epochInfo = await provider.connection.getEpochInfo(); + const transferFee = getEpochFee(transferFeeConfig, BigInt(epochInfo.epoch)); + return transferFee; + } + + const WAIT_EPOCH_TIMEOUT_MS = 30 * 1000; + + async function getCurrentEpoch(): Promise { + const epochInfo = await provider.connection.getEpochInfo("confirmed"); + return epochInfo.epoch; + } + + async function waitEpoch(waitForEpoch: number) { + const current = await getCurrentEpoch(); + const startWait = Date.now(); + + while (Date.now() - startWait < WAIT_EPOCH_TIMEOUT_MS) { + const epoch = await getCurrentEpoch(); + if (epoch >= waitForEpoch) return; + sleep(1000); + } + throw Error("waitEpoch Timeout, Please set slots_per_epoch smaller in Anchor.toml"); + } + + async function fetchTransferFeeConfig(mint: PublicKey): Promise { + const mintData = await getMint(provider.connection, mint, "confirmed", TEST_TOKEN_2022_PROGRAM_ID); + const config = getTransferFeeConfig(mintData); + assert.ok(config !== null); + return config!; + } + + async function fetchTransferFeeWithheldAmount(account: PublicKey): Promise { + const accountData = await getAccount(provider.connection, account, "confirmed", TEST_TOKEN_2022_PROGRAM_ID); + const amount = getTransferFeeAmount(accountData); + assert.ok(amount !== null); + return new BN(amount.withheldAmount.toString()); + } + + describe("collect_fees_v2, collect_protocol_fees_v2", () => { + let fixture: WhirlpoolTestFixtureV2; + let feeAccountA: PublicKey; + let feeAccountB: PublicKey; + + beforeEach(async () => { + // In same tick array - start index 22528 + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + + const tickSpacing = TickSpacing.Standard; + fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { + isToken2022: true, + hasTransferFeeExtension: true, + transferFeeInitialBps: 500, + }, // 5% + tokenTraitB: { + isToken2022: true, + hasTransferFeeExtension: true, + transferFeeInitialBps: 1000, + }, // 10% + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }, // In range position + { tickLowerIndex: 0, tickUpperIndex: 128, liquidityAmount: new anchor.BN(1_000_000) }, // Out of range position + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + const tickArrayPda = PDAUtil.getTickArray( + ctx.program.programId, + whirlpoolPda.publicKey, + 22528, + ); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + // Accrue fees in token A + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + }), + ).buildAndExecute(); + + // Accrue fees in token B + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(5)), + amountSpecifiedIsInput: true, + aToB: false, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + }), + ).buildAndExecute(); + + await toTx( + ctx, + WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + position: positions[0].publicKey, + tickArrayLower: tickArrayPda.publicKey, + tickArrayUpper: tickArrayPda.publicKey, + }), + ).buildAndExecute(); + + const whirlpoolData = (await fetcher.getPool(whirlpoolPda.publicKey, IGNORE_CACHE))!; + assert.ok(!whirlpoolData.protocolFeeOwedA.isZero()); + assert.ok(!whirlpoolData.protocolFeeOwedB.isZero()); + + const positionBeforeCollect = (await fetcher.getPosition( + positions[0].publicKey, + IGNORE_CACHE, + )) as PositionData; + assert.ok(!positionBeforeCollect.feeOwedA.isZero()); + assert.ok(!positionBeforeCollect.feeOwedB.isZero()); + + feeAccountA = await createTokenAccountV2( + provider, + { isToken2022: true }, + tokenMintA, + provider.wallet.publicKey, + ); + feeAccountB = await createTokenAccountV2( + provider, + { isToken2022: true }, + tokenMintB, + provider.wallet.publicKey, + ); + }); + + it("collect_fees_v2: with transfer fee", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + const transferFeeA = await getTransferFee(tokenMintA); + const transferFeeB = await getTransferFee(tokenMintB); + assert.equal(transferFeeA.transferFeeBasisPoints, 500); // 5% + assert.equal(transferFeeB.transferFeeBasisPoints, 1000); // 10% + + // feeOwed includes transfer fee + const positionBeforeCollect = (await fetcher.getPosition( + positions[0].publicKey, + IGNORE_CACHE, + )) as PositionData; + assert.ok(!positionBeforeCollect.feeOwedA.isZero()); + assert.ok(!positionBeforeCollect.feeOwedB.isZero()); + + // transfer fee should be non zero + const expectedTransferFeeExcludedAmountA = calculateTransferFeeExcludedAmount( + transferFeeA, + positionBeforeCollect.feeOwedA, + ); + const expectedTransferFeeExcludedAmountB = calculateTransferFeeExcludedAmount( + transferFeeB, + positionBeforeCollect.feeOwedB, + ); + assert.ok(expectedTransferFeeExcludedAmountA.fee.gtn(0)); + assert.ok(expectedTransferFeeExcludedAmountB.fee.gtn(0)); + + const preVaultBalanceA = await getTokenBalance(provider, tokenVaultAKeypair.publicKey); + const preVaultBalanceB = await getTokenBalance(provider, tokenVaultBKeypair.publicKey); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }), + ).buildAndExecute(); + + // vault sent owed only (transfer fee is paid from owed) + const postVaultBalanceA = await getTokenBalance(provider, tokenVaultAKeypair.publicKey); + const postVaultBalanceB = await getTokenBalance(provider, tokenVaultBKeypair.publicKey); + assert.ok( + new BN(preVaultBalanceA).sub(new BN(postVaultBalanceA)).eq(positionBeforeCollect.feeOwedA), + ); + assert.ok( + new BN(preVaultBalanceB).sub(new BN(postVaultBalanceB)).eq(positionBeforeCollect.feeOwedB), + ); + + // owner received feeOwed minus transfer fee (transferFeeExcludedAmount) + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(feeBalanceA).eq(expectedTransferFeeExcludedAmountA.amount)); + assert.ok(new BN(feeBalanceB).eq(expectedTransferFeeExcludedAmountB.amount)); + + //console.log("A", positionBeforeCollect.feeOwedA.toString(), feeBalanceA.toString(), expectedTransferFeeExcludedAmountA.amount.toString(), expectedTransferFeeExcludedAmountA.fee.toString()); + //console.log("B", positionBeforeCollect.feeOwedB.toString(), feeBalanceB.toString(), expectedTransferFeeExcludedAmountB.amount.toString(), expectedTransferFeeExcludedAmountB.fee.toString()); + + // all owed amount should be collected + const positionAfterCollect = (await fetcher.getPosition( + positions[0].publicKey, + IGNORE_CACHE, + )) as PositionData; + assert.ok(positionAfterCollect.feeOwedA.isZero()); + assert.ok(positionAfterCollect.feeOwedB.isZero()); + }); + + it("collect_fees_v2: feeOwed is zero", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + const transferFeeA = await getTransferFee(tokenMintA); + const transferFeeB = await getTransferFee(tokenMintB); + assert.equal(transferFeeA.transferFeeBasisPoints, 500); // 5% + assert.equal(transferFeeB.transferFeeBasisPoints, 1000); // 10% + + // collect owed fees + await toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }), + ).buildAndExecute(); + + // feeOwed includes transfer fee + const positionBeforeCollect = (await fetcher.getPosition( + positions[0].publicKey, + IGNORE_CACHE, + )) as PositionData; + assert.ok(positionBeforeCollect.feeOwedA.isZero()); + assert.ok(positionBeforeCollect.feeOwedB.isZero()); + + // transfer fee should be zero + const expectedTransferFeeExcludedAmountA = calculateTransferFeeExcludedAmount( + transferFeeA, + positionBeforeCollect.feeOwedA, + ); + const expectedTransferFeeExcludedAmountB = calculateTransferFeeExcludedAmount( + transferFeeB, + positionBeforeCollect.feeOwedB, + ); + assert.ok(expectedTransferFeeExcludedAmountA.fee.isZero()); + assert.ok(expectedTransferFeeExcludedAmountB.fee.isZero()); + + const preVaultBalanceA = await getTokenBalance(provider, tokenVaultAKeypair.publicKey); + const preVaultBalanceB = await getTokenBalance(provider, tokenVaultBKeypair.publicKey); + const preFeeBalanceA = await getTokenBalance(provider, feeAccountA); + const preFeeBalanceB = await getTokenBalance(provider, feeAccountB); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }), + ).buildAndExecute(); + + // vault sent owed only (transfer fee is paid from owed) + const postVaultBalanceA = await getTokenBalance(provider, tokenVaultAKeypair.publicKey); + const postVaultBalanceB = await getTokenBalance(provider, tokenVaultBKeypair.publicKey); + assert.ok(new BN(preVaultBalanceA).sub(new BN(postVaultBalanceA)).isZero()); + assert.ok(new BN(preVaultBalanceB).sub(new BN(postVaultBalanceB)).isZero()); + + const postFeeBalanceA = await getTokenBalance(provider, feeAccountA); + const postFeeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(postFeeBalanceA).sub(new BN(preFeeBalanceA)).isZero()); + assert.ok(new BN(postFeeBalanceB).sub(new BN(preFeeBalanceB)).isZero()); + }); + + it("collect_fees_v2: transfer fee rate is 0%", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + tokenMintA, + 0, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ), + createSetTransferFeeInstruction( + tokenMintB, + 0, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ) + ] + }).buildAndExecute(); + + const updatedFeeConfigA = await fetchTransferFeeConfig(tokenMintA); + const updatedFeeConfigB = await fetchTransferFeeConfig(tokenMintB); + assert.equal(updatedFeeConfigA.newerTransferFee.transferFeeBasisPoints, 0); + assert.equal(updatedFeeConfigB.newerTransferFee.transferFeeBasisPoints, 0); + + // wait for epoch to enable updated fee rate + await waitEpoch(Number(updatedFeeConfigA.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigA.newerTransferFee.epoch); + await waitEpoch(Number(updatedFeeConfigB.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigB.newerTransferFee.epoch); + + // feeOwed includes transfer fee + const positionBeforeCollect = (await fetcher.getPosition( + positions[0].publicKey, + IGNORE_CACHE, + )) as PositionData; + assert.ok(!positionBeforeCollect.feeOwedA.isZero()); + assert.ok(!positionBeforeCollect.feeOwedB.isZero()); + + // transfer fee should be zero + const expectedTransferFeeExcludedAmountA = calculateTransferFeeExcludedAmount( + updatedFeeConfigA.newerTransferFee, + positionBeforeCollect.feeOwedA, + ); + const expectedTransferFeeExcludedAmountB = calculateTransferFeeExcludedAmount( + updatedFeeConfigB.newerTransferFee, + positionBeforeCollect.feeOwedB, + ); + assert.ok(expectedTransferFeeExcludedAmountA.amount.eq(positionBeforeCollect.feeOwedA)); + assert.ok(expectedTransferFeeExcludedAmountB.amount.eq(positionBeforeCollect.feeOwedB)); + assert.ok(expectedTransferFeeExcludedAmountA.fee.isZero()); + assert.ok(expectedTransferFeeExcludedAmountB.fee.isZero()); + + const preVaultBalanceA = await getTokenBalance(provider, tokenVaultAKeypair.publicKey); + const preVaultBalanceB = await getTokenBalance(provider, tokenVaultBKeypair.publicKey); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }), + ).buildAndExecute(); + + // vault sent owed only (transfer fee is paid from owed) + const postVaultBalanceA = await getTokenBalance(provider, tokenVaultAKeypair.publicKey); + const postVaultBalanceB = await getTokenBalance(provider, tokenVaultBKeypair.publicKey); + assert.ok( + new BN(preVaultBalanceA).sub(new BN(postVaultBalanceA)).eq(positionBeforeCollect.feeOwedA), + ); + assert.ok( + new BN(preVaultBalanceB).sub(new BN(postVaultBalanceB)).eq(positionBeforeCollect.feeOwedB), + ); + + // owner received feeOwed minus transfer fee (transferFeeExcludedAmount) + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(feeBalanceA).eq(expectedTransferFeeExcludedAmountA.amount)); + assert.ok(new BN(feeBalanceB).eq(expectedTransferFeeExcludedAmountB.amount)); + }); + + it("collect_fees_v2: transfer fee rate is 100%", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + tokenMintA, + MAX_FEE_BASIS_POINTS, // 100 % + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ), + createSetTransferFeeInstruction( + tokenMintB, + MAX_FEE_BASIS_POINTS, // 100 % + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ) + ] + }).buildAndExecute(); + + const updatedFeeConfigA = await fetchTransferFeeConfig(tokenMintA); + const updatedFeeConfigB = await fetchTransferFeeConfig(tokenMintB); + assert.equal(updatedFeeConfigA.newerTransferFee.transferFeeBasisPoints, MAX_FEE_BASIS_POINTS); + assert.equal(updatedFeeConfigB.newerTransferFee.transferFeeBasisPoints, MAX_FEE_BASIS_POINTS); + + // wait for epoch to enable updated fee rate + await waitEpoch(Number(updatedFeeConfigA.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigA.newerTransferFee.epoch); + await waitEpoch(Number(updatedFeeConfigB.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigB.newerTransferFee.epoch); + + // feeOwed includes transfer fee + const positionBeforeCollect = (await fetcher.getPosition( + positions[0].publicKey, + IGNORE_CACHE, + )) as PositionData; + assert.ok(!positionBeforeCollect.feeOwedA.isZero()); + assert.ok(!positionBeforeCollect.feeOwedB.isZero()); + + // transfer fee should be zero + const expectedTransferFeeExcludedAmountA = calculateTransferFeeExcludedAmount( + updatedFeeConfigA.newerTransferFee, + positionBeforeCollect.feeOwedA, + ); + const expectedTransferFeeExcludedAmountB = calculateTransferFeeExcludedAmount( + updatedFeeConfigB.newerTransferFee, + positionBeforeCollect.feeOwedB, + ); + assert.ok(expectedTransferFeeExcludedAmountA.amount.isZero()); + assert.ok(expectedTransferFeeExcludedAmountB.amount.isZero()); + assert.ok(expectedTransferFeeExcludedAmountA.fee.eq(positionBeforeCollect.feeOwedA)); + assert.ok(expectedTransferFeeExcludedAmountB.fee.eq(positionBeforeCollect.feeOwedB)); + + const preVaultBalanceA = await getTokenBalance(provider, tokenVaultAKeypair.publicKey); + const preVaultBalanceB = await getTokenBalance(provider, tokenVaultBKeypair.publicKey); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + }), + ).buildAndExecute(); + + // vault sent owed only (transfer fee is paid from owed) + const postVaultBalanceA = await getTokenBalance(provider, tokenVaultAKeypair.publicKey); + const postVaultBalanceB = await getTokenBalance(provider, tokenVaultBKeypair.publicKey); + assert.ok( + new BN(preVaultBalanceA).sub(new BN(postVaultBalanceA)).eq(positionBeforeCollect.feeOwedA), + ); + assert.ok( + new BN(preVaultBalanceB).sub(new BN(postVaultBalanceB)).eq(positionBeforeCollect.feeOwedB), + ); + + // owner received 0 tokens + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(feeBalanceA).isZero()); + assert.ok(new BN(feeBalanceB).isZero()); + // all tokens should be withheld as transfer fee + const transferFeeWithheldA = await fetchTransferFeeWithheldAmount(feeAccountA); + const transferFeeWithheldB = await fetchTransferFeeWithheldAmount(feeAccountB); + assert.ok(transferFeeWithheldA.eq(positionBeforeCollect.feeOwedA)); + assert.ok(transferFeeWithheldB.eq(positionBeforeCollect.feeOwedB)); + }); + + it("collect_protocol_fees_v2: with transfer fee", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKeypair }, + } = fixture.getInfos(); + + const transferFeeA = await getTransferFee(tokenMintA); + const transferFeeB = await getTransferFee(tokenMintB); + assert.equal(transferFeeA.transferFeeBasisPoints, 500); // 5% + assert.equal(transferFeeB.transferFeeBasisPoints, 1000); // 10% + + // protocolFeeOwed includes transfer fee + const poolBeforeCollect = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE, + )) as WhirlpoolData; + assert.ok(!poolBeforeCollect.protocolFeeOwedA.isZero()); + assert.ok(!poolBeforeCollect.protocolFeeOwedB.isZero()); + + // transfer fee should be non zero + const expectedTransferFeeExcludedAmountA = calculateTransferFeeExcludedAmount( + transferFeeA, + poolBeforeCollect.protocolFeeOwedA, + ); + const expectedTransferFeeExcludedAmountB = calculateTransferFeeExcludedAmount( + transferFeeB, + poolBeforeCollect.protocolFeeOwedB, + ); + assert.ok(expectedTransferFeeExcludedAmountA.fee.gtn(0)); + assert.ok(expectedTransferFeeExcludedAmountB.fee.gtn(0)); + + const preVaultBalanceA = await getTokenBalance(provider, tokenVaultAKeypair.publicKey); + const preVaultBalanceB = await getTokenBalance(provider, tokenVaultBKeypair.publicKey); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + }), + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(); + + // vault sent owed only (transfer fee is paid from owed) + const postVaultBalanceA = await getTokenBalance(provider, tokenVaultAKeypair.publicKey); + const postVaultBalanceB = await getTokenBalance(provider, tokenVaultBKeypair.publicKey); + assert.ok( + new BN(preVaultBalanceA) + .sub(new BN(postVaultBalanceA)) + .eq(poolBeforeCollect.protocolFeeOwedA), + ); + assert.ok( + new BN(preVaultBalanceB) + .sub(new BN(postVaultBalanceB)) + .eq(poolBeforeCollect.protocolFeeOwedB), + ); + + // protocol received feeOwed minus transfer fee (transferFeeExcludedAmount) + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(feeBalanceA).eq(expectedTransferFeeExcludedAmountA.amount)); + assert.ok(new BN(feeBalanceB).eq(expectedTransferFeeExcludedAmountB.amount)); + + // all owed amount should be collected + const poolAfterCollect = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE, + )) as WhirlpoolData; + assert.ok(poolAfterCollect.protocolFeeOwedA.isZero()); + assert.ok(poolAfterCollect.protocolFeeOwedB.isZero()); + }); + + it("collect_protocol_fees_v2: protocolFeeOwed is zero", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKeypair }, + } = fixture.getInfos(); + + // collect + await toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + }), + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(); + + const transferFeeA = await getTransferFee(tokenMintA); + const transferFeeB = await getTransferFee(tokenMintB); + assert.equal(transferFeeA.transferFeeBasisPoints, 500); // 5% + assert.equal(transferFeeB.transferFeeBasisPoints, 1000); // 10% + + // protocolFeeOwed includes transfer fee + const poolBeforeCollect = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE, + )) as WhirlpoolData; + assert.ok(poolBeforeCollect.protocolFeeOwedA.isZero()); + assert.ok(poolBeforeCollect.protocolFeeOwedB.isZero()); + + // transfer fee should be zero + const expectedTransferFeeExcludedAmountA = calculateTransferFeeExcludedAmount( + transferFeeA, + poolBeforeCollect.protocolFeeOwedA, + ); + const expectedTransferFeeExcludedAmountB = calculateTransferFeeExcludedAmount( + transferFeeB, + poolBeforeCollect.protocolFeeOwedB, + ); + assert.ok(expectedTransferFeeExcludedAmountA.amount.isZero()); + assert.ok(expectedTransferFeeExcludedAmountB.amount.isZero()); + assert.ok(expectedTransferFeeExcludedAmountA.fee.isZero()); + assert.ok(expectedTransferFeeExcludedAmountB.fee.isZero()); + + const preVaultBalanceA = await getTokenBalance(provider, tokenVaultAKeypair.publicKey); + const preVaultBalanceB = await getTokenBalance(provider, tokenVaultBKeypair.publicKey); + const preFeeBalanceA = await getTokenBalance(provider, feeAccountA); + const preFeeBalanceB = await getTokenBalance(provider, feeAccountB); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + }), + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(); + + // vault balance should not change + const postVaultBalanceA = await getTokenBalance(provider, tokenVaultAKeypair.publicKey); + const postVaultBalanceB = await getTokenBalance(provider, tokenVaultBKeypair.publicKey); + assert.ok(new BN(preVaultBalanceA).eq(new BN(postVaultBalanceA))); + assert.ok(new BN(preVaultBalanceB).eq(new BN(postVaultBalanceB))); + + // protocol received 0 tokens + const postFeeBalanceA = await getTokenBalance(provider, feeAccountA); + const postFeeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(preFeeBalanceA).eq(new BN(postFeeBalanceA))); + assert.ok(new BN(preFeeBalanceB).eq(new BN(postFeeBalanceB))); + }); + + it("collect_protocol_fees_v2: transfer fee rate is 0%", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKeypair }, + } = fixture.getInfos(); + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + tokenMintA, + 0, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ), + createSetTransferFeeInstruction( + tokenMintB, + 0, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ) + ] + }).buildAndExecute(); + + const updatedFeeConfigA = await fetchTransferFeeConfig(tokenMintA); + const updatedFeeConfigB = await fetchTransferFeeConfig(tokenMintB); + assert.equal(updatedFeeConfigA.newerTransferFee.transferFeeBasisPoints, 0); + assert.equal(updatedFeeConfigB.newerTransferFee.transferFeeBasisPoints, 0); + + // wait for epoch to enable updated fee rate + await waitEpoch(Number(updatedFeeConfigA.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigA.newerTransferFee.epoch); + await waitEpoch(Number(updatedFeeConfigB.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigB.newerTransferFee.epoch); + + // protocolFeeOwed includes transfer fee + const poolBeforeCollect = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE, + )) as WhirlpoolData; + assert.ok(!poolBeforeCollect.protocolFeeOwedA.isZero()); + assert.ok(!poolBeforeCollect.protocolFeeOwedB.isZero()); + + // transfer fee should be zero + const expectedTransferFeeExcludedAmountA = calculateTransferFeeExcludedAmount( + updatedFeeConfigA.newerTransferFee, + poolBeforeCollect.protocolFeeOwedA, + ); + const expectedTransferFeeExcludedAmountB = calculateTransferFeeExcludedAmount( + updatedFeeConfigB.newerTransferFee, + poolBeforeCollect.protocolFeeOwedB, + ); + assert.ok(expectedTransferFeeExcludedAmountA.amount.eq(poolBeforeCollect.protocolFeeOwedA)); + assert.ok(expectedTransferFeeExcludedAmountB.amount.eq(poolBeforeCollect.protocolFeeOwedB)); + assert.ok(expectedTransferFeeExcludedAmountA.fee.isZero()); + assert.ok(expectedTransferFeeExcludedAmountB.fee.isZero()); + + const preVaultBalanceA = await getTokenBalance(provider, tokenVaultAKeypair.publicKey); + const preVaultBalanceB = await getTokenBalance(provider, tokenVaultBKeypair.publicKey); + const preFeeBalanceA = await getTokenBalance(provider, feeAccountA); + const preFeeBalanceB = await getTokenBalance(provider, feeAccountB); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + }), + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(); + + // vault balance should not change + const postVaultBalanceA = await getTokenBalance(provider, tokenVaultAKeypair.publicKey); + const postVaultBalanceB = await getTokenBalance(provider, tokenVaultBKeypair.publicKey); + assert.ok(new BN(preVaultBalanceA).sub(new BN(postVaultBalanceA)).eq(poolBeforeCollect.protocolFeeOwedA)); + assert.ok(new BN(preVaultBalanceB).sub(new BN(postVaultBalanceB)).eq(poolBeforeCollect.protocolFeeOwedB)); + + // protocol received all owed amount + const postFeeBalanceA = await getTokenBalance(provider, feeAccountA); + const postFeeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(postFeeBalanceA).sub(new BN(preFeeBalanceA)).eq(poolBeforeCollect.protocolFeeOwedA)); + assert.ok(new BN(postFeeBalanceB).sub(new BN(preFeeBalanceB)).eq(poolBeforeCollect.protocolFeeOwedB)); + }); + + it("collect_protocol_fees_v2: transfer fee rate is 100%", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKeypair }, + } = fixture.getInfos(); + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + tokenMintA, + MAX_FEE_BASIS_POINTS, // 100 % + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ), + createSetTransferFeeInstruction( + tokenMintB, + MAX_FEE_BASIS_POINTS, // 100 % + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ) + ] + }).buildAndExecute(); + + const updatedFeeConfigA = await fetchTransferFeeConfig(tokenMintA); + const updatedFeeConfigB = await fetchTransferFeeConfig(tokenMintB); + assert.equal(updatedFeeConfigA.newerTransferFee.transferFeeBasisPoints, MAX_FEE_BASIS_POINTS); + assert.equal(updatedFeeConfigB.newerTransferFee.transferFeeBasisPoints, MAX_FEE_BASIS_POINTS); + + // wait for epoch to enable updated fee rate + await waitEpoch(Number(updatedFeeConfigA.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigA.newerTransferFee.epoch); + await waitEpoch(Number(updatedFeeConfigB.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigB.newerTransferFee.epoch); + + // protocolFeeOwed includes transfer fee + const poolBeforeCollect = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE, + )) as WhirlpoolData; + assert.ok(!poolBeforeCollect.protocolFeeOwedA.isZero()); + assert.ok(!poolBeforeCollect.protocolFeeOwedB.isZero()); + + // transfer fee should be 100% + const expectedTransferFeeExcludedAmountA = calculateTransferFeeExcludedAmount( + updatedFeeConfigA.newerTransferFee, + poolBeforeCollect.protocolFeeOwedA, + ); + const expectedTransferFeeExcludedAmountB = calculateTransferFeeExcludedAmount( + updatedFeeConfigB.newerTransferFee, + poolBeforeCollect.protocolFeeOwedB, + ); + assert.ok(expectedTransferFeeExcludedAmountA.fee.eq(poolBeforeCollect.protocolFeeOwedA)); + assert.ok(expectedTransferFeeExcludedAmountB.fee.eq(poolBeforeCollect.protocolFeeOwedB)); + assert.ok(expectedTransferFeeExcludedAmountA.amount.isZero()); + assert.ok(expectedTransferFeeExcludedAmountB.amount.isZero()); + + const preVaultBalanceA = await getTokenBalance(provider, tokenVaultAKeypair.publicKey); + const preVaultBalanceB = await getTokenBalance(provider, tokenVaultBKeypair.publicKey); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + }), + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(); + + // vault balance should not change + const postVaultBalanceA = await getTokenBalance(provider, tokenVaultAKeypair.publicKey); + const postVaultBalanceB = await getTokenBalance(provider, tokenVaultBKeypair.publicKey); + assert.ok(new BN(preVaultBalanceA).sub(new BN(postVaultBalanceA)).eq(poolBeforeCollect.protocolFeeOwedA)); + assert.ok(new BN(preVaultBalanceB).sub(new BN(postVaultBalanceB)).eq(poolBeforeCollect.protocolFeeOwedB)); + + // protocol received 0 tokens + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(feeBalanceA).isZero()); + assert.ok(new BN(feeBalanceB).isZero()); + // all tokens should be withheld as transfer fee + const transferFeeWithheldA = await fetchTransferFeeWithheldAmount(feeAccountA); + const transferFeeWithheldB = await fetchTransferFeeWithheldAmount(feeAccountB); + assert.ok(transferFeeWithheldA.eq(poolBeforeCollect.protocolFeeOwedA)); + assert.ok(transferFeeWithheldB.eq(poolBeforeCollect.protocolFeeOwedB)); + }); + }); + + describe("collect_reward_v2", () => { + let fixture: WhirlpoolTestFixtureV2; + let rewardAccounts: PublicKey[]; + + beforeEach(async () => { + const vaultStartBalance = 1_000_000; + const lowerTickIndex = -1280, + upperTickIndex = 1280, + tickSpacing = TickSpacing.Standard; + fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: tickSpacing, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: lowerTickIndex, + tickUpperIndex: upperTickIndex, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: { + isToken2022: true, + hasTransferFeeExtension: true, + transferFeeInitialBps: 500, + }, // 5% + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: { + isToken2022: true, + hasTransferFeeExtension: true, + transferFeeInitialBps: 1000, + }, // 10% + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: { + isToken2022: true, + hasTransferFeeExtension: true, + transferFeeInitialBps: 5000, + }, // 50% + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + // accrue rewards + await sleep(3000); + + await toTx( + ctx, + WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + position: positions[0].publicKey, + tickArrayLower: positions[0].tickArrayLower, + tickArrayUpper: positions[0].tickArrayUpper, + }), + ).buildAndExecute(); + + // Generate collect reward expectation + const whirlpoolData = (await fetcher.getPool(whirlpoolPda.publicKey)) as WhirlpoolData; + const positionPreCollect = await client.getPosition(positions[0].publicKey, IGNORE_CACHE); + + // Lock the collectRewards quote to the last time we called updateFeesAndRewards + const expectation = collectRewardsQuote({ + whirlpool: whirlpoolData, + position: positionPreCollect.getData(), + tickLower: positionPreCollect.getLowerTickData(), + tickUpper: positionPreCollect.getUpperTickData(), + timeStampInSeconds: whirlpoolData.rewardLastUpdatedTimestamp, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }); + + // Check that the expectation is not zero + for (let i = 0; i < NUM_REWARDS; i++) { + assert.ok(!expectation.rewardOwed[i]!.isZero()); + assert.ok(!expectation.transferFee.deductedFromRewardOwed[i]!.isZero()); + } + + rewardAccounts = await Promise.all( + rewards.map((reward) => { + return createTokenAccountV2( + provider, + { isToken2022: true }, + reward.rewardMint, + provider.wallet.publicKey, + ); + }), + ); + }); + + it("collect_reward_v2: with transfer fee", async () => { + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + const whirlpoolData = (await fetcher.getPool(whirlpoolPda.publicKey)) as WhirlpoolData; + const positionPreCollect = await client.getPosition(positions[0].publicKey, IGNORE_CACHE); + const expectation = collectRewardsQuote({ + whirlpool: whirlpoolData, + position: positionPreCollect.getData(), + tickLower: positionPreCollect.getLowerTickData(), + tickUpper: positionPreCollect.getUpperTickData(), + timeStampInSeconds: whirlpoolData.rewardLastUpdatedTimestamp, + tokenExtensionCtx: withNoExtension, // no TransferFee consideration because it is taken into account later + }); + + for (let i = 0; i < NUM_REWARDS; i++) { + const transferFee = await getTransferFee(rewards[i].rewardMint); + assert.equal(transferFee.transferFeeBasisPoints, [500, 1000, 5000][i]); + + // expectation include transfer fee + const expectedTransferFeeExcludedAmount = calculateTransferFeeExcludedAmount( + transferFee, + expectation.rewardOwed[i]!, + ); + assert.ok(expectedTransferFeeExcludedAmount.fee.gtn(0)); + + const preVaultBalance = await getTokenBalance( + provider, + rewards[i].rewardVaultKeypair.publicKey, + ); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[i].rewardMint, + rewardTokenProgram: rewards[i].tokenProgram, + rewardOwnerAccount: rewardAccounts[i], + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + rewardIndex: i, + }), + ).buildAndExecute(); + + // vault sent owed only (no transfer fee, transfer fee is paid from owed) + const postVaultBalance = await getTokenBalance( + provider, + rewards[i].rewardVaultKeypair.publicKey, + ); + assert.ok(new BN(preVaultBalance).sub(new BN(postVaultBalance)).eq(expectation.rewardOwed[i]!)); + + // owner received expectation minus transfer fee (transferFeeExcludedAmount) + const rewardBalance = await getTokenBalance(provider, rewardAccounts[i]); + assert.ok(new BN(rewardBalance).eq(expectedTransferFeeExcludedAmount.amount)); + + //console.log("R", expectation[i]?.toString(), rewardBalance.toString(), expectedTransferFeeExcludedAmount.amount.toString(), expectedTransferFeeExcludedAmount.fee.toString()); + } + + const positionPostCollect = await client.getPosition(positions[0].publicKey, IGNORE_CACHE); + const expectationPostCollect = collectRewardsQuote({ + whirlpool: whirlpoolData, + position: positionPostCollect.getData(), + tickLower: positionPostCollect.getLowerTickData(), + tickUpper: positionPostCollect.getUpperTickData(), + timeStampInSeconds: whirlpoolData.rewardLastUpdatedTimestamp, + tokenExtensionCtx: withNoExtension, + }); + + assert.ok(expectationPostCollect.rewardOwed.every((n) => n!.isZero())); + }); + + it("collect_reward_v2: rewardOwed is zero", async () => { + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + // collect + for (let i = 0; i < NUM_REWARDS; i++) { + await toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[i].rewardMint, + rewardTokenProgram: rewards[i].tokenProgram, + rewardOwnerAccount: rewardAccounts[i], + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + rewardIndex: i, + }), + ).buildAndExecute(); + } + + const whirlpoolData = (await fetcher.getPool(whirlpoolPda.publicKey)) as WhirlpoolData; + const positionPreCollect = await client.getPosition(positions[0].publicKey, IGNORE_CACHE); + + for (let i = 0; i < NUM_REWARDS; i++) { + const transferFee = await getTransferFee(rewards[i].rewardMint); + assert.equal(transferFee.transferFeeBasisPoints, [500, 1000, 5000][i]); + + // expectation include transfer fee + const expectedTransferFeeExcludedAmount = calculateTransferFeeExcludedAmount( + transferFee, + positionPreCollect.getData().rewardInfos[i].amountOwed, + ); + assert.ok(expectedTransferFeeExcludedAmount.amount.isZero()); + assert.ok(expectedTransferFeeExcludedAmount.fee.isZero()); + + const preVaultBalance = await getTokenBalance( + provider, + rewards[i].rewardVaultKeypair.publicKey, + ); + const preRewardBalance = await getTokenBalance(provider, rewardAccounts[i]); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[i].rewardMint, + rewardTokenProgram: rewards[i].tokenProgram, + rewardOwnerAccount: rewardAccounts[i], + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + rewardIndex: i, + }), + ).buildAndExecute(); + + // vault sent owed only (no transfer fee, transfer fee is paid from owed) + const postVaultBalance = await getTokenBalance( + provider, + rewards[i].rewardVaultKeypair.publicKey, + ); + assert.ok(new BN(preVaultBalance).eq(new BN(postVaultBalance))); + + // owner received expectation minus transfer fee (transferFeeExcludedAmount) + const postRewardBalance = await getTokenBalance(provider, rewardAccounts[i]); + assert.ok(new BN(postRewardBalance).eq(new BN(preRewardBalance))); + } + }); + + it("collect_reward_v2: transfer fee rate is 0%", async () => { + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + const whirlpoolData = (await fetcher.getPool(whirlpoolPda.publicKey)) as WhirlpoolData; + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: whirlpoolData.rewardInfos.map((rewardInfo, i) => + createSetTransferFeeInstruction( + rewardInfo.mint, + 0, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + )), + }).buildAndExecute(); + + const positionPreCollect = await client.getPosition(positions[0].publicKey, IGNORE_CACHE); + + for (let i = 0; i < NUM_REWARDS; i++) { + const updatedFeeConfig = await fetchTransferFeeConfig(rewards[i].rewardMint); + + assert.equal(updatedFeeConfig.newerTransferFee.transferFeeBasisPoints, 0); + await waitEpoch(Number(updatedFeeConfig.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfig.newerTransferFee.epoch); + + const transferFee = await getTransferFee(rewards[i].rewardMint); + + // expectation include transfer fee + const expectedTransferFeeExcludedAmount = calculateTransferFeeExcludedAmount( + transferFee, + positionPreCollect.getData().rewardInfos[i].amountOwed, + ); + assert.ok(expectedTransferFeeExcludedAmount.amount.eq(positionPreCollect.getData().rewardInfos[i].amountOwed)); + assert.ok(expectedTransferFeeExcludedAmount.fee.isZero()); + + const preVaultBalance = await getTokenBalance( + provider, + rewards[i].rewardVaultKeypair.publicKey, + ); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[i].rewardMint, + rewardTokenProgram: rewards[i].tokenProgram, + rewardOwnerAccount: rewardAccounts[i], + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + rewardIndex: i, + }), + ).buildAndExecute(); + + // vault sent owed only (no transfer fee, transfer fee is paid from owed) + const postVaultBalance = await getTokenBalance( + provider, + rewards[i].rewardVaultKeypair.publicKey, + ); + assert.ok(new BN(preVaultBalance).sub(new BN(postVaultBalance)).eq(positionPreCollect.getData().rewardInfos[i].amountOwed)); + + // owner received expectation minus transfer fee (transferFeeExcludedAmount) + const postRewardBalance = await getTokenBalance(provider, rewardAccounts[i]); + assert.ok(new BN(postRewardBalance).eq(expectedTransferFeeExcludedAmount.amount)); + } + }); + + it("collect_reward_v2: transfer fee rate is 100%", async () => { + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + const whirlpoolData = (await fetcher.getPool(whirlpoolPda.publicKey)) as WhirlpoolData; + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: whirlpoolData.rewardInfos.map((rewardInfo, i) => + createSetTransferFeeInstruction( + rewardInfo.mint, + MAX_FEE_BASIS_POINTS, // 100 % + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + )), + }).buildAndExecute(); + + const positionPreCollect = await client.getPosition(positions[0].publicKey, IGNORE_CACHE); + + for (let i = 0; i < NUM_REWARDS; i++) { + const updatedFeeConfig = await fetchTransferFeeConfig(rewards[i].rewardMint); + + assert.equal(updatedFeeConfig.newerTransferFee.transferFeeBasisPoints, MAX_FEE_BASIS_POINTS); + await waitEpoch(Number(updatedFeeConfig.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfig.newerTransferFee.epoch); + + const transferFee = await getTransferFee(rewards[i].rewardMint); + + // expectation include transfer fee + const expectedTransferFeeExcludedAmount = calculateTransferFeeExcludedAmount( + transferFee, + positionPreCollect.getData().rewardInfos[i].amountOwed, + ); + assert.ok(expectedTransferFeeExcludedAmount.fee.eq(positionPreCollect.getData().rewardInfos[i].amountOwed)); + assert.ok(expectedTransferFeeExcludedAmount.amount.isZero()); + + const preVaultBalance = await getTokenBalance( + provider, + rewards[i].rewardVaultKeypair.publicKey, + ); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[i].rewardMint, + rewardTokenProgram: rewards[i].tokenProgram, + rewardOwnerAccount: rewardAccounts[i], + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + rewardIndex: i, + }), + ).buildAndExecute(); + + // vault sent owed only (no transfer fee, transfer fee is paid from owed) + const postVaultBalance = await getTokenBalance( + provider, + rewards[i].rewardVaultKeypair.publicKey, + ); + assert.ok(new BN(preVaultBalance).sub(new BN(postVaultBalance)).eq(positionPreCollect.getData().rewardInfos[i].amountOwed)); + + // owner received expectation minus transfer fee (transferFeeExcludedAmount) + const postRewardBalance = await getTokenBalance(provider, rewardAccounts[i]); + assert.ok(new BN(postRewardBalance).isZero()); + + const withheldAmount = await fetchTransferFeeWithheldAmount(rewardAccounts[i]); + assert.ok(withheldAmount.eq(positionPreCollect.getData().rewardInfos[i].amountOwed)); + } + }); + }); + + describe("increase_liquidity_v2", () => { + const tickLowerIndex = 7168; + const tickUpperIndex = 8960; + const currTick = Math.round((tickLowerIndex + tickUpperIndex) / 2); + + const aboveLowerIndex = TickUtil.getNextInitializableTickIndex(currTick + 1, TickSpacing.Standard); + const aboveUpperIndex = tickUpperIndex; + const belowLowerIndex = tickLowerIndex; + const belowUpperIndex = TickUtil.getPrevInitializableTickIndex(currTick - 1, TickSpacing.Standard); + + let fixture: WhirlpoolTestFixtureV2; + + beforeEach(async () => { + fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { + isToken2022: true, + hasTransferFeeExtension: true, + transferFeeInitialBps: 500, + }, // 5% + tokenTraitB: { + isToken2022: true, + hasTransferFeeExtension: true, + transferFeeInitialBps: 1000, + }, // 10% + tickSpacing: TickSpacing.Standard, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }, + { tickLowerIndex: aboveLowerIndex, tickUpperIndex: aboveUpperIndex, liquidityAmount: ZERO_BN }, + { tickLowerIndex: belowLowerIndex, tickUpperIndex: belowUpperIndex, liquidityAmount: ZERO_BN }, + ], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + }); + }); + + it("increase_liquidity_v2: with transfer fee", async () => { + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const positionInitInfo = positions[0]; + + const transferFeeA = await getTransferFee(poolInitInfo.tokenMintA); + const transferFeeB = await getTransferFee(poolInitInfo.tokenMintB); + assert.equal(transferFeeA.transferFeeBasisPoints, 500); // 5% + assert.equal(transferFeeB.transferFeeBasisPoints, 1000); // 10% + + const tokenAmount = toTokenAmount(1_000_000 * 0.8, 1_000_000 * 0.8); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount, + ); + const requiredAmountDelta = PoolUtil.getTokenAmountsFromLiquidity( + liquidityAmount, + PriceMath.tickIndexToSqrtPriceX64(currTick), + PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex), + true, + ); + + // transfer fee should be non zero + assert.ok(requiredAmountDelta.tokenA.gtn(0)); + assert.ok(requiredAmountDelta.tokenB.gtn(0)); + const expectedTransferFeeIncludedAmountA = calculateTransferFeeIncludedAmount( + transferFeeA, + requiredAmountDelta.tokenA, + ); + const expectedTransferFeeIncludedAmountB = calculateTransferFeeIncludedAmount( + transferFeeB, + requiredAmountDelta.tokenB, + ); + assert.ok(expectedTransferFeeIncludedAmountA.fee.gtn(0)); + assert.ok(expectedTransferFeeIncludedAmountB.fee.gtn(0)); + + const preVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const preVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const preOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const preOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: expectedTransferFeeIncludedAmountA.amount, + tokenMaxB: expectedTransferFeeIncludedAmountB.amount, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }), + ).buildAndExecute(); + + const postVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const postVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const postOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const postOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + // owner sent requiredAmountDelta plus transfer fees + assert.ok( + preOwnerAccountBalanceA + .sub(postOwnerAccountBalanceA) + .eq(expectedTransferFeeIncludedAmountA.amount), + ); + assert.ok( + preOwnerAccountBalanceB + .sub(postOwnerAccountBalanceB) + .eq(expectedTransferFeeIncludedAmountB.amount), + ); + // vault received requiredAmountDelta + assert.ok(postVaultBalanceA.sub(preVaultBalanceA).eq(requiredAmountDelta.tokenA)); + assert.ok(postVaultBalanceB.sub(preVaultBalanceB).eq(requiredAmountDelta.tokenB)); + }); + + it("increase_liquidity_v2: transfer fee rate is 0%", async () => { + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const positionInitInfo = positions[0]; + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + poolInitInfo.tokenMintA, + 0, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ), + createSetTransferFeeInstruction( + poolInitInfo.tokenMintB, + 0, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ) + ] + }).buildAndExecute(); + + const updatedFeeConfigA = await fetchTransferFeeConfig(poolInitInfo.tokenMintA); + const updatedFeeConfigB = await fetchTransferFeeConfig(poolInitInfo.tokenMintB); + assert.equal(updatedFeeConfigA.newerTransferFee.transferFeeBasisPoints, 0); + assert.equal(updatedFeeConfigB.newerTransferFee.transferFeeBasisPoints, 0); + + // wait for epoch to enable updated fee rate + await waitEpoch(Number(updatedFeeConfigA.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigA.newerTransferFee.epoch); + await waitEpoch(Number(updatedFeeConfigB.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigB.newerTransferFee.epoch); + + const tokenAmount = toTokenAmount(1_000_000 * 0.8, 1_000_000 * 0.8); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount, + ); + const requiredAmountDelta = PoolUtil.getTokenAmountsFromLiquidity( + liquidityAmount, + PriceMath.tickIndexToSqrtPriceX64(currTick), + PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex), + true, + ); + + // transfer fee should be zero + assert.ok(requiredAmountDelta.tokenA.gtn(0)); + assert.ok(requiredAmountDelta.tokenB.gtn(0)); + const expectedTransferFeeIncludedAmountA = calculateTransferFeeIncludedAmount( + updatedFeeConfigA.newerTransferFee, + requiredAmountDelta.tokenA, + ); + const expectedTransferFeeIncludedAmountB = calculateTransferFeeIncludedAmount( + updatedFeeConfigB.newerTransferFee, + requiredAmountDelta.tokenB, + ); + assert.ok(expectedTransferFeeIncludedAmountA.fee.isZero()); + assert.ok(expectedTransferFeeIncludedAmountB.fee.isZero()); + assert.ok(expectedTransferFeeIncludedAmountA.amount.eq(requiredAmountDelta.tokenA)); + assert.ok(expectedTransferFeeIncludedAmountB.amount.eq(requiredAmountDelta.tokenB)); + + const preVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const preVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const preOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const preOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: expectedTransferFeeIncludedAmountA.amount, + tokenMaxB: expectedTransferFeeIncludedAmountB.amount, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }), + ).buildAndExecute(); + + const postVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const postVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const postOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const postOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + // owner sent requiredAmountDelta plus transfer fees (0) + assert.ok( + preOwnerAccountBalanceA + .sub(postOwnerAccountBalanceA) + .eq(requiredAmountDelta.tokenA), + ); + assert.ok( + preOwnerAccountBalanceB + .sub(postOwnerAccountBalanceB) + .eq(requiredAmountDelta.tokenB), + ); + // vault received requiredAmountDelta + assert.ok(postVaultBalanceA.sub(preVaultBalanceA).eq(requiredAmountDelta.tokenA)); + assert.ok(postVaultBalanceB.sub(preVaultBalanceB).eq(requiredAmountDelta.tokenB)); + }); + + it("increase_liquidity_v2: [FAIL] transfer fee rate is 100% without cap", async () => { + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const positionInitInfo = positions[0]; + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + poolInitInfo.tokenMintA, + MAX_FEE_BASIS_POINTS, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ), + createSetTransferFeeInstruction( + poolInitInfo.tokenMintB, + MAX_FEE_BASIS_POINTS, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ) + ] + }).buildAndExecute(); + + const updatedFeeConfigA = await fetchTransferFeeConfig(poolInitInfo.tokenMintA); + const updatedFeeConfigB = await fetchTransferFeeConfig(poolInitInfo.tokenMintB); + assert.equal(updatedFeeConfigA.newerTransferFee.transferFeeBasisPoints, MAX_FEE_BASIS_POINTS); + assert.equal(updatedFeeConfigB.newerTransferFee.transferFeeBasisPoints, MAX_FEE_BASIS_POINTS); + + // wait for epoch to enable updated fee rate + await waitEpoch(Number(updatedFeeConfigA.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigA.newerTransferFee.epoch); + await waitEpoch(Number(updatedFeeConfigB.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigB.newerTransferFee.epoch); + + const tokenAmount = toTokenAmount(1_000_000 * 0.8, 1_000_000 * 0.8); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount, + ); + const requiredAmountDelta = PoolUtil.getTokenAmountsFromLiquidity( + liquidityAmount, + PriceMath.tickIndexToSqrtPriceX64(currTick), + PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex), + true, + ); + + assert.ok(requiredAmountDelta.tokenA.gtn(0)); + assert.ok(requiredAmountDelta.tokenB.gtn(0)); + + // overflow at client-side + assert.throws(() => { + calculateTransferFeeIncludedAmount(updatedFeeConfigA.newerTransferFee, requiredAmountDelta.tokenA); + }); + assert.throws(() => { + calculateTransferFeeIncludedAmount(updatedFeeConfigB.newerTransferFee, requiredAmountDelta.tokenB); + }); + + // overflow at contract-side + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: U64_MAX, + tokenMaxB: U64_MAX, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }), + ).buildAndExecute(), + /0x17a4/, // TransferFeeCalculationError + ); + }); + + it("increase_liquidity_v2: transfer fee rate is 100% with cap", async () => { + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const positionInitInfo = positions[0]; + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + poolInitInfo.tokenMintA, + MAX_FEE_BASIS_POINTS, + 99n, // cap + provider.wallet.publicKey, + ), + createSetTransferFeeInstruction( + poolInitInfo.tokenMintB, + MAX_FEE_BASIS_POINTS, + 99n, // cap + provider.wallet.publicKey, + ) + ] + }).buildAndExecute(); + + const updatedFeeConfigA = await fetchTransferFeeConfig(poolInitInfo.tokenMintA); + const updatedFeeConfigB = await fetchTransferFeeConfig(poolInitInfo.tokenMintB); + assert.equal(updatedFeeConfigA.newerTransferFee.transferFeeBasisPoints, MAX_FEE_BASIS_POINTS); + assert.equal(updatedFeeConfigB.newerTransferFee.transferFeeBasisPoints, MAX_FEE_BASIS_POINTS); + assert.equal(updatedFeeConfigA.newerTransferFee.maximumFee, 99n); + assert.equal(updatedFeeConfigB.newerTransferFee.maximumFee, 99n); + + // wait for epoch to enable updated fee rate + await waitEpoch(Number(updatedFeeConfigA.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigA.newerTransferFee.epoch); + await waitEpoch(Number(updatedFeeConfigB.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigB.newerTransferFee.epoch); + + const tokenAmount = toTokenAmount(1_000_000 * 0.8, 1_000_000 * 0.8); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount, + ); + const requiredAmountDelta = PoolUtil.getTokenAmountsFromLiquidity( + liquidityAmount, + PriceMath.tickIndexToSqrtPriceX64(currTick), + PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex), + true, + ); + + assert.ok(requiredAmountDelta.tokenA.gtn(0)); + assert.ok(requiredAmountDelta.tokenB.gtn(0)); + + const expectedTransferFeeIncludedAmountA = calculateTransferFeeIncludedAmount( + updatedFeeConfigA.newerTransferFee, + requiredAmountDelta.tokenA, + ); + const expectedTransferFeeIncludedAmountB = calculateTransferFeeIncludedAmount( + updatedFeeConfigB.newerTransferFee, + requiredAmountDelta.tokenB, + ); + assert.ok(expectedTransferFeeIncludedAmountA.fee.eq(new BN(99))); + assert.ok(expectedTransferFeeIncludedAmountB.fee.eq(new BN(99))); + assert.ok(expectedTransferFeeIncludedAmountA.amount.sub(expectedTransferFeeIncludedAmountA.fee).eq(requiredAmountDelta.tokenA)); + assert.ok(expectedTransferFeeIncludedAmountB.amount.sub(expectedTransferFeeIncludedAmountB.fee).eq(requiredAmountDelta.tokenB)); + + const preVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const preVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const preOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const preOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: expectedTransferFeeIncludedAmountA.amount, + tokenMaxB: expectedTransferFeeIncludedAmountB.amount, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }), + ).buildAndExecute(); + + const postVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const postVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const postOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const postOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + // owner sent requiredAmountDelta plus transfer fees + assert.ok( + preOwnerAccountBalanceA + .sub(postOwnerAccountBalanceA) + .eq(expectedTransferFeeIncludedAmountA.amount) + ); + assert.ok( + preOwnerAccountBalanceB + .sub(postOwnerAccountBalanceB) + .eq(expectedTransferFeeIncludedAmountB.amount), + ); + // vault received requiredAmountDelta + assert.ok(postVaultBalanceA.sub(preVaultBalanceA).eq(requiredAmountDelta.tokenA)); + assert.ok(postVaultBalanceB.sub(preVaultBalanceB).eq(requiredAmountDelta.tokenB)); + + const withheldAmountA = await fetchTransferFeeWithheldAmount(poolInitInfo.tokenVaultAKeypair.publicKey); + const withheldAmountB = await fetchTransferFeeWithheldAmount(poolInitInfo.tokenVaultBKeypair.publicKey); + assert.ok(new BN(withheldAmountA).eq(new BN(99))); + assert.ok(new BN(withheldAmountB).eq(new BN(99))); + }); + + it("increase_liquidity_v2: out or range (above, tokenB amount is zero)", async () => { + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const positionInitInfo = positions[1]; + + const transferFeeA = await getTransferFee(poolInitInfo.tokenMintA); + const transferFeeB = await getTransferFee(poolInitInfo.tokenMintB); + assert.equal(transferFeeA.transferFeeBasisPoints, 500); // 5% + assert.equal(transferFeeB.transferFeeBasisPoints, 1000); // 10% + + const tokenAmount = toTokenAmount(1_000_000 * 0.8, 1_000_000 * 0.8); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + aboveLowerIndex, + aboveUpperIndex, + tokenAmount, + ); + const requiredAmountDelta = PoolUtil.getTokenAmountsFromLiquidity( + liquidityAmount, + PriceMath.tickIndexToSqrtPriceX64(currTick), + PriceMath.tickIndexToSqrtPriceX64(aboveLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(aboveUpperIndex), + true, + ); + + assert.ok(requiredAmountDelta.tokenA.gtn(0)); + assert.ok(requiredAmountDelta.tokenB.isZero()); // out of range, all asset is in tokenA + const expectedTransferFeeIncludedAmountA = calculateTransferFeeIncludedAmount( + transferFeeA, + requiredAmountDelta.tokenA, + ); + const expectedTransferFeeIncludedAmountB = calculateTransferFeeIncludedAmount( + transferFeeB, + requiredAmountDelta.tokenB, + ); + assert.ok(expectedTransferFeeIncludedAmountA.fee.gtn(0)); + assert.ok(expectedTransferFeeIncludedAmountB.amount.isZero()); + assert.ok(expectedTransferFeeIncludedAmountB.fee.isZero()); + + const preVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const preVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const preOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const preOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: expectedTransferFeeIncludedAmountA.amount, + tokenMaxB: expectedTransferFeeIncludedAmountB.amount, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }), + ).buildAndExecute(); + + const postVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const postVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const postOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const postOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + // owner sent requiredAmountDelta plus transfer fees + assert.ok( + preOwnerAccountBalanceA + .sub(postOwnerAccountBalanceA) + .eq(expectedTransferFeeIncludedAmountA.amount), + ); + assert.ok( + preOwnerAccountBalanceB + .sub(postOwnerAccountBalanceB) + .isZero() + ); + // vault received requiredAmountDelta + assert.ok(postVaultBalanceA.sub(preVaultBalanceA).eq(requiredAmountDelta.tokenA)); + assert.ok(postVaultBalanceB.sub(preVaultBalanceB).isZero()); + }); + + it("increase_liquidity_v2: out or range (below, tokenA amount is zero)", async () => { + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const positionInitInfo = positions[2]; + + const transferFeeA = await getTransferFee(poolInitInfo.tokenMintA); + const transferFeeB = await getTransferFee(poolInitInfo.tokenMintB); + assert.equal(transferFeeA.transferFeeBasisPoints, 500); // 5% + assert.equal(transferFeeB.transferFeeBasisPoints, 1000); // 10% + + const tokenAmount = toTokenAmount(1_000_000 * 0.8, 1_000_000 * 0.8); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + belowLowerIndex, + belowUpperIndex, + tokenAmount, + ); + const requiredAmountDelta = PoolUtil.getTokenAmountsFromLiquidity( + liquidityAmount, + PriceMath.tickIndexToSqrtPriceX64(currTick), + PriceMath.tickIndexToSqrtPriceX64(belowLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(belowUpperIndex), + true, + ); + + assert.ok(requiredAmountDelta.tokenA.isZero()); // out of range, all asset is in tokenB + assert.ok(requiredAmountDelta.tokenB.gtn(0)); + const expectedTransferFeeIncludedAmountA = calculateTransferFeeIncludedAmount( + transferFeeA, + requiredAmountDelta.tokenA, + ); + const expectedTransferFeeIncludedAmountB = calculateTransferFeeIncludedAmount( + transferFeeB, + requiredAmountDelta.tokenB, + ); + assert.ok(expectedTransferFeeIncludedAmountA.amount.isZero()); + assert.ok(expectedTransferFeeIncludedAmountA.fee.isZero()); + assert.ok(expectedTransferFeeIncludedAmountB.fee.gtn(0)); + + const preVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const preVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const preOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const preOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: expectedTransferFeeIncludedAmountA.amount, + tokenMaxB: expectedTransferFeeIncludedAmountB.amount, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }), + ).buildAndExecute(); + + const postVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const postVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const postOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const postOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + // owner sent requiredAmountDelta plus transfer fees + assert.ok( + preOwnerAccountBalanceA + .sub(postOwnerAccountBalanceA) + .isZero(), + ); + assert.ok( + preOwnerAccountBalanceB + .sub(postOwnerAccountBalanceB) + .eq(expectedTransferFeeIncludedAmountB.amount) + ); + // vault received requiredAmountDelta + assert.ok(postVaultBalanceA.sub(preVaultBalanceA).isZero()); + assert.ok(postVaultBalanceB.sub(preVaultBalanceB).eq(requiredAmountDelta.tokenB)); + }); + + it("increase_liquidity_v2: [FAIL] TokenMaxExceeded due to transfer fee", async () => { + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const positionInitInfo = positions[0]; + + const transferFeeA = await getTransferFee(poolInitInfo.tokenMintA); + const transferFeeB = await getTransferFee(poolInitInfo.tokenMintB); + assert.equal(transferFeeA.transferFeeBasisPoints, 500); // 5% + assert.equal(transferFeeB.transferFeeBasisPoints, 1000); // 10% + + const tokenAmount = toTokenAmount(1_000_000 * 0.8, 1_000_000 * 0.8); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount, + ); + const requiredAmountDelta = PoolUtil.getTokenAmountsFromLiquidity( + liquidityAmount, + PriceMath.tickIndexToSqrtPriceX64(currTick), + PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex), + true, + ); + + // transfer fee should be non zero + assert.ok(requiredAmountDelta.tokenA.gtn(0)); + assert.ok(requiredAmountDelta.tokenB.gtn(0)); + const expectedTransferFeeIncludedAmountA = calculateTransferFeeIncludedAmount( + transferFeeA, + requiredAmountDelta.tokenA, + ); + const expectedTransferFeeIncludedAmountB = calculateTransferFeeIncludedAmount( + transferFeeB, + requiredAmountDelta.tokenB, + ); + assert.ok(expectedTransferFeeIncludedAmountA.fee.gtn(0)); + assert.ok(expectedTransferFeeIncludedAmountB.fee.gtn(0)); + + const normalParams: IncreaseLiquidityV2Params = { + liquidityAmount, + tokenMaxA: expectedTransferFeeIncludedAmountA.amount, + tokenMaxB: expectedTransferFeeIncludedAmountB.amount, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + }; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + ...normalParams, + // TransferFee is not taken into account + tokenMaxA: requiredAmountDelta.tokenA, + }), + ).buildAndExecute(), + /0x1781/, // TokenMaxExceeded + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + ...normalParams, + // TransferFee is not taken into account + tokenMaxB: requiredAmountDelta.tokenB, + }), + ).buildAndExecute(), + /0x1781/, // TokenMaxExceeded + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + ...normalParams, + // set maxA to expected - 1 + tokenMaxA: requiredAmountDelta.tokenA + .add(expectedTransferFeeIncludedAmountA.fee) + .subn(1), + }), + ).buildAndExecute(), + /0x1781/, // TokenMaxExceeded + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + ...normalParams, + // set maxB to expected - 1 + tokenMaxB: requiredAmountDelta.tokenB + .add(expectedTransferFeeIncludedAmountB.fee) + .subn(1), + }), + ).buildAndExecute(), + /0x1781/, // TokenMaxExceeded + ); + + // success with normal params + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, normalParams), + ).buildAndExecute(); + }); + }); + + describe("decrease_liquidity_v2", () => { + let fixture: WhirlpoolTestFixtureV2; + let destAccountA: PublicKey; + let destAccountB: PublicKey; + + const tickLowerIndex = 7168; + const tickUpperIndex = 8960; + const currTick = Math.round((tickLowerIndex + tickUpperIndex) / 2); + + const aboveLowerIndex = TickUtil.getNextInitializableTickIndex(currTick + 1, TickSpacing.Standard); + const aboveUpperIndex = tickUpperIndex; + const belowLowerIndex = tickLowerIndex; + const belowUpperIndex = TickUtil.getPrevInitializableTickIndex(currTick - 1, TickSpacing.Standard); + + beforeEach(async () => { + const liquidityAmount = new anchor.BN(1_250_000); + fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { + isToken2022: true, + hasTransferFeeExtension: true, + transferFeeInitialBps: 500, + }, // 5% + tokenTraitB: { + isToken2022: true, + hasTransferFeeExtension: true, + transferFeeInitialBps: 1000, + }, // 10% + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount }, + { tickLowerIndex: aboveLowerIndex, tickUpperIndex: aboveUpperIndex, liquidityAmount }, + { tickLowerIndex: belowLowerIndex, tickUpperIndex: belowUpperIndex, liquidityAmount }, + ], + }); + const { poolInitInfo } = fixture.getInfos(); + + destAccountA = await createTokenAccountV2( + provider, + { isToken2022: true }, + poolInitInfo.tokenMintA, + provider.wallet.publicKey, + ); + destAccountB = await createTokenAccountV2( + provider, + { isToken2022: true }, + poolInitInfo.tokenMintB, + provider.wallet.publicKey, + ); + }); + + it("decrease_liquidity_v2: with transfer fee", async () => { + const { poolInitInfo, positions } = fixture.getInfos(); + + const transferFeeA = await getTransferFee(poolInitInfo.tokenMintA); + const transferFeeB = await getTransferFee(poolInitInfo.tokenMintB); + assert.equal(transferFeeA.transferFeeBasisPoints, 500); // 5% + assert.equal(transferFeeB.transferFeeBasisPoints, 1000); // 10% + + const position = positions[0]; + const positionData = (await fetcher.getPosition( + position.publicKey, + IGNORE_CACHE, + )) as PositionData; + const whirlpoolData = (await fetcher.getPool( + positionData.whirlpool, + IGNORE_CACHE, + )) as WhirlpoolData; + const expectedAmount = PoolUtil.getTokenAmountsFromLiquidity( + positionData.liquidity, + whirlpoolData.sqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(positionData.tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(positionData.tickUpperIndex), + false, + ); + + // transfer fee should be non zero + assert.ok(expectedAmount.tokenA.gtn(0)); + assert.ok(expectedAmount.tokenB.gtn(0)); + const expectedTransferFeeExcludedAmountA = calculateTransferFeeExcludedAmount( + transferFeeA, + expectedAmount.tokenA, + ); + const expectedTransferFeeExcludedAmountB = calculateTransferFeeExcludedAmount( + transferFeeB, + expectedAmount.tokenB, + ); + assert.ok(expectedTransferFeeExcludedAmountA.fee.gtn(0)); + assert.ok(expectedTransferFeeExcludedAmountB.fee.gtn(0)); + + const preVaultBalanceA = await getTokenBalance( + provider, + poolInitInfo.tokenVaultAKeypair.publicKey, + ); + const preVaultBalanceB = await getTokenBalance( + provider, + poolInitInfo.tokenVaultBKeypair.publicKey, + ); + + const sig = await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount: positionData.liquidity, + tokenMinA: expectedAmount.tokenA.sub(expectedTransferFeeExcludedAmountA.fee), + tokenMinB: expectedAmount.tokenB.sub(expectedTransferFeeExcludedAmountB.fee), + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: destAccountA, + tokenOwnerAccountB: destAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }), + ).buildAndExecute(); + + const postVaultBalanceA = await getTokenBalance( + provider, + poolInitInfo.tokenVaultAKeypair.publicKey, + ); + const postVaultBalanceB = await getTokenBalance( + provider, + poolInitInfo.tokenVaultBKeypair.publicKey, + ); + assert.ok(new BN(preVaultBalanceA).sub(new BN(postVaultBalanceA)).eq(expectedAmount.tokenA)); + assert.ok(new BN(preVaultBalanceB).sub(new BN(postVaultBalanceB)).eq(expectedAmount.tokenB)); + + // owner received withdrawable amount minus transfer fee (transferFeeExcludedAmount) + const destBalanceA = await getTokenBalance(provider, destAccountA); + const destBalanceB = await getTokenBalance(provider, destAccountB); + //console.log("A", destBalanceA.toString(), expectedTransferFeeExcludedAmountA.amount.toString(), expectedTransferFeeExcludedAmountA.fee.toString()); + //console.log("B", destBalanceB.toString(), expectedTransferFeeExcludedAmountB.amount.toString(), expectedTransferFeeExcludedAmountB.fee.toString()); + + assert.ok(new BN(destBalanceA).eq(expectedTransferFeeExcludedAmountA.amount)); + assert.ok(new BN(destBalanceB).eq(expectedTransferFeeExcludedAmountB.amount)); + + // all liquidity have been decreased + const positionDataAfterWithdraw = (await fetcher.getPosition( + position.publicKey, + IGNORE_CACHE, + )) as PositionData; + assert.ok(positionDataAfterWithdraw.liquidity.isZero()); + }); + + it("decrease_liquidity_v2: transfer fee rate is 0%", async () => { + const { poolInitInfo, positions } = fixture.getInfos(); + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + poolInitInfo.tokenMintA, + 0, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ), + createSetTransferFeeInstruction( + poolInitInfo.tokenMintB, + 0, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ) + ] + }).buildAndExecute(); + + const updatedFeeConfigA = await fetchTransferFeeConfig(poolInitInfo.tokenMintA); + const updatedFeeConfigB = await fetchTransferFeeConfig(poolInitInfo.tokenMintB); + assert.equal(updatedFeeConfigA.newerTransferFee.transferFeeBasisPoints, 0); + assert.equal(updatedFeeConfigB.newerTransferFee.transferFeeBasisPoints, 0); + + // wait for epoch to enable updated fee rate + await waitEpoch(Number(updatedFeeConfigA.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigA.newerTransferFee.epoch); + await waitEpoch(Number(updatedFeeConfigB.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigB.newerTransferFee.epoch); + + const position = positions[0]; + const positionData = (await fetcher.getPosition( + position.publicKey, + IGNORE_CACHE, + )) as PositionData; + const whirlpoolData = (await fetcher.getPool( + positionData.whirlpool, + IGNORE_CACHE, + )) as WhirlpoolData; + const expectedAmount = PoolUtil.getTokenAmountsFromLiquidity( + positionData.liquidity, + whirlpoolData.sqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(positionData.tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(positionData.tickUpperIndex), + false, + ); + + // transfer fee should be zero + assert.ok(expectedAmount.tokenA.gtn(0)); + assert.ok(expectedAmount.tokenB.gtn(0)); + const expectedTransferFeeExcludedAmountA = calculateTransferFeeExcludedAmount( + updatedFeeConfigA.newerTransferFee, + expectedAmount.tokenA, + ); + const expectedTransferFeeExcludedAmountB = calculateTransferFeeExcludedAmount( + updatedFeeConfigB.newerTransferFee, + expectedAmount.tokenB, + ); + assert.ok(expectedTransferFeeExcludedAmountA.fee.isZero()); + assert.ok(expectedTransferFeeExcludedAmountB.fee.isZero()); + assert.ok(expectedTransferFeeExcludedAmountA.amount.eq(expectedAmount.tokenA)); + assert.ok(expectedTransferFeeExcludedAmountB.amount.eq(expectedAmount.tokenB)); + + const preVaultBalanceA = await getTokenBalance( + provider, + poolInitInfo.tokenVaultAKeypair.publicKey, + ); + const preVaultBalanceB = await getTokenBalance( + provider, + poolInitInfo.tokenVaultBKeypair.publicKey, + ); + + const sig = await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount: positionData.liquidity, + tokenMinA: expectedAmount.tokenA.sub(expectedTransferFeeExcludedAmountA.fee), + tokenMinB: expectedAmount.tokenB.sub(expectedTransferFeeExcludedAmountB.fee), + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: destAccountA, + tokenOwnerAccountB: destAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }), + ).buildAndExecute(); + + const postVaultBalanceA = await getTokenBalance( + provider, + poolInitInfo.tokenVaultAKeypair.publicKey, + ); + const postVaultBalanceB = await getTokenBalance( + provider, + poolInitInfo.tokenVaultBKeypair.publicKey, + ); + assert.ok(new BN(preVaultBalanceA).sub(new BN(postVaultBalanceA)).eq(expectedAmount.tokenA)); + assert.ok(new BN(preVaultBalanceB).sub(new BN(postVaultBalanceB)).eq(expectedAmount.tokenB)); + + // owner received withdrawable amount minus transfer fee (0) + const destBalanceA = await getTokenBalance(provider, destAccountA); + const destBalanceB = await getTokenBalance(provider, destAccountB); + + assert.ok(new BN(destBalanceA).eq(expectedTransferFeeExcludedAmountA.amount)); + assert.ok(new BN(destBalanceB).eq(expectedTransferFeeExcludedAmountB.amount)); + }); + + it("decrease_liquidity_v2: transfer fee rate is 100% without cap", async () => { + const { poolInitInfo, positions } = fixture.getInfos(); + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + poolInitInfo.tokenMintA, + MAX_FEE_BASIS_POINTS, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ), + createSetTransferFeeInstruction( + poolInitInfo.tokenMintB, + MAX_FEE_BASIS_POINTS, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ) + ] + }).buildAndExecute(); + + const updatedFeeConfigA = await fetchTransferFeeConfig(poolInitInfo.tokenMintA); + const updatedFeeConfigB = await fetchTransferFeeConfig(poolInitInfo.tokenMintB); + assert.equal(updatedFeeConfigA.newerTransferFee.transferFeeBasisPoints, MAX_FEE_BASIS_POINTS); + assert.equal(updatedFeeConfigB.newerTransferFee.transferFeeBasisPoints, MAX_FEE_BASIS_POINTS); + + // wait for epoch to enable updated fee rate + await waitEpoch(Number(updatedFeeConfigA.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigA.newerTransferFee.epoch); + await waitEpoch(Number(updatedFeeConfigB.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigB.newerTransferFee.epoch); + + const position = positions[0]; + const positionData = (await fetcher.getPosition( + position.publicKey, + IGNORE_CACHE, + )) as PositionData; + const whirlpoolData = (await fetcher.getPool( + positionData.whirlpool, + IGNORE_CACHE, + )) as WhirlpoolData; + const expectedAmount = PoolUtil.getTokenAmountsFromLiquidity( + positionData.liquidity, + whirlpoolData.sqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(positionData.tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(positionData.tickUpperIndex), + false, + ); + + // transfer fee should be zero + assert.ok(expectedAmount.tokenA.gtn(0)); + assert.ok(expectedAmount.tokenB.gtn(0)); + const expectedTransferFeeExcludedAmountA = calculateTransferFeeExcludedAmount( + updatedFeeConfigA.newerTransferFee, + expectedAmount.tokenA, + ); + const expectedTransferFeeExcludedAmountB = calculateTransferFeeExcludedAmount( + updatedFeeConfigB.newerTransferFee, + expectedAmount.tokenB, + ); + assert.ok(expectedTransferFeeExcludedAmountA.fee.eq(expectedAmount.tokenA)); + assert.ok(expectedTransferFeeExcludedAmountB.fee.eq(expectedAmount.tokenB)); + assert.ok(expectedTransferFeeExcludedAmountA.amount.isZero()); + assert.ok(expectedTransferFeeExcludedAmountB.amount.isZero()); + + const preVaultBalanceA = await getTokenBalance( + provider, + poolInitInfo.tokenVaultAKeypair.publicKey, + ); + const preVaultBalanceB = await getTokenBalance( + provider, + poolInitInfo.tokenVaultBKeypair.publicKey, + ); + + const sig = await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount: positionData.liquidity, + tokenMinA: expectedAmount.tokenA.sub(expectedTransferFeeExcludedAmountA.fee), + tokenMinB: expectedAmount.tokenB.sub(expectedTransferFeeExcludedAmountB.fee), + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: destAccountA, + tokenOwnerAccountB: destAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }), + ).buildAndExecute(); + + const postVaultBalanceA = await getTokenBalance( + provider, + poolInitInfo.tokenVaultAKeypair.publicKey, + ); + const postVaultBalanceB = await getTokenBalance( + provider, + poolInitInfo.tokenVaultBKeypair.publicKey, + ); + assert.ok(new BN(preVaultBalanceA).sub(new BN(postVaultBalanceA)).eq(expectedAmount.tokenA)); + assert.ok(new BN(preVaultBalanceB).sub(new BN(postVaultBalanceB)).eq(expectedAmount.tokenB)); + + // owner received 0 tokens + const destBalanceA = await getTokenBalance(provider, destAccountA); + const destBalanceB = await getTokenBalance(provider, destAccountB); + + // all amount is collected as transfer fee + assert.ok(new BN(destBalanceA).isZero()); + assert.ok(new BN(destBalanceB).isZero()); + const withheldAmountA = await fetchTransferFeeWithheldAmount(destAccountA); + const withheldAmountB = await fetchTransferFeeWithheldAmount(destAccountB); + assert.ok(withheldAmountA.eq(expectedAmount.tokenA)); + assert.ok(withheldAmountB.eq(expectedAmount.tokenB)); + }); + + it("decrease_liquidity_v2: transfer fee rate is 100% with cap", async () => { + const { poolInitInfo, positions } = fixture.getInfos(); + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + poolInitInfo.tokenMintA, + MAX_FEE_BASIS_POINTS, + 99n, // cap + provider.wallet.publicKey, + ), + createSetTransferFeeInstruction( + poolInitInfo.tokenMintB, + MAX_FEE_BASIS_POINTS, + 99n, // cap + provider.wallet.publicKey, + ) + ] + }).buildAndExecute(); + + const updatedFeeConfigA = await fetchTransferFeeConfig(poolInitInfo.tokenMintA); + const updatedFeeConfigB = await fetchTransferFeeConfig(poolInitInfo.tokenMintB); + assert.equal(updatedFeeConfigA.newerTransferFee.transferFeeBasisPoints, MAX_FEE_BASIS_POINTS); + assert.equal(updatedFeeConfigB.newerTransferFee.transferFeeBasisPoints, MAX_FEE_BASIS_POINTS); + + // wait for epoch to enable updated fee rate + await waitEpoch(Number(updatedFeeConfigA.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigA.newerTransferFee.epoch); + await waitEpoch(Number(updatedFeeConfigB.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigB.newerTransferFee.epoch); + + const position = positions[0]; + const positionData = (await fetcher.getPosition( + position.publicKey, + IGNORE_CACHE, + )) as PositionData; + const whirlpoolData = (await fetcher.getPool( + positionData.whirlpool, + IGNORE_CACHE, + )) as WhirlpoolData; + const expectedAmount = PoolUtil.getTokenAmountsFromLiquidity( + positionData.liquidity, + whirlpoolData.sqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(positionData.tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(positionData.tickUpperIndex), + false, + ); + + // transfer fee should be zero + assert.ok(expectedAmount.tokenA.gtn(0)); + assert.ok(expectedAmount.tokenB.gtn(0)); + const expectedTransferFeeExcludedAmountA = calculateTransferFeeExcludedAmount( + updatedFeeConfigA.newerTransferFee, + expectedAmount.tokenA, + ); + const expectedTransferFeeExcludedAmountB = calculateTransferFeeExcludedAmount( + updatedFeeConfigB.newerTransferFee, + expectedAmount.tokenB, + ); + assert.ok(expectedTransferFeeExcludedAmountA.fee.eqn(99)); + assert.ok(expectedTransferFeeExcludedAmountB.fee.eqn(99)); + + const preVaultBalanceA = await getTokenBalance( + provider, + poolInitInfo.tokenVaultAKeypair.publicKey, + ); + const preVaultBalanceB = await getTokenBalance( + provider, + poolInitInfo.tokenVaultBKeypair.publicKey, + ); + + const sig = await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount: positionData.liquidity, + tokenMinA: expectedAmount.tokenA.sub(expectedTransferFeeExcludedAmountA.fee), + tokenMinB: expectedAmount.tokenB.sub(expectedTransferFeeExcludedAmountB.fee), + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: destAccountA, + tokenOwnerAccountB: destAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }), + ).buildAndExecute(); + + const postVaultBalanceA = await getTokenBalance( + provider, + poolInitInfo.tokenVaultAKeypair.publicKey, + ); + const postVaultBalanceB = await getTokenBalance( + provider, + poolInitInfo.tokenVaultBKeypair.publicKey, + ); + assert.ok(new BN(preVaultBalanceA).sub(new BN(postVaultBalanceA)).eq(expectedAmount.tokenA)); + assert.ok(new BN(preVaultBalanceB).sub(new BN(postVaultBalanceB)).eq(expectedAmount.tokenB)); + + // owner received expectedAmount minus capped transfer fee + const destBalanceA = await getTokenBalance(provider, destAccountA); + const destBalanceB = await getTokenBalance(provider, destAccountB); + + // all amount is collected as transfer fee + assert.ok(new BN(destBalanceA).eq(expectedTransferFeeExcludedAmountA.amount)); + assert.ok(new BN(destBalanceB).eq(expectedTransferFeeExcludedAmountB.amount)); + const withheldAmountA = await fetchTransferFeeWithheldAmount(destAccountA); + const withheldAmountB = await fetchTransferFeeWithheldAmount(destAccountB); + assert.ok(withheldAmountA.eqn(99)); + assert.ok(withheldAmountB.eqn(99)); + }); + + it("decrease_liquidity_v2: out or range (above, tokenB amount is zero", async () => { + const { poolInitInfo, positions } = fixture.getInfos(); + + const transferFeeA = await getTransferFee(poolInitInfo.tokenMintA); + const transferFeeB = await getTransferFee(poolInitInfo.tokenMintB); + assert.equal(transferFeeA.transferFeeBasisPoints, 500); // 5% + assert.equal(transferFeeB.transferFeeBasisPoints, 1000); // 10% + + const position = positions[1]; // [1] for above + const positionData = (await fetcher.getPosition( + position.publicKey, + IGNORE_CACHE, + )) as PositionData; + const whirlpoolData = (await fetcher.getPool( + positionData.whirlpool, + IGNORE_CACHE, + )) as WhirlpoolData; + const expectedAmount = PoolUtil.getTokenAmountsFromLiquidity( + positionData.liquidity, + whirlpoolData.sqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(positionData.tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(positionData.tickUpperIndex), + false, + ); + + assert.ok(expectedAmount.tokenA.gtn(0)); + assert.ok(expectedAmount.tokenB.isZero()); + const expectedTransferFeeExcludedAmountA = calculateTransferFeeExcludedAmount( + transferFeeA, + expectedAmount.tokenA, + ); + const expectedTransferFeeExcludedAmountB = calculateTransferFeeExcludedAmount( + transferFeeB, + expectedAmount.tokenB, + ); + assert.ok(expectedTransferFeeExcludedAmountA.fee.gtn(0)); + assert.ok(expectedTransferFeeExcludedAmountB.fee.isZero()); + + const preVaultBalanceA = await getTokenBalance( + provider, + poolInitInfo.tokenVaultAKeypair.publicKey, + ); + const preVaultBalanceB = await getTokenBalance( + provider, + poolInitInfo.tokenVaultBKeypair.publicKey, + ); + + const sig = await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount: positionData.liquidity, + tokenMinA: expectedAmount.tokenA.sub(expectedTransferFeeExcludedAmountA.fee), + tokenMinB: expectedAmount.tokenB.sub(expectedTransferFeeExcludedAmountB.fee), + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: destAccountA, + tokenOwnerAccountB: destAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }), + ).buildAndExecute(); + + const postVaultBalanceA = await getTokenBalance( + provider, + poolInitInfo.tokenVaultAKeypair.publicKey, + ); + const postVaultBalanceB = await getTokenBalance( + provider, + poolInitInfo.tokenVaultBKeypair.publicKey, + ); + assert.ok(new BN(preVaultBalanceA).sub(new BN(postVaultBalanceA)).eq(expectedAmount.tokenA)); + assert.ok(new BN(preVaultBalanceB).sub(new BN(postVaultBalanceB)).isZero()); + + const destBalanceA = await getTokenBalance(provider, destAccountA); + const destBalanceB = await getTokenBalance(provider, destAccountB); + assert.ok(new BN(destBalanceA).eq(expectedTransferFeeExcludedAmountA.amount)); + assert.ok(new BN(destBalanceB).isZero()); + }); + + it("decrease_liquidity_v2: out or range (above, tokenA amount is zero", async () => { + const { poolInitInfo, positions } = fixture.getInfos(); + + const transferFeeA = await getTransferFee(poolInitInfo.tokenMintA); + const transferFeeB = await getTransferFee(poolInitInfo.tokenMintB); + assert.equal(transferFeeA.transferFeeBasisPoints, 500); // 5% + assert.equal(transferFeeB.transferFeeBasisPoints, 1000); // 10% + + const position = positions[2]; // [2] for below + const positionData = (await fetcher.getPosition( + position.publicKey, + IGNORE_CACHE, + )) as PositionData; + const whirlpoolData = (await fetcher.getPool( + positionData.whirlpool, + IGNORE_CACHE, + )) as WhirlpoolData; + const expectedAmount = PoolUtil.getTokenAmountsFromLiquidity( + positionData.liquidity, + whirlpoolData.sqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(positionData.tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(positionData.tickUpperIndex), + false, + ); + + assert.ok(expectedAmount.tokenA.isZero()); + assert.ok(expectedAmount.tokenB.gtn(0)); + const expectedTransferFeeExcludedAmountA = calculateTransferFeeExcludedAmount( + transferFeeA, + expectedAmount.tokenA, + ); + const expectedTransferFeeExcludedAmountB = calculateTransferFeeExcludedAmount( + transferFeeB, + expectedAmount.tokenB, + ); + assert.ok(expectedTransferFeeExcludedAmountA.fee.isZero()); + assert.ok(expectedTransferFeeExcludedAmountB.fee.gtn(0)); + + const preVaultBalanceA = await getTokenBalance( + provider, + poolInitInfo.tokenVaultAKeypair.publicKey, + ); + const preVaultBalanceB = await getTokenBalance( + provider, + poolInitInfo.tokenVaultBKeypair.publicKey, + ); + + const sig = await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount: positionData.liquidity, + tokenMinA: expectedAmount.tokenA.sub(expectedTransferFeeExcludedAmountA.fee), + tokenMinB: expectedAmount.tokenB.sub(expectedTransferFeeExcludedAmountB.fee), + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: destAccountA, + tokenOwnerAccountB: destAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }), + ).buildAndExecute(); + + const postVaultBalanceA = await getTokenBalance( + provider, + poolInitInfo.tokenVaultAKeypair.publicKey, + ); + const postVaultBalanceB = await getTokenBalance( + provider, + poolInitInfo.tokenVaultBKeypair.publicKey, + ); + assert.ok(new BN(preVaultBalanceA).sub(new BN(postVaultBalanceA)).isZero()); + assert.ok(new BN(preVaultBalanceB).sub(new BN(postVaultBalanceB)).eq(expectedAmount.tokenB)); + + const destBalanceA = await getTokenBalance(provider, destAccountA); + const destBalanceB = await getTokenBalance(provider, destAccountB); + assert.ok(new BN(destBalanceA).isZero()); + assert.ok(new BN(destBalanceB).eq(expectedTransferFeeExcludedAmountB.amount)); + }); + + it("decrease_liquidity_v2: [FAIL] TokenMinSubceeded due to transfer fee", async () => { + const { poolInitInfo, positions } = fixture.getInfos(); + + const transferFeeA = await getTransferFee(poolInitInfo.tokenMintA); + const transferFeeB = await getTransferFee(poolInitInfo.tokenMintB); + assert.equal(transferFeeA.transferFeeBasisPoints, 500); // 5% + assert.equal(transferFeeB.transferFeeBasisPoints, 1000); // 10% + + const position = positions[0]; + const positionData = (await fetcher.getPosition( + position.publicKey, + IGNORE_CACHE, + )) as PositionData; + const whirlpoolData = (await fetcher.getPool( + positionData.whirlpool, + IGNORE_CACHE, + )) as WhirlpoolData; + const expectedAmount = PoolUtil.getTokenAmountsFromLiquidity( + positionData.liquidity, + whirlpoolData.sqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(positionData.tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(positionData.tickUpperIndex), + false, + ); + + // transfer fee should be non zero + assert.ok(expectedAmount.tokenA.gtn(0)); + assert.ok(expectedAmount.tokenB.gtn(0)); + const expectedTransferFeeExcludedAmountA = calculateTransferFeeExcludedAmount( + transferFeeA, + expectedAmount.tokenA, + ); + const expectedTransferFeeExcludedAmountB = calculateTransferFeeExcludedAmount( + transferFeeB, + expectedAmount.tokenB, + ); + assert.ok(expectedTransferFeeExcludedAmountA.fee.gtn(0)); + assert.ok(expectedTransferFeeExcludedAmountB.fee.gtn(0)); + + const normalParams: DecreaseLiquidityV2Params = { + liquidityAmount: positionData.liquidity, + tokenMinA: expectedAmount.tokenA.sub(expectedTransferFeeExcludedAmountA.fee), + tokenMinB: expectedAmount.tokenB.sub(expectedTransferFeeExcludedAmountB.fee), + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: position.publicKey, + positionTokenAccount: position.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: destAccountA, + tokenOwnerAccountB: destAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: position.tickArrayLower, + tickArrayUpper: position.tickArrayUpper, + }; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + ...normalParams, + // TransferFee is not taken into account + tokenMinA: expectedAmount.tokenA, + }), + ).buildAndExecute(), + /0x1782/, // TokenMinSubceeded + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + ...normalParams, + // TransferFee is not taken into account + tokenMinB: expectedAmount.tokenB, + }), + ).buildAndExecute(), + /0x1782/, // TokenMinSubceeded + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + ...normalParams, + // set minA to expected + 1 + tokenMinA: expectedAmount.tokenA.sub(expectedTransferFeeExcludedAmountA.fee).addn(1), + }), + ).buildAndExecute(), + /0x1782/, // TokenMinSubceeded + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + ...normalParams, + // set minB to expected + 1 + tokenMinB: expectedAmount.tokenB.sub(expectedTransferFeeExcludedAmountB.fee).addn(1), + }), + ).buildAndExecute(), + /0x1782/, // TokenMinSubceeded + ); + + // success with normal params + await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, normalParams), + ).buildAndExecute(); + }); + }); + + describe("swap_v2", () => { + let poolInitInfo: InitPoolV2Params; + let whirlpoolPda: PDA; + let transferFeeA: TransferFee | null; + let transferFeeB: TransferFee | null; + let tokenAccountA: PublicKey; + let tokenAccountB: PublicKey; + let oraclePubkey: PublicKey; + + const variations: { tokenA: TokenTrait; tokenB: TokenTrait }[] = [ + // both A & B has transfer fee + { + tokenA: { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 500 }, + tokenB: { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 1000 }, + }, + // only A has transfer fee + { + tokenA: { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 500 }, + tokenB: { isToken2022: true, hasTransferFeeExtension: false }, + }, + // only B has transfer fee + { + tokenA: { isToken2022: true, hasTransferFeeExtension: false }, + tokenB: { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 1000 }, + }, + // both A & B has transfer fee extension, but bps is zero + { + tokenA: { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + tokenB: { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + }, + ]; + + variations.forEach(({ tokenA, tokenB }) => { + const labelA = `TokenA: transfer fee bps = ${ + tokenA.hasTransferFeeExtension ? tokenA.transferFeeInitialBps?.toString() : "none" + }`; + const labelB = `TokenB: transfer fee bps = ${ + tokenB.hasTransferFeeExtension ? tokenB.transferFeeInitialBps?.toString() : "none" + }`; + describe(`${labelA}, ${labelB}`, () => { + beforeEach(async () => { + const init = await initTestPoolWithTokensV2(ctx, tokenA, tokenB, TickSpacing.Standard); + poolInitInfo = init.poolInitInfo; + whirlpoolPda = init.whirlpoolPda; + tokenAccountA = init.tokenAccountA; + tokenAccountB = init.tokenAccountB; + + const aToB = false; + await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, // to 33792 + 3, + TickSpacing.Standard, + aToB, + ); + + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + + await fundPositionsV2(ctx, poolInitInfo, tokenAccountA, tokenAccountB, fundParams); + oraclePubkey = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey).publicKey; + + transferFeeA = tokenA.hasTransferFeeExtension + ? await getTransferFee(poolInitInfo.tokenMintA) + : null; + transferFeeB = tokenB.hasTransferFeeExtension + ? await getTransferFee(poolInitInfo.tokenMintB) + : null; + + if (transferFeeA) + assert.equal(transferFeeA.transferFeeBasisPoints, tokenA.transferFeeInitialBps!); + if (transferFeeB) + assert.equal(transferFeeB.transferFeeBasisPoints, tokenB.transferFeeInitialBps!); + }); + + it("A --> B, ExactIn", async () => { + const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey; + const whirlpoolData = (await fetcher.getPool( + whirlpoolKey, + IGNORE_CACHE, + )) as WhirlpoolData; + + const aToB = true; + const inputAmount = new BN(100000); + const transferFeeExcludedInputAmount = transferFeeA + ? calculateTransferFeeExcludedAmount(transferFeeA, inputAmount) + : { amount: inputAmount, fee: ZERO_BN }; + if (transferFeeA && transferFeeA.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedInputAmount.fee.gtn(0)); + + const quoteAToB = swapQuoteWithParams( + { + // A --> B, ExactIn + amountSpecifiedIsInput: true, + aToB, + tokenAmount: transferFeeExcludedInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), // 0% slippage + ); + + const transferFeeExcludedOutputAmount = transferFeeB + ? calculateTransferFeeExcludedAmount(transferFeeB, quoteAToB.estimatedAmountOut) + : { amount: quoteAToB.estimatedAmountOut, fee: ZERO_BN }; + if (transferFeeB && transferFeeB.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedOutputAmount.fee.gtn(0)); + + const expectedOwnerAccountADelta = inputAmount.neg(); // out + const expectedOwnerAccountBDelta = transferFeeExcludedOutputAmount.amount; // in + const expectedVaultAccountADelta = transferFeeExcludedInputAmount.amount; // in + const expectedVaultAccountBDelta = quoteAToB.estimatedAmountOut.neg(); // out + assert.ok(expectedVaultAccountADelta.eq(quoteAToB.estimatedAmountIn)); + assert.ok(expectedVaultAccountBDelta.eq(quoteAToB.estimatedAmountOut.neg())); + + const preVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const preVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const preOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const preOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteAToB, + amount: inputAmount, // transfer fee included + otherAmountThreshold: transferFeeExcludedOutputAmount.amount.addn(1), // transfer fee excluded + + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(), + /0x1794/, // AmountOutBelowMinimum + ); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteAToB, + amount: inputAmount, // transfer fee included + otherAmountThreshold: transferFeeExcludedOutputAmount.amount, // transfer fee excluded + + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(); + + const postVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const postVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const postOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const postOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + assert.ok(postVaultBalanceA.sub(preVaultBalanceA).eq(expectedVaultAccountADelta)); + assert.ok(postVaultBalanceB.sub(preVaultBalanceB).eq(expectedVaultAccountBDelta)); + assert.ok( + postOwnerAccountBalanceA.sub(preOwnerAccountBalanceA).eq(expectedOwnerAccountADelta), + ); + assert.ok( + postOwnerAccountBalanceB.sub(preOwnerAccountBalanceB).eq(expectedOwnerAccountBDelta), + ); + }); + + it("A <-- B, ExactIn", async () => { + const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey; + const whirlpoolData = (await fetcher.getPool( + whirlpoolKey, + IGNORE_CACHE, + )) as WhirlpoolData; + + const aToB = false; + const inputAmount = new BN(100000); + const transferFeeExcludedInputAmount = transferFeeB + ? calculateTransferFeeExcludedAmount(transferFeeB, inputAmount) + : { amount: inputAmount, fee: ZERO_BN }; + if (transferFeeB && transferFeeB.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedInputAmount.fee.gtn(0)); + + const quoteBToA = swapQuoteWithParams( + { + // A <-- B, ExactIn + amountSpecifiedIsInput: true, + aToB, + tokenAmount: transferFeeExcludedInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), // 0% slippage + ); + + const transferFeeExcludedOutputAmount = transferFeeA + ? calculateTransferFeeExcludedAmount(transferFeeA, quoteBToA.estimatedAmountOut) + : { amount: quoteBToA.estimatedAmountOut, fee: ZERO_BN }; + if (transferFeeA && transferFeeA.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedOutputAmount.fee.gtn(0)); + + const expectedOwnerAccountADelta = transferFeeExcludedOutputAmount.amount; // in + const expectedOwnerAccountBDelta = inputAmount.neg(); // out + const expectedVaultAccountADelta = quoteBToA.estimatedAmountOut.neg(); // out + const expectedVaultAccountBDelta = transferFeeExcludedInputAmount.amount; // in + assert.ok(expectedVaultAccountADelta.eq(quoteBToA.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountBDelta.eq(quoteBToA.estimatedAmountIn)); + + const preVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const preVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const preOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const preOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteBToA, + amount: inputAmount, // transfer fee included + otherAmountThreshold: transferFeeExcludedOutputAmount.amount.addn(1), // transfer fee excluded + + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(), + /0x1794/, // AmountOutBelowMinimum + ); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteBToA, + amount: inputAmount, // transfer fee included + otherAmountThreshold: transferFeeExcludedOutputAmount.amount, // transfer fee excluded + + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(); + + const postVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const postVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const postOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const postOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + assert.ok(postVaultBalanceA.sub(preVaultBalanceA).eq(expectedVaultAccountADelta)); + assert.ok(postVaultBalanceB.sub(preVaultBalanceB).eq(expectedVaultAccountBDelta)); + assert.ok( + postOwnerAccountBalanceA.sub(preOwnerAccountBalanceA).eq(expectedOwnerAccountADelta), + ); + assert.ok( + postOwnerAccountBalanceB.sub(preOwnerAccountBalanceB).eq(expectedOwnerAccountBDelta), + ); + }); + + it("A --> B, ExactOut", async () => { + const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey; + const whirlpoolData = (await fetcher.getPool( + whirlpoolKey, + IGNORE_CACHE, + )) as WhirlpoolData; + + const aToB = true; + const outputAmount = new BN(2000000); + const transferFeeIncludedOutputAmount = transferFeeB + ? calculateTransferFeeIncludedAmount(transferFeeB, outputAmount) + : { amount: outputAmount, fee: ZERO_BN }; + if (transferFeeB && transferFeeB.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedOutputAmount.fee.gtn(0)); + + const quoteAToB = swapQuoteWithParams( + { + // A --> B, ExactOut + amountSpecifiedIsInput: false, + aToB, + tokenAmount: transferFeeIncludedOutputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), // 0% slippage + ); + + const transferFeeIncludedInputAmount = transferFeeA + ? calculateTransferFeeIncludedAmount(transferFeeA, quoteAToB.estimatedAmountIn) + : { amount: quoteAToB.estimatedAmountIn, fee: ZERO_BN }; + if (transferFeeA && transferFeeA.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedInputAmount.fee.gtn(0)); + + const expectedOwnerAccountADelta = transferFeeIncludedInputAmount.amount.neg(); // out + const expectedOwnerAccountBDelta = outputAmount; // in + const expectedVaultAccountADelta = quoteAToB.estimatedAmountIn; // in + const expectedVaultAccountBDelta = transferFeeIncludedOutputAmount.amount.neg(); // out + assert.ok(expectedVaultAccountADelta.eq(quoteAToB.estimatedAmountIn)); + assert.ok(expectedVaultAccountBDelta.eq(quoteAToB.estimatedAmountOut.neg())); + + const preVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const preVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const preOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const preOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteAToB, + amount: outputAmount, // transfer fee excluded + otherAmountThreshold: transferFeeIncludedInputAmount.amount.subn(1), // transfer fee included + + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(), + /0x1795/, // AmountInAboveMaximum + ); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteAToB, + amount: outputAmount, // transfer fee excluded + otherAmountThreshold: transferFeeIncludedInputAmount.amount, // transfer fee included + + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(); + + const postVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const postVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const postOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const postOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + assert.ok(postVaultBalanceA.sub(preVaultBalanceA).eq(expectedVaultAccountADelta)); + assert.ok(postVaultBalanceB.sub(preVaultBalanceB).eq(expectedVaultAccountBDelta)); + assert.ok( + postOwnerAccountBalanceA.sub(preOwnerAccountBalanceA).eq(expectedOwnerAccountADelta), + ); + assert.ok( + postOwnerAccountBalanceB.sub(preOwnerAccountBalanceB).eq(expectedOwnerAccountBDelta), + ); + }); + + it("A <-- B, ExactOut", async () => { + const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey; + const whirlpoolData = (await fetcher.getPool( + whirlpoolKey, + IGNORE_CACHE, + )) as WhirlpoolData; + + const aToB = false; + const outputAmount = new BN(100000); + const transferFeeIncludedOutputAmount = transferFeeA + ? calculateTransferFeeIncludedAmount(transferFeeA, outputAmount) + : { amount: outputAmount, fee: ZERO_BN }; + if (transferFeeA && transferFeeA.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedOutputAmount.fee.gtn(0)); + + const quoteBToA = swapQuoteWithParams( + { + // A <-- B, ExactOut + amountSpecifiedIsInput: false, + aToB, + tokenAmount: transferFeeIncludedOutputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), // 0% slippage + ); + + const transferFeeIncludedInputAmount = transferFeeB + ? calculateTransferFeeIncludedAmount(transferFeeB, quoteBToA.estimatedAmountIn) + : { amount: quoteBToA.estimatedAmountIn, fee: ZERO_BN }; + if (transferFeeB && transferFeeB.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedInputAmount.fee.gtn(0)); + + const expectedOwnerAccountADelta = outputAmount; // in + const expectedOwnerAccountBDelta = transferFeeIncludedInputAmount.amount.neg(); // out + const expectedVaultAccountADelta = transferFeeIncludedOutputAmount.amount.neg(); // out + const expectedVaultAccountBDelta = quoteBToA.estimatedAmountIn; // in + assert.ok(expectedVaultAccountADelta.eq(quoteBToA.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountBDelta.eq(quoteBToA.estimatedAmountIn)); + + const preVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const preVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const preOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const preOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteBToA, + amount: outputAmount, // transfer fee excluded + otherAmountThreshold: transferFeeIncludedInputAmount.amount.subn(1), // transfer fee included + + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(), + /0x1795/, // AmountInAboveMaximum + ); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteBToA, + amount: outputAmount, // transfer fee excluded + otherAmountThreshold: transferFeeIncludedInputAmount.amount, // transfer fee included + + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(); + + const postVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const postVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const postOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const postOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + assert.ok(postVaultBalanceA.sub(preVaultBalanceA).eq(expectedVaultAccountADelta)); + assert.ok(postVaultBalanceB.sub(preVaultBalanceB).eq(expectedVaultAccountBDelta)); + assert.ok( + postOwnerAccountBalanceA.sub(preOwnerAccountBalanceA).eq(expectedOwnerAccountADelta), + ); + assert.ok( + postOwnerAccountBalanceB.sub(preOwnerAccountBalanceB).eq(expectedOwnerAccountBDelta), + ); + }); + }); + }); + + const variationsWith100PercentFee: { tokenA: TokenTrait; tokenB: TokenTrait }[] = [ + { + tokenA: { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: MAX_FEE_BASIS_POINTS }, + tokenB: { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + }, + { + tokenA: { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: MAX_FEE_BASIS_POINTS, transferFeeInitialMax: 99n }, + tokenB: { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + }, + { + tokenA: { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + tokenB: { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: MAX_FEE_BASIS_POINTS }, + }, + { + tokenA: { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + tokenB: { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: MAX_FEE_BASIS_POINTS, transferFeeInitialMax: 99n }, + }, + ]; + + variationsWith100PercentFee.forEach(({ tokenA, tokenB }) => { + const labelA = `TokenA: transfer fee bps = ${tokenA.transferFeeInitialBps ? ("100%" + (tokenA.transferFeeInitialMax? " with cap" : " without cap")) : "0%"}`; + const labelB = `TokenB: transfer fee bps = ${tokenB.transferFeeInitialBps ? ("100%" + (tokenB.transferFeeInitialMax? " with cap" : " without cap")) : "0%"}`; + + describe(`${labelA}, ${labelB}`, () => { + beforeEach(async () => { + const init = await initTestPoolWithTokensV2( + ctx, + {isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0}, + {isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0}, + TickSpacing.Standard + ); + poolInitInfo = init.poolInitInfo; + whirlpoolPda = init.whirlpoolPda; + tokenAccountA = init.tokenAccountA; + tokenAccountB = init.tokenAccountB; + + const aToB = false; + await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, // to 33792 + 3, + TickSpacing.Standard, + aToB, + ); + + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + + await fundPositionsV2(ctx, poolInitInfo, tokenAccountA, tokenAccountB, fundParams); + oraclePubkey = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey).publicKey; + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + poolInitInfo.tokenMintA, + tokenA.transferFeeInitialBps!, + tokenA.transferFeeInitialMax ?? BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ), + createSetTransferFeeInstruction( + poolInitInfo.tokenMintB, + tokenB.transferFeeInitialBps!, + tokenB.transferFeeInitialMax ?? BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ) + ] + }).buildAndExecute(); + + // wait for epoch to enable updated fee rate + const updatedFeeConfigA = await fetchTransferFeeConfig(poolInitInfo.tokenMintA); + await waitEpoch(Number(updatedFeeConfigA.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigA.newerTransferFee.epoch); + + transferFeeA = tokenA.hasTransferFeeExtension + ? await getTransferFee(poolInitInfo.tokenMintA) + : null; + transferFeeB = tokenB.hasTransferFeeExtension + ? await getTransferFee(poolInitInfo.tokenMintB) + : null; + + assert.equal(transferFeeA!.transferFeeBasisPoints, tokenA.transferFeeInitialBps!); + assert.equal(transferFeeA!.maximumFee, tokenA.transferFeeInitialMax ?? BigInt(U64_MAX.toString())); + assert.equal(transferFeeB!.transferFeeBasisPoints, tokenB.transferFeeInitialBps!); + assert.equal(transferFeeB!.maximumFee, tokenB.transferFeeInitialMax ?? BigInt(U64_MAX.toString())); + }); + + it("A --> B, ExactIn", async () => { + const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey; + const whirlpoolData = (await fetcher.getPool( + whirlpoolKey, + IGNORE_CACHE, + )) as WhirlpoolData; + + const aToB = true; + const inputAmount = new BN(100000); + + // edge-case + if (transferFeeA!.transferFeeBasisPoints === MAX_FEE_BASIS_POINTS && transferFeeA!.maximumFee === BigInt(U64_MAX.toString())) { + // we cannot determine input size because all amount will be collected as transfer fee + const tickArrays = await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE, + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: inputAmount, + otherAmountThreshold: new BN(0), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + amountSpecifiedIsInput: true, + aToB, + tickArray0: tickArrays[0].address, + tickArray1: tickArrays[0].address, + tickArray2: tickArrays[0].address, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(), + /0x1793/, // ZeroTradableAmount (All amount is collected as transfer fee...) + ); + + return; + } + + const transferFeeExcludedInputAmount = transferFeeA + ? calculateTransferFeeExcludedAmount(transferFeeA, inputAmount) + : { amount: inputAmount, fee: ZERO_BN }; + if (transferFeeA && transferFeeA.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedInputAmount.fee.gtn(0)); + + const quoteAToB = swapQuoteWithParams( + { + // A --> B, ExactIn + amountSpecifiedIsInput: true, + aToB, + tokenAmount: transferFeeExcludedInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), // 0% slippage + ); + + const transferFeeExcludedOutputAmount = transferFeeB + ? calculateTransferFeeExcludedAmount(transferFeeB, quoteAToB.estimatedAmountOut) + : { amount: quoteAToB.estimatedAmountOut, fee: ZERO_BN }; + if (transferFeeB && transferFeeB.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedOutputAmount.fee.gtn(0)); + + const expectedOwnerAccountADelta = inputAmount.neg(); // out + const expectedOwnerAccountBDelta = transferFeeExcludedOutputAmount.amount; // in + const expectedVaultAccountADelta = transferFeeExcludedInputAmount.amount; // in + const expectedVaultAccountBDelta = quoteAToB.estimatedAmountOut.neg(); // out + assert.ok(expectedVaultAccountADelta.eq(quoteAToB.estimatedAmountIn)); + assert.ok(expectedVaultAccountBDelta.eq(quoteAToB.estimatedAmountOut.neg())); + + const preVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const preVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const preOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const preOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteAToB, + amount: inputAmount, // transfer fee included + otherAmountThreshold: transferFeeExcludedOutputAmount.amount.addn(1), // transfer fee excluded + + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(), + /0x1794/, // AmountOutBelowMinimum + ); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteAToB, + amount: inputAmount, // transfer fee included + otherAmountThreshold: transferFeeExcludedOutputAmount.amount, // transfer fee excluded + + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(); + + const postVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const postVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const postOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const postOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + assert.ok(postVaultBalanceA.sub(preVaultBalanceA).eq(expectedVaultAccountADelta)); + assert.ok(postVaultBalanceB.sub(preVaultBalanceB).eq(expectedVaultAccountBDelta)); + assert.ok( + postOwnerAccountBalanceA.sub(preOwnerAccountBalanceA).eq(expectedOwnerAccountADelta), + ); + assert.ok( + postOwnerAccountBalanceB.sub(preOwnerAccountBalanceB).eq(expectedOwnerAccountBDelta), + ); + }); + + it("A <-- B, ExactIn", async () => { + const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey; + const whirlpoolData = (await fetcher.getPool( + whirlpoolKey, + IGNORE_CACHE, + )) as WhirlpoolData; + + const aToB = false; + const inputAmount = new BN(100000); + + // edge-case + if (transferFeeB!.transferFeeBasisPoints === MAX_FEE_BASIS_POINTS && transferFeeB!.maximumFee === BigInt(U64_MAX.toString())) { + // we cannot determine input size because all amount will be collected as transfer fee + const tickArrays = await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE, + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: inputAmount, + otherAmountThreshold: new BN(0), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + amountSpecifiedIsInput: true, + aToB, + tickArray0: tickArrays[0].address, + tickArray1: tickArrays[0].address, + tickArray2: tickArrays[0].address, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(), + /0x1793/, // ZeroTradableAmount (All amount is collected as transfer fee...) + ); + + return; + } + + const transferFeeExcludedInputAmount = transferFeeB + ? calculateTransferFeeExcludedAmount(transferFeeB, inputAmount) + : { amount: inputAmount, fee: ZERO_BN }; + if (transferFeeB && transferFeeB.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedInputAmount.fee.gtn(0)); + + const quoteBToA = swapQuoteWithParams( + { + // A <-- B, ExactIn + amountSpecifiedIsInput: true, + aToB, + tokenAmount: transferFeeExcludedInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), // 0% slippage + ); + + const transferFeeExcludedOutputAmount = transferFeeA + ? calculateTransferFeeExcludedAmount(transferFeeA, quoteBToA.estimatedAmountOut) + : { amount: quoteBToA.estimatedAmountOut, fee: ZERO_BN }; + if (transferFeeA && transferFeeA.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedOutputAmount.fee.gtn(0)); + + const expectedOwnerAccountADelta = transferFeeExcludedOutputAmount.amount; // in + const expectedOwnerAccountBDelta = inputAmount.neg(); // out + const expectedVaultAccountADelta = quoteBToA.estimatedAmountOut.neg(); // out + const expectedVaultAccountBDelta = transferFeeExcludedInputAmount.amount; // in + assert.ok(expectedVaultAccountADelta.eq(quoteBToA.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountBDelta.eq(quoteBToA.estimatedAmountIn)); + + const preVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const preVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const preOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const preOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteBToA, + amount: inputAmount, // transfer fee included + otherAmountThreshold: transferFeeExcludedOutputAmount.amount.addn(1), // transfer fee excluded + + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(), + /0x1794/, // AmountOutBelowMinimum + ); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteBToA, + amount: inputAmount, // transfer fee included + otherAmountThreshold: transferFeeExcludedOutputAmount.amount, // transfer fee excluded + + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(); + + const postVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const postVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const postOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const postOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + assert.ok(postVaultBalanceA.sub(preVaultBalanceA).eq(expectedVaultAccountADelta)); + assert.ok(postVaultBalanceB.sub(preVaultBalanceB).eq(expectedVaultAccountBDelta)); + assert.ok( + postOwnerAccountBalanceA.sub(preOwnerAccountBalanceA).eq(expectedOwnerAccountADelta), + ); + assert.ok( + postOwnerAccountBalanceB.sub(preOwnerAccountBalanceB).eq(expectedOwnerAccountBDelta), + ); + }); + + it("A --> B, ExactOut", async () => { + const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey; + const whirlpoolData = (await fetcher.getPool( + whirlpoolKey, + IGNORE_CACHE, + )) as WhirlpoolData; + + const aToB = true; + const outputAmount = new BN(2000000); + + // edge-case + if (transferFeeA!.transferFeeBasisPoints === MAX_FEE_BASIS_POINTS && transferFeeA!.maximumFee === BigInt(U64_MAX.toString())) { + // we cannot determine input size because all amount will be collected as transfer fee + const tickArrays = await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE, + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: outputAmount, + otherAmountThreshold: U64_MAX, + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + amountSpecifiedIsInput: false, + aToB, + tickArray0: tickArrays[0].address, + tickArray1: tickArrays[0].address, + tickArray2: tickArrays[0].address, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(), + /0x17a4/, // TransferFeeCalculationError + ); + + return; + } + + if (transferFeeB!.transferFeeBasisPoints === MAX_FEE_BASIS_POINTS && transferFeeB!.maximumFee === BigInt(U64_MAX.toString())) { + // we cannot determine output size including transfer fee because all amount will be collected as transfer fee + const tickArrays = await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE, + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: outputAmount, + otherAmountThreshold: U64_MAX, + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + amountSpecifiedIsInput: false, + aToB, + tickArray0: tickArrays[0].address, + tickArray1: tickArrays[0].address, + tickArray2: tickArrays[0].address, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(), + /0x17a4/, // TransferFeeCalculationError + ); + + return; + } + + const transferFeeIncludedOutputAmount = transferFeeB + ? calculateTransferFeeIncludedAmount(transferFeeB, outputAmount) + : { amount: outputAmount, fee: ZERO_BN }; + if (transferFeeB && transferFeeB.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedOutputAmount.fee.gtn(0)); + + const quoteAToB = swapQuoteWithParams( + { + // A --> B, ExactOut + amountSpecifiedIsInput: false, + aToB, + tokenAmount: transferFeeIncludedOutputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), // 0% slippage + ); + + const transferFeeIncludedInputAmount = transferFeeA + ? calculateTransferFeeIncludedAmount(transferFeeA, quoteAToB.estimatedAmountIn) + : { amount: quoteAToB.estimatedAmountIn, fee: ZERO_BN }; + if (transferFeeA && transferFeeA.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedInputAmount.fee.gtn(0)); + + const expectedOwnerAccountADelta = transferFeeIncludedInputAmount.amount.neg(); // out + const expectedOwnerAccountBDelta = outputAmount; // in + const expectedVaultAccountADelta = quoteAToB.estimatedAmountIn; // in + const expectedVaultAccountBDelta = transferFeeIncludedOutputAmount.amount.neg(); // out + assert.ok(expectedVaultAccountADelta.eq(quoteAToB.estimatedAmountIn)); + assert.ok(expectedVaultAccountBDelta.eq(quoteAToB.estimatedAmountOut.neg())); + + const preVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const preVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const preOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const preOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteAToB, + amount: outputAmount, // transfer fee excluded + otherAmountThreshold: transferFeeIncludedInputAmount.amount.subn(1), // transfer fee included + + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(), + /0x1795/, // AmountInAboveMaximum + ); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteAToB, + amount: outputAmount, // transfer fee excluded + otherAmountThreshold: transferFeeIncludedInputAmount.amount, // transfer fee included + + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(); + + const postVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const postVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const postOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const postOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + assert.ok(postVaultBalanceA.sub(preVaultBalanceA).eq(expectedVaultAccountADelta)); + assert.ok(postVaultBalanceB.sub(preVaultBalanceB).eq(expectedVaultAccountBDelta)); + assert.ok( + postOwnerAccountBalanceA.sub(preOwnerAccountBalanceA).eq(expectedOwnerAccountADelta), + ); + assert.ok( + postOwnerAccountBalanceB.sub(preOwnerAccountBalanceB).eq(expectedOwnerAccountBDelta), + ); + }); + + it("A <-- B, ExactOut", async () => { + const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey; + const whirlpoolData = (await fetcher.getPool( + whirlpoolKey, + IGNORE_CACHE, + )) as WhirlpoolData; + + const aToB = false; + const outputAmount = new BN(100000); + + // edge-case + if (transferFeeA!.transferFeeBasisPoints === MAX_FEE_BASIS_POINTS && transferFeeA!.maximumFee === BigInt(U64_MAX.toString())) { + // we cannot determine output size including transfer fee because all amount will be collected as transfer fee + const tickArrays = await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE, + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: outputAmount, + otherAmountThreshold: U64_MAX, + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + amountSpecifiedIsInput: false, + aToB, + tickArray0: tickArrays[0].address, + tickArray1: tickArrays[0].address, + tickArray2: tickArrays[0].address, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(), + /0x17a4/, // TransferFeeCalculationError + ); + + return; + } + + if (transferFeeB!.transferFeeBasisPoints === MAX_FEE_BASIS_POINTS && transferFeeB!.maximumFee === BigInt(U64_MAX.toString())) { + // we cannot determine input size because all amount will be collected as transfer fee + const tickArrays = await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE, + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: outputAmount, + otherAmountThreshold: U64_MAX, + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + amountSpecifiedIsInput: false, + aToB, + tickArray0: tickArrays[0].address, + tickArray1: tickArrays[0].address, + tickArray2: tickArrays[0].address, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(), + /0x17a4/, // TransferFeeCalculationError + ); + + return; + } + + const transferFeeIncludedOutputAmount = transferFeeA + ? calculateTransferFeeIncludedAmount(transferFeeA, outputAmount) + : { amount: outputAmount, fee: ZERO_BN }; + if (transferFeeA && transferFeeA.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedOutputAmount.fee.gtn(0)); + + const quoteBToA = swapQuoteWithParams( + { + // A <-- B, ExactOut + amountSpecifiedIsInput: false, + aToB, + tokenAmount: transferFeeIncludedOutputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), // 0% slippage + ); + + const transferFeeIncludedInputAmount = transferFeeB + ? calculateTransferFeeIncludedAmount(transferFeeB, quoteBToA.estimatedAmountIn) + : { amount: quoteBToA.estimatedAmountIn, fee: ZERO_BN }; + if (transferFeeB && transferFeeB.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedInputAmount.fee.gtn(0)); + + const expectedOwnerAccountADelta = outputAmount; // in + const expectedOwnerAccountBDelta = transferFeeIncludedInputAmount.amount.neg(); // out + const expectedVaultAccountADelta = transferFeeIncludedOutputAmount.amount.neg(); // out + const expectedVaultAccountBDelta = quoteBToA.estimatedAmountIn; // in + assert.ok(expectedVaultAccountADelta.eq(quoteBToA.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountBDelta.eq(quoteBToA.estimatedAmountIn)); + + const preVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const preVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const preOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const preOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteBToA, + amount: outputAmount, // transfer fee excluded + otherAmountThreshold: transferFeeIncludedInputAmount.amount.subn(1), // transfer fee included + + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(), + /0x1795/, // AmountInAboveMaximum + ); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteBToA, + amount: outputAmount, // transfer fee excluded + otherAmountThreshold: transferFeeIncludedInputAmount.amount, // transfer fee included + + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + }), + ).buildAndExecute(); + + const postVaultBalanceA = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey), + ); + const postVaultBalanceB = new BN( + await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey), + ); + const postOwnerAccountBalanceA = new BN(await getTokenBalance(provider, tokenAccountA)); + const postOwnerAccountBalanceB = new BN(await getTokenBalance(provider, tokenAccountB)); + + assert.ok(postVaultBalanceA.sub(preVaultBalanceA).eq(expectedVaultAccountADelta)); + assert.ok(postVaultBalanceB.sub(preVaultBalanceB).eq(expectedVaultAccountBDelta)); + assert.ok( + postOwnerAccountBalanceA.sub(preOwnerAccountBalanceA).eq(expectedOwnerAccountADelta), + ); + assert.ok( + postOwnerAccountBalanceB.sub(preOwnerAccountBalanceB).eq(expectedOwnerAccountBDelta), + ); + }); + }); + }); + }); + + describe("two_hop_swap", () => { + let aqConfig: InitAquariumV2Params; + let aquarium: TestAquarium; + let whirlpoolOneKey: PublicKey; + let whirlpoolTwoKey: PublicKey; + let whirlpoolDataOne: WhirlpoolData; + let whirlpoolDataTwo: WhirlpoolData; + + const variations: TokenTrait[][] = [ + // all token has transfer fee + [ + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 300 }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 500 }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 1000 }, + ], + // input token has transfer fee + [ + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 300 }, + { isToken2022: true, hasTransferFeeExtension: false }, + { isToken2022: true, hasTransferFeeExtension: false }, + ], + // input and mid token has transfer fee + [ + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 300 }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 500 }, + { isToken2022: true, hasTransferFeeExtension: false }, + ], + // output token has transfer fee + [ + { isToken2022: true, hasTransferFeeExtension: false }, + { isToken2022: true, hasTransferFeeExtension: false }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 1000 }, + ], + // output and mid token has transfer fee + [ + { isToken2022: true, hasTransferFeeExtension: false }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 500 }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 1000 }, + ], + // mid token has transfer fee + [ + { isToken2022: true, hasTransferFeeExtension: false }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 500 }, + { isToken2022: true, hasTransferFeeExtension: false }, + ], + // input and output token has transfer fee + [ + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 300 }, + { isToken2022: true, hasTransferFeeExtension: false }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 1000 }, + ], + // all token has transfer fee, but bps are zero + [ + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + ], + ]; + + variations.forEach(([token0, token1, token2]) => { + const label0 = `Token0: transfer fee bps = ${ + token0.hasTransferFeeExtension ? token0.transferFeeInitialBps?.toString() : "none" + }`; + const label1 = `Token1: transfer fee bps = ${ + token1.hasTransferFeeExtension ? token1.transferFeeInitialBps?.toString() : "none" + }`; + const label2 = `Token2: transfer fee bps = ${ + token2.hasTransferFeeExtension ? token2.transferFeeInitialBps?.toString() : "none" + }`; + + describe(`${label0}, ${label1}, ${label2}`, () => { + beforeEach(async () => { + aqConfig = getDefaultAquariumV2(); + // Add a third token and account and a second pool + aqConfig.initMintParams = [ + { tokenTrait: token0 }, + { tokenTrait: token1 }, + { tokenTrait: token2 }, + ]; + aqConfig.initTokenAccParams.push({ mintIndex: 2 }); + aqConfig.initPoolParams.push({ mintIndices: [1, 2], tickSpacing: TickSpacing.Standard }); + + // Add tick arrays and positions + const aToB = false; + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 0, + startTickIndex: 22528, + arrayCount: 3, + aToB, + }); + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 1, + startTickIndex: 22528, + arrayCount: 3, + aToB, + }); + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + aqConfig.initPositionParams.push({ poolIndex: 0, fundParams }); + aqConfig.initPositionParams.push({ poolIndex: 1, fundParams }); + + aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { pools } = aquarium; + + whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE, + )) as WhirlpoolData; + whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE, + )) as WhirlpoolData; + }); + + it("T0 --> T1 --> T2, ExactIn", async () => { + const [inputToken, midToken, outputToken] = aquarium.mintKeys; + const [inputTokenTrait, midTokenTrait, outputTokenTrait] = [token0, token1, token2]; + + const transferFeeInput = inputTokenTrait.hasTransferFeeExtension ? await getTransferFee(inputToken) : null; + const transferFeeMid = midTokenTrait.hasTransferFeeExtension ? await getTransferFee(midToken) : null; + const transferFeeOutput = outputTokenTrait.hasTransferFeeExtension ? await getTransferFee(outputToken) : null; + + const inputAmount = new BN(1000); + const transferFeeExcludedInputAmount = transferFeeInput + ? calculateTransferFeeExcludedAmount(transferFeeInput, inputAmount) + : { amount: inputAmount, fee: ZERO_BN }; + if (transferFeeInput && transferFeeInput.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedInputAmount.fee.gtn(0)); + + const aToBOne = whirlpoolDataOne.tokenMintA.equals(inputToken); + const quote = swapQuoteWithParams( + { + // T0 --> T1, ExactIn + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: transferFeeExcludedInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), + ); + + /* + // vault -> owner + const transferFeeExcludedMidOutputAmount = transferFeeMid + ? calculateTransferFeeExcludedAmount(transferFeeMid, quote.estimatedAmountOut) + : { amount: quote.estimatedAmountOut, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedMidOutputAmount.fee.gtn(0)); + + // owner -> vault + const transferFeeExcludedMidInputAmount = transferFeeMid + ? calculateTransferFeeExcludedAmount(transferFeeMid, transferFeeExcludedMidOutputAmount.amount) + : { amount: transferFeeExcludedMidOutputAmount.amount, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedMidInputAmount.fee.gtn(0)); + */ + + // vault to vault + const transferFeeExcludedMidInputAmount = transferFeeMid + ? calculateTransferFeeExcludedAmount(transferFeeMid, quote.estimatedAmountOut) + : { amount: quote.estimatedAmountOut, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedMidInputAmount.fee.gtn(0)); + + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(midToken); + const quote2 = swapQuoteWithParams( + { + // T1 --> T2, ExactIn + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: transferFeeExcludedMidInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), + ); + + const transferFeeExcludedOutputAmount = transferFeeOutput + ? calculateTransferFeeExcludedAmount(transferFeeOutput, quote2.estimatedAmountOut) + : { amount: quote2.estimatedAmountOut, fee: ZERO_BN }; + if (transferFeeOutput && transferFeeOutput.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedOutputAmount.fee.gtn(0)); + + const expectedOwnerAccountInputDelta = inputAmount.neg(); // out + const expectedOwnerAccountMidDelta = ZERO_BN; // in = out + const expectedOwnerAccountOutputDelta = transferFeeExcludedOutputAmount.amount; // in + const [expectedVaultAccountOneADelta, expectedVaultAccountOneBDelta] = aToBOne + ? [transferFeeExcludedInputAmount.amount, quote.estimatedAmountOut.neg()] + : [quote.estimatedAmountOut.neg(), transferFeeExcludedInputAmount.amount]; + const [expectedVaultAccountTwoADelta, expectedVaultAccountTwoBDelta] = aToBTwo + ? [transferFeeExcludedMidInputAmount.amount, quote2.estimatedAmountOut.neg()] + : [quote2.estimatedAmountOut.neg(), transferFeeExcludedMidInputAmount.amount]; + assert.ok(expectedVaultAccountOneADelta.eq(aToBOne ? quote.estimatedAmountIn : quote.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountOneBDelta.eq(aToBOne ? quote.estimatedAmountOut.neg() : quote.estimatedAmountIn)); + assert.ok(expectedVaultAccountTwoADelta.eq(aToBTwo ? quote2.estimatedAmountIn : quote2.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountTwoBDelta.eq(aToBTwo ? quote2.estimatedAmountOut.neg() : quote2.estimatedAmountIn)); + + const pools = aquarium.pools; + const tokenAccKeys = getTokenAccsForPoolsV2(pools, aquarium.tokenAccounts); + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + const baseIxParams: TwoHopSwapV2Params = { + ...twoHopQuote, + amount: inputAmount, // transfer fee included + otherAmountThreshold: transferFeeExcludedOutputAmount.amount, // transfer fee excluded + + tokenAuthority: ctx.wallet.publicKey, + whirlpoolOne: pools[0].whirlpoolPda.publicKey, + whirlpoolTwo: pools[1].whirlpoolPda.publicKey, + tokenMintInput: twoHopQuote.aToBOne ? pools[0].tokenMintA : pools[0].tokenMintB, + tokenMintIntermediate: twoHopQuote.aToBOne ? pools[0].tokenMintB : pools[0].tokenMintA, + tokenMintOutput: twoHopQuote.aToBTwo ? pools[1].tokenMintB : pools[1].tokenMintA, + tokenProgramInput: twoHopQuote.aToBOne ? pools[0].tokenProgramA : pools[0].tokenProgramB, + tokenProgramIntermediate: twoHopQuote.aToBOne ? pools[0].tokenProgramB : pools[0].tokenProgramA, + tokenProgramOutput: twoHopQuote.aToBTwo ? pools[1].tokenProgramB : pools[1].tokenProgramA, + tokenOwnerAccountInput: twoHopQuote.aToBOne ? tokenAccKeys[0] : tokenAccKeys[1], + tokenOwnerAccountOutput: twoHopQuote.aToBTwo ? tokenAccKeys[3] : tokenAccKeys[2], + tokenVaultOneInput: twoHopQuote.aToBOne ? pools[0].tokenVaultAKeypair.publicKey : pools[0].tokenVaultBKeypair.publicKey, + tokenVaultOneIntermediate: twoHopQuote.aToBOne ? pools[0].tokenVaultBKeypair.publicKey : pools[0].tokenVaultAKeypair.publicKey, + tokenVaultTwoIntermediate: twoHopQuote.aToBTwo ? pools[1].tokenVaultAKeypair.publicKey : pools[1].tokenVaultBKeypair.publicKey, + tokenVaultTwoOutput: twoHopQuote.aToBTwo ? pools[1].tokenVaultBKeypair.publicKey : pools[1].tokenVaultAKeypair.publicKey, + oracleOne: PDAUtil.getOracle(ctx.program.programId, pools[0].whirlpoolPda.publicKey) + .publicKey, + oracleTwo: PDAUtil.getOracle(ctx.program.programId, pools[1].whirlpoolPda.publicKey) + .publicKey, + }; + + const preVaultBalanceOneA = new BN(await getTokenBalance(provider, pools[0].tokenVaultAKeypair.publicKey)); + const preVaultBalanceOneB = new BN(await getTokenBalance(provider, pools[0].tokenVaultBKeypair.publicKey)); + const preVaultBalanceTwoA = new BN(await getTokenBalance(provider, pools[1].tokenVaultAKeypair.publicKey)); + const preVaultBalanceTwoB = new BN(await getTokenBalance(provider, pools[1].tokenVaultBKeypair.publicKey)); + const preOwnerAccountBalanceInput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountInput)); + const preOwnerAccountBalanceOutput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountOutput)); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...baseIxParams, + otherAmountThreshold: baseIxParams.otherAmountThreshold.addn(1), + }) + ).prependInstruction(useMaxCU()).buildAndExecute(), // add CU + /0x1794/, // AmountOutBelowMinimum + ); + + await toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, baseIxParams) + ).prependInstruction(useMaxCU()).buildAndExecute(); // add CU + + const postVaultBalanceOneA = new BN(await getTokenBalance(provider, pools[0].tokenVaultAKeypair.publicKey)); + const postVaultBalanceOneB = new BN(await getTokenBalance(provider, pools[0].tokenVaultBKeypair.publicKey)); + const postVaultBalanceTwoA = new BN(await getTokenBalance(provider, pools[1].tokenVaultAKeypair.publicKey)); + const postVaultBalanceTwoB = new BN(await getTokenBalance(provider, pools[1].tokenVaultBKeypair.publicKey)); + const postOwnerAccountBalanceInput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountInput)); + const postOwnerAccountBalanceOutput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountOutput)); + + assert.ok(postVaultBalanceOneA.sub(preVaultBalanceOneA).eq(expectedVaultAccountOneADelta)); + assert.ok(postVaultBalanceOneB.sub(preVaultBalanceOneB).eq(expectedVaultAccountOneBDelta)); + assert.ok(postVaultBalanceTwoA.sub(preVaultBalanceTwoA).eq(expectedVaultAccountTwoADelta)); + assert.ok(postVaultBalanceTwoB.sub(preVaultBalanceTwoB).eq(expectedVaultAccountTwoBDelta)); + assert.ok(postOwnerAccountBalanceInput.sub(preOwnerAccountBalanceInput).eq(expectedOwnerAccountInputDelta)); + assert.ok(postOwnerAccountBalanceOutput.sub(preOwnerAccountBalanceOutput).eq(expectedOwnerAccountOutputDelta)); + + //console.log(`aToB: ${aToBOne} ${aToBTwo}`); + //console.log("in", transferFeeExcludedInputAmount.amount.toString(), transferFeeExcludedInputAmount.fee.toString()); + //console.log("midout", transferFeeExcludedMidOutputAmount.amount.toString(), transferFeeExcludedMidOutputAmount.fee.toString()); + //console.log("midin", transferFeeExcludedMidInputAmount.amount.toString(), transferFeeExcludedMidInputAmount.fee.toString()); + //console.log("out", transferFeeExcludedOutputAmount.amount.toString(), transferFeeExcludedOutputAmount.fee.toString()); + //console.log("q1", quote.estimatedAmountIn.toString(), quote.estimatedAmountOut.toString()); + //console.log("q2", quote2.estimatedAmountIn.toString(), quote2.estimatedAmountOut.toString()); + }); + + it("T0 <-- T1 <-- T2, ExactIn", async () => { + const [outputToken, midToken, inputToken] = aquarium.mintKeys; + const [outputTokenTrait, midTokenTrait, inputTokenTrait] = [token0, token1, token2]; + + const transferFeeInput = inputTokenTrait.hasTransferFeeExtension ? await getTransferFee(inputToken) : null; + const transferFeeMid = midTokenTrait.hasTransferFeeExtension ? await getTransferFee(midToken) : null; + const transferFeeOutput = outputTokenTrait.hasTransferFeeExtension ? await getTransferFee(outputToken) : null; + + const inputAmount = new BN(100000); + const transferFeeExcludedInputAmount = transferFeeInput + ? calculateTransferFeeExcludedAmount(transferFeeInput, inputAmount) + : { amount: inputAmount, fee: ZERO_BN }; + if (transferFeeInput && transferFeeInput.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedInputAmount.fee.gtn(0)); + + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(inputToken); + const quote = swapQuoteWithParams( + { + // T1 <-- T2, ExactIn + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: transferFeeExcludedInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), + ); + + /* + // vault -> owner + const transferFeeExcludedMidOutputAmount = transferFeeMid + ? calculateTransferFeeExcludedAmount(transferFeeMid, quote.estimatedAmountOut) + : { amount: quote.estimatedAmountOut, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedMidOutputAmount.fee.gtn(0)); + + // owner -> vault + const transferFeeExcludedMidInputAmount = transferFeeMid + ? calculateTransferFeeExcludedAmount(transferFeeMid, transferFeeExcludedMidOutputAmount.amount) + : { amount: transferFeeExcludedMidOutputAmount.amount, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedMidInputAmount.fee.gtn(0)); + */ + + // vault to vault + const transferFeeExcludedMidInputAmount = transferFeeMid + ? calculateTransferFeeExcludedAmount(transferFeeMid, quote.estimatedAmountOut) + : { amount: quote.estimatedAmountOut, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedMidInputAmount.fee.gtn(0)); + + const aToBOne = whirlpoolDataOne.tokenMintA.equals(midToken); + const quote2 = swapQuoteWithParams( + { + // T0 <-- T1, ExactIn + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: transferFeeExcludedMidInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), + ); + + const transferFeeExcludedOutputAmount = transferFeeOutput + ? calculateTransferFeeExcludedAmount(transferFeeOutput, quote2.estimatedAmountOut) + : { amount: quote2.estimatedAmountOut, fee: ZERO_BN }; + if (transferFeeOutput && transferFeeOutput.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedOutputAmount.fee.gtn(0)); + + const expectedOwnerAccountInputDelta = inputAmount.neg(); // out + const expectedOwnerAccountMidDelta = ZERO_BN; // in = out + const expectedOwnerAccountOutputDelta = transferFeeExcludedOutputAmount.amount; // in + const [expectedVaultAccountOneADelta, expectedVaultAccountOneBDelta] = aToBOne + ? [transferFeeExcludedInputAmount.amount, quote.estimatedAmountOut.neg()] + : [quote.estimatedAmountOut.neg(), transferFeeExcludedInputAmount.amount]; + const [expectedVaultAccountTwoADelta, expectedVaultAccountTwoBDelta] = aToBTwo + ? [transferFeeExcludedMidInputAmount.amount, quote2.estimatedAmountOut.neg()] + : [quote2.estimatedAmountOut.neg(), transferFeeExcludedMidInputAmount.amount]; + assert.ok(expectedVaultAccountOneADelta.eq(aToBOne ? quote.estimatedAmountIn : quote.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountOneBDelta.eq(aToBOne ? quote.estimatedAmountOut.neg() : quote.estimatedAmountIn)); + assert.ok(expectedVaultAccountTwoADelta.eq(aToBTwo ? quote2.estimatedAmountIn : quote2.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountTwoBDelta.eq(aToBTwo ? quote2.estimatedAmountOut.neg() : quote2.estimatedAmountIn)); + + const pools = aquarium.pools; + const tokenAccKeys = getTokenAccsForPoolsV2(pools, aquarium.tokenAccounts); + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + const baseIxParams: TwoHopSwapV2Params = { + ...twoHopQuote, + amount: inputAmount, // transfer fee included + otherAmountThreshold: transferFeeExcludedOutputAmount.amount, // transfer fee excluded + + tokenAuthority: ctx.wallet.publicKey, + whirlpoolOne: pools[1].whirlpoolPda.publicKey, + whirlpoolTwo: pools[0].whirlpoolPda.publicKey, + tokenMintInput: aToBTwo ? pools[1].tokenMintA : pools[1].tokenMintB, + tokenMintIntermediate: aToBTwo ? pools[1].tokenMintB : pools[1].tokenMintA, + tokenMintOutput: aToBOne ? pools[0].tokenMintB : pools[0].tokenMintA, + tokenProgramInput: aToBTwo ? pools[1].tokenProgramA : pools[1].tokenProgramB, + tokenProgramIntermediate: aToBTwo ? pools[1].tokenProgramB : pools[1].tokenProgramA, + tokenProgramOutput: aToBOne ? pools[0].tokenProgramB : pools[0].tokenProgramA, + tokenOwnerAccountInput: aToBTwo ? tokenAccKeys[2] : tokenAccKeys[3], + tokenOwnerAccountOutput: aToBOne ? tokenAccKeys[1] : tokenAccKeys[0], + tokenVaultOneInput: aToBTwo ? pools[1].tokenVaultAKeypair.publicKey : pools[1].tokenVaultBKeypair.publicKey, + tokenVaultOneIntermediate: aToBTwo ? pools[1].tokenVaultBKeypair.publicKey : pools[1].tokenVaultAKeypair.publicKey, + tokenVaultTwoIntermediate: aToBOne ? pools[0].tokenVaultAKeypair.publicKey : pools[0].tokenVaultBKeypair.publicKey, + tokenVaultTwoOutput: aToBOne ? pools[0].tokenVaultBKeypair.publicKey : pools[0].tokenVaultAKeypair.publicKey, + oracleOne: PDAUtil.getOracle(ctx.program.programId, pools[1].whirlpoolPda.publicKey) + .publicKey, + oracleTwo: PDAUtil.getOracle(ctx.program.programId, pools[0].whirlpoolPda.publicKey) + .publicKey, + }; + + const preVaultBalanceOneA = new BN(await getTokenBalance(provider, pools[1].tokenVaultAKeypair.publicKey)); + const preVaultBalanceOneB = new BN(await getTokenBalance(provider, pools[1].tokenVaultBKeypair.publicKey)); + const preVaultBalanceTwoA = new BN(await getTokenBalance(provider, pools[0].tokenVaultAKeypair.publicKey)); + const preVaultBalanceTwoB = new BN(await getTokenBalance(provider, pools[0].tokenVaultBKeypair.publicKey)); + const preOwnerAccountBalanceInput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountInput)); + const preOwnerAccountBalanceOutput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountOutput)); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...baseIxParams, + otherAmountThreshold: baseIxParams.otherAmountThreshold.addn(1), + }) + ).prependInstruction(useMaxCU()).buildAndExecute(), // add CU + /0x1794/, // AmountOutBelowMinimum + ); + + await toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, baseIxParams) + ).prependInstruction(useMaxCU()).buildAndExecute(); // add CU + + const postVaultBalanceOneA = new BN(await getTokenBalance(provider, pools[1].tokenVaultAKeypair.publicKey)); + const postVaultBalanceOneB = new BN(await getTokenBalance(provider, pools[1].tokenVaultBKeypair.publicKey)); + const postVaultBalanceTwoA = new BN(await getTokenBalance(provider, pools[0].tokenVaultAKeypair.publicKey)); + const postVaultBalanceTwoB = new BN(await getTokenBalance(provider, pools[0].tokenVaultBKeypair.publicKey)); + const postOwnerAccountBalanceInput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountInput)); + const postOwnerAccountBalanceOutput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountOutput)); + + assert.ok(postVaultBalanceOneA.sub(preVaultBalanceOneA).eq(expectedVaultAccountOneADelta)); + assert.ok(postVaultBalanceOneB.sub(preVaultBalanceOneB).eq(expectedVaultAccountOneBDelta)); + assert.ok(postVaultBalanceTwoA.sub(preVaultBalanceTwoA).eq(expectedVaultAccountTwoADelta)); + assert.ok(postVaultBalanceTwoB.sub(preVaultBalanceTwoB).eq(expectedVaultAccountTwoBDelta)); + assert.ok(postOwnerAccountBalanceInput.sub(preOwnerAccountBalanceInput).eq(expectedOwnerAccountInputDelta)); + assert.ok(postOwnerAccountBalanceOutput.sub(preOwnerAccountBalanceOutput).eq(expectedOwnerAccountOutputDelta)); + + //console.log(`aToB: ${aToBTwo} ${aToBOne}`); + //console.log("in", transferFeeExcludedInputAmount.amount.toString(), transferFeeExcludedInputAmount.fee.toString()); + //console.log("midout", transferFeeExcludedMidOutputAmount.amount.toString(), transferFeeExcludedMidOutputAmount.fee.toString()); + //console.log("midin", transferFeeExcludedMidInputAmount.amount.toString(), transferFeeExcludedMidInputAmount.fee.toString()); + //console.log("out", transferFeeExcludedOutputAmount.amount.toString(), transferFeeExcludedOutputAmount.fee.toString()); + //console.log("q1", quote.estimatedAmountIn.toString(), quote.estimatedAmountOut.toString()); + //console.log("q2", quote2.estimatedAmountIn.toString(), quote2.estimatedAmountOut.toString()); + }) + + it("T0 --> T1 --> T2, ExactOut", async () => { + const [inputToken, midToken, outputToken] = aquarium.mintKeys; + const [inputTokenTrait, midTokenTrait, outputTokenTrait] = [token0, token1, token2]; + + const transferFeeInput = inputTokenTrait.hasTransferFeeExtension ? await getTransferFee(inputToken) : null; + const transferFeeMid = midTokenTrait.hasTransferFeeExtension ? await getTransferFee(midToken) : null; + const transferFeeOutput = outputTokenTrait.hasTransferFeeExtension ? await getTransferFee(outputToken) : null; + + const outputAmount = new BN(500000); + const transferFeeIncludedOutputAmount = transferFeeOutput + ? calculateTransferFeeIncludedAmount(transferFeeOutput, outputAmount) + : { amount: outputAmount, fee: ZERO_BN }; + if (transferFeeOutput && transferFeeOutput.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedOutputAmount.fee.gtn(0)); + + const aToBTwo = whirlpoolDataTwo.tokenMintB.equals(outputToken); + const quote2 = swapQuoteWithParams( + { + // T1 --> T2, ExactOut + amountSpecifiedIsInput: false, + aToB: aToBTwo, + tokenAmount: transferFeeIncludedOutputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), + ); + + /* + // owner -> vault + const transferFeeIncludedMidInputAmount = transferFeeMid + ? calculateTransferFeeIncludedAmount(transferFeeMid, quote2.estimatedAmountIn) + : { amount: quote2.estimatedAmountIn, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedMidInputAmount.fee.gtn(0)); + + // vault -> owner + const transferFeeIncludedMidOutputAmount = transferFeeMid + ? calculateTransferFeeIncludedAmount(transferFeeMid, transferFeeIncludedMidInputAmount.amount) + : { amount: transferFeeIncludedMidInputAmount.amount, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedMidOutputAmount.fee.gtn(0)); + */ + + // vault to vault + const transferFeeIncludedMidOutputAmount = transferFeeMid + ? calculateTransferFeeIncludedAmount(transferFeeMid, quote2.estimatedAmountIn) + : { amount: quote2.estimatedAmountIn, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedMidOutputAmount.fee.gtn(0)); + + const aToBOne = whirlpoolDataOne.tokenMintB.equals(midToken); + const quote = swapQuoteWithParams( + { + // T0 --> T1, ExactOut + amountSpecifiedIsInput: false, + aToB: aToBOne, + tokenAmount: transferFeeIncludedMidOutputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), + ); + + const transferFeeIncludedInputAmount = transferFeeInput + ? calculateTransferFeeIncludedAmount(transferFeeInput, quote.estimatedAmountIn) + : { amount: quote.estimatedAmountIn, fee: ZERO_BN }; + if (transferFeeInput && transferFeeInput.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedInputAmount.fee.gtn(0)); + + const expectedOwnerAccountInputDelta = transferFeeIncludedInputAmount.amount.neg(); // out + const expectedOwnerAccountMidDelta = ZERO_BN; // in = out + const expectedOwnerAccountOutputDelta = outputAmount; // in + const [expectedVaultAccountOneADelta, expectedVaultAccountOneBDelta] = aToBOne + ? [quote.estimatedAmountIn, transferFeeIncludedMidOutputAmount.amount.neg()] + : [transferFeeIncludedMidOutputAmount.amount.neg(), quote.estimatedAmountIn]; + const [expectedVaultAccountTwoADelta, expectedVaultAccountTwoBDelta] = aToBTwo + ? [quote2.estimatedAmountIn, transferFeeIncludedOutputAmount.amount.neg()] + : [transferFeeIncludedOutputAmount.amount.neg(), quote2.estimatedAmountIn]; + assert.ok(expectedVaultAccountOneADelta.eq(aToBOne ? quote.estimatedAmountIn : quote.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountOneBDelta.eq(aToBOne ? quote.estimatedAmountOut.neg() : quote.estimatedAmountIn)); + assert.ok(expectedVaultAccountTwoADelta.eq(aToBTwo ? quote2.estimatedAmountIn : quote2.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountTwoBDelta.eq(aToBTwo ? quote2.estimatedAmountOut.neg() : quote2.estimatedAmountIn)); + + const pools = aquarium.pools; + const tokenAccKeys = getTokenAccsForPoolsV2(pools, aquarium.tokenAccounts); + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + const baseIxParams: TwoHopSwapV2Params = { + ...twoHopQuote, + amount: outputAmount, // transfer fee excluded + otherAmountThreshold: transferFeeIncludedInputAmount.amount, // transfer fee included + + tokenAuthority: ctx.wallet.publicKey, + whirlpoolOne: pools[0].whirlpoolPda.publicKey, + whirlpoolTwo: pools[1].whirlpoolPda.publicKey, + tokenMintInput: twoHopQuote.aToBOne ? pools[0].tokenMintA : pools[0].tokenMintB, + tokenMintIntermediate: twoHopQuote.aToBOne ? pools[0].tokenMintB : pools[0].tokenMintA, + tokenMintOutput: twoHopQuote.aToBTwo ? pools[1].tokenMintB : pools[1].tokenMintA, + tokenProgramInput: twoHopQuote.aToBOne ? pools[0].tokenProgramA : pools[0].tokenProgramB, + tokenProgramIntermediate: twoHopQuote.aToBOne ? pools[0].tokenProgramB : pools[0].tokenProgramA, + tokenProgramOutput: twoHopQuote.aToBTwo ? pools[1].tokenProgramB : pools[1].tokenProgramA, + tokenOwnerAccountInput: twoHopQuote.aToBOne ? tokenAccKeys[0] : tokenAccKeys[1], + tokenOwnerAccountOutput: twoHopQuote.aToBTwo ? tokenAccKeys[3] : tokenAccKeys[2], + tokenVaultOneInput: twoHopQuote.aToBOne ? pools[0].tokenVaultAKeypair.publicKey : pools[0].tokenVaultBKeypair.publicKey, + tokenVaultOneIntermediate: twoHopQuote.aToBOne ? pools[0].tokenVaultBKeypair.publicKey : pools[0].tokenVaultAKeypair.publicKey, + tokenVaultTwoIntermediate: twoHopQuote.aToBTwo ? pools[1].tokenVaultAKeypair.publicKey : pools[1].tokenVaultBKeypair.publicKey, + tokenVaultTwoOutput: twoHopQuote.aToBTwo ? pools[1].tokenVaultBKeypair.publicKey : pools[1].tokenVaultAKeypair.publicKey, + oracleOne: PDAUtil.getOracle(ctx.program.programId, pools[0].whirlpoolPda.publicKey) + .publicKey, + oracleTwo: PDAUtil.getOracle(ctx.program.programId, pools[1].whirlpoolPda.publicKey) + .publicKey, + }; + + const preVaultBalanceOneA = new BN(await getTokenBalance(provider, pools[0].tokenVaultAKeypair.publicKey)); + const preVaultBalanceOneB = new BN(await getTokenBalance(provider, pools[0].tokenVaultBKeypair.publicKey)); + const preVaultBalanceTwoA = new BN(await getTokenBalance(provider, pools[1].tokenVaultAKeypair.publicKey)); + const preVaultBalanceTwoB = new BN(await getTokenBalance(provider, pools[1].tokenVaultBKeypair.publicKey)); + const preOwnerAccountBalanceInput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountInput)); + const preOwnerAccountBalanceOutput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountOutput)); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...baseIxParams, + otherAmountThreshold: baseIxParams.otherAmountThreshold.subn(1), + }) + ).prependInstruction(useMaxCU()).buildAndExecute(), // add CU + /0x1795/, // AmountInAboveMaximum + ); + + await toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, baseIxParams) + ).prependInstruction(useMaxCU()).buildAndExecute(); // add CU + + const postVaultBalanceOneA = new BN(await getTokenBalance(provider, pools[0].tokenVaultAKeypair.publicKey)); + const postVaultBalanceOneB = new BN(await getTokenBalance(provider, pools[0].tokenVaultBKeypair.publicKey)); + const postVaultBalanceTwoA = new BN(await getTokenBalance(provider, pools[1].tokenVaultAKeypair.publicKey)); + const postVaultBalanceTwoB = new BN(await getTokenBalance(provider, pools[1].tokenVaultBKeypair.publicKey)); + const postOwnerAccountBalanceInput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountInput)); + const postOwnerAccountBalanceOutput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountOutput)); + + assert.ok(postVaultBalanceOneA.sub(preVaultBalanceOneA).eq(expectedVaultAccountOneADelta)); + assert.ok(postVaultBalanceOneB.sub(preVaultBalanceOneB).eq(expectedVaultAccountOneBDelta)); + assert.ok(postVaultBalanceTwoA.sub(preVaultBalanceTwoA).eq(expectedVaultAccountTwoADelta)); + assert.ok(postVaultBalanceTwoB.sub(preVaultBalanceTwoB).eq(expectedVaultAccountTwoBDelta)); + assert.ok(postOwnerAccountBalanceInput.sub(preOwnerAccountBalanceInput).eq(expectedOwnerAccountInputDelta)); + assert.ok(postOwnerAccountBalanceOutput.sub(preOwnerAccountBalanceOutput).eq(expectedOwnerAccountOutputDelta)); + + //console.log(`aToB: ${aToBOne} ${aToBTwo}`); + //console.log("out", transferFeeIncludedOutputAmount.amount.toString(), transferFeeIncludedOutputAmount.fee.toString()); + //console.log("midin", transferFeeIncludedMidInputAmount.amount.toString(), transferFeeIncludedMidInputAmount.fee.toString()); + //console.log("midout", transferFeeIncludedMidOutputAmount.amount.toString(), transferFeeIncludedMidOutputAmount.fee.toString()); + //console.log("in", transferFeeIncludedInputAmount.amount.toString(), transferFeeIncludedInputAmount.fee.toString()); + //console.log("q2", quote2.estimatedAmountIn.toString(), quote2.estimatedAmountOut.toString()); + //console.log("q1", quote.estimatedAmountIn.toString(), quote.estimatedAmountOut.toString()); + }); + + it("T0 <-- T1 <-- T2, ExactOut", async () => { + const [outputToken, midToken, inputToken] = aquarium.mintKeys; + const [outputTokenTrait, midTokenTrait, inputTokenTrait] = [token0, token1, token2]; + + const transferFeeInput = inputTokenTrait.hasTransferFeeExtension ? await getTransferFee(inputToken) : null; + const transferFeeMid = midTokenTrait.hasTransferFeeExtension ? await getTransferFee(midToken) : null; + const transferFeeOutput = outputTokenTrait.hasTransferFeeExtension ? await getTransferFee(outputToken) : null; + + const outputAmount = new BN(1000); + const transferFeeIncludedOutputAmount = transferFeeOutput + ? calculateTransferFeeIncludedAmount(transferFeeOutput, outputAmount) + : { amount: outputAmount, fee: ZERO_BN }; + if (transferFeeOutput && transferFeeOutput.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedOutputAmount.fee.gtn(0)); + + const aToBTwo = whirlpoolDataOne.tokenMintB.equals(outputToken); + const quote2 = swapQuoteWithParams( + { + // T0 <-- T1, ExactOut + amountSpecifiedIsInput: false, + aToB: aToBTwo, + tokenAmount: transferFeeIncludedOutputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), + ); + + /* + // owner -> vault + const transferFeeIncludedMidInputAmount = transferFeeMid + ? calculateTransferFeeIncludedAmount(transferFeeMid, quote2.estimatedAmountIn) + : { amount: quote2.estimatedAmountIn, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedMidInputAmount.fee.gtn(0)); + + // vault -> owner + const transferFeeIncludedMidOutputAmount = transferFeeMid + ? calculateTransferFeeIncludedAmount(transferFeeMid, transferFeeIncludedMidInputAmount.amount) + : { amount: transferFeeIncludedMidInputAmount.amount, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedMidOutputAmount.fee.gtn(0)); + */ + + // vault to vault + const transferFeeIncludedMidOutputAmount = transferFeeMid + ? calculateTransferFeeIncludedAmount(transferFeeMid, quote2.estimatedAmountIn) + : { amount: quote2.estimatedAmountIn, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedMidOutputAmount.fee.gtn(0)); + + const aToBOne = whirlpoolDataTwo.tokenMintB.equals(midToken); + const quote = swapQuoteWithParams( + { + // T1 <-- T2, ExactOut + amountSpecifiedIsInput: false, + aToB: aToBOne, + tokenAmount: transferFeeIncludedMidOutputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), + ); + + const transferFeeIncludedInputAmount = transferFeeInput + ? calculateTransferFeeIncludedAmount(transferFeeInput, quote.estimatedAmountIn) + : { amount: quote.estimatedAmountIn, fee: ZERO_BN }; + if (transferFeeInput && transferFeeInput.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedInputAmount.fee.gtn(0)); + + const expectedOwnerAccountInputDelta = transferFeeIncludedInputAmount.amount.neg(); // out + const expectedOwnerAccountMidDelta = ZERO_BN; // in = out + const expectedOwnerAccountOutputDelta = outputAmount; // in + const [expectedVaultAccountTwoADelta, expectedVaultAccountTwoBDelta] = aToBTwo + ? [quote2.estimatedAmountIn, transferFeeIncludedOutputAmount.amount.neg()] + : [transferFeeIncludedOutputAmount.amount.neg(), quote2.estimatedAmountIn]; + const [expectedVaultAccountOneADelta, expectedVaultAccountOneBDelta] = aToBOne + ? [quote.estimatedAmountIn, transferFeeIncludedMidOutputAmount.amount.neg()] + : [transferFeeIncludedMidOutputAmount.amount.neg(), quote.estimatedAmountIn]; + assert.ok(expectedVaultAccountTwoADelta.eq(aToBTwo ? quote2.estimatedAmountIn : quote2.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountTwoBDelta.eq(aToBTwo ? quote2.estimatedAmountOut.neg() : quote2.estimatedAmountIn)); + assert.ok(expectedVaultAccountOneADelta.eq(aToBOne ? quote.estimatedAmountIn : quote.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountOneBDelta.eq(aToBOne ? quote.estimatedAmountOut.neg() : quote.estimatedAmountIn)); + + const pools = aquarium.pools; + const tokenAccKeys = getTokenAccsForPoolsV2(pools, aquarium.tokenAccounts); + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + const baseIxParams: TwoHopSwapV2Params = { + ...twoHopQuote, + amount: outputAmount, // transfer fee excluded + otherAmountThreshold: transferFeeIncludedInputAmount.amount, // transfer fee included + + tokenAuthority: ctx.wallet.publicKey, + whirlpoolOne: pools[1].whirlpoolPda.publicKey, + whirlpoolTwo: pools[0].whirlpoolPda.publicKey, + tokenMintInput: aToBTwo ? pools[1].tokenMintA : pools[1].tokenMintB, + tokenMintIntermediate: aToBTwo ? pools[1].tokenMintB : pools[1].tokenMintA, + tokenMintOutput: aToBOne ? pools[0].tokenMintB : pools[0].tokenMintA, + tokenProgramInput: aToBTwo ? pools[1].tokenProgramA : pools[1].tokenProgramB, + tokenProgramIntermediate: aToBTwo ? pools[1].tokenProgramB : pools[1].tokenProgramA, + tokenProgramOutput: aToBOne ? pools[0].tokenProgramB : pools[0].tokenProgramA, + tokenOwnerAccountInput: aToBTwo ? tokenAccKeys[2] : tokenAccKeys[3], + tokenOwnerAccountOutput: aToBOne ? tokenAccKeys[1] : tokenAccKeys[0], + tokenVaultOneInput: aToBTwo ? pools[1].tokenVaultAKeypair.publicKey : pools[1].tokenVaultBKeypair.publicKey, + tokenVaultOneIntermediate: aToBTwo ? pools[1].tokenVaultBKeypair.publicKey : pools[1].tokenVaultAKeypair.publicKey, + tokenVaultTwoIntermediate: aToBOne ? pools[0].tokenVaultAKeypair.publicKey : pools[0].tokenVaultBKeypair.publicKey, + tokenVaultTwoOutput: aToBOne ? pools[0].tokenVaultBKeypair.publicKey : pools[0].tokenVaultAKeypair.publicKey, + oracleOne: PDAUtil.getOracle(ctx.program.programId, pools[1].whirlpoolPda.publicKey) + .publicKey, + oracleTwo: PDAUtil.getOracle(ctx.program.programId, pools[0].whirlpoolPda.publicKey) + .publicKey, + }; + + const preVaultBalanceOneA = new BN(await getTokenBalance(provider, pools[1].tokenVaultAKeypair.publicKey)); + const preVaultBalanceOneB = new BN(await getTokenBalance(provider, pools[1].tokenVaultBKeypair.publicKey)); + const preVaultBalanceTwoA = new BN(await getTokenBalance(provider, pools[0].tokenVaultAKeypair.publicKey)); + const preVaultBalanceTwoB = new BN(await getTokenBalance(provider, pools[0].tokenVaultBKeypair.publicKey)); + const preOwnerAccountBalanceInput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountInput)); + const preOwnerAccountBalanceOutput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountOutput)); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...baseIxParams, + otherAmountThreshold: baseIxParams.otherAmountThreshold.subn(1), + }) + ).prependInstruction(useMaxCU()).buildAndExecute(), // add CU + /0x1795/, // AmountInAboveMaximum + ); + + await toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, baseIxParams) + ).prependInstruction(useMaxCU()).buildAndExecute(); // add CU + + const postVaultBalanceOneA = new BN(await getTokenBalance(provider, pools[1].tokenVaultAKeypair.publicKey)); + const postVaultBalanceOneB = new BN(await getTokenBalance(provider, pools[1].tokenVaultBKeypair.publicKey)); + const postVaultBalanceTwoA = new BN(await getTokenBalance(provider, pools[0].tokenVaultAKeypair.publicKey)); + const postVaultBalanceTwoB = new BN(await getTokenBalance(provider, pools[0].tokenVaultBKeypair.publicKey)); + const postOwnerAccountBalanceInput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountInput)); + const postOwnerAccountBalanceOutput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountOutput)); + + assert.ok(postVaultBalanceOneA.sub(preVaultBalanceOneA).eq(expectedVaultAccountOneADelta)); + assert.ok(postVaultBalanceOneB.sub(preVaultBalanceOneB).eq(expectedVaultAccountOneBDelta)); + assert.ok(postVaultBalanceTwoA.sub(preVaultBalanceTwoA).eq(expectedVaultAccountTwoADelta)); + assert.ok(postVaultBalanceTwoB.sub(preVaultBalanceTwoB).eq(expectedVaultAccountTwoBDelta)); + assert.ok(postOwnerAccountBalanceInput.sub(preOwnerAccountBalanceInput).eq(expectedOwnerAccountInputDelta)); + assert.ok(postOwnerAccountBalanceOutput.sub(preOwnerAccountBalanceOutput).eq(expectedOwnerAccountOutputDelta)); + + //console.log(`aToB: ${aToBTwo} ${aToBOne}`); + //console.log("out", transferFeeIncludedOutputAmount.amount.toString(), transferFeeIncludedOutputAmount.fee.toString()); + //console.log("midin", transferFeeIncludedMidInputAmount.amount.toString(), transferFeeIncludedMidInputAmount.fee.toString()); + //console.log("midout", transferFeeIncludedMidOutputAmount.amount.toString(), transferFeeIncludedMidOutputAmount.fee.toString()); + //console.log("in", transferFeeIncludedInputAmount.amount.toString(), transferFeeIncludedInputAmount.fee.toString()); + //console.log("q2", quote2.estimatedAmountIn.toString(), quote2.estimatedAmountOut.toString()); + //console.log("q1", quote.estimatedAmountIn.toString(), quote.estimatedAmountOut.toString()); + }); + }); + }); + + const variationsWith100PercentFee: TokenTrait[][] = [ + [ + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: MAX_FEE_BASIS_POINTS }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + ], + [ + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: MAX_FEE_BASIS_POINTS, transferFeeInitialMax: 99n }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + ], + [ + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: MAX_FEE_BASIS_POINTS }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + ], + [ + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: MAX_FEE_BASIS_POINTS, transferFeeInitialMax: 99n }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + ], + [ + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: MAX_FEE_BASIS_POINTS }, + ], + [ + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0 }, + { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: MAX_FEE_BASIS_POINTS, transferFeeInitialMax: 99n }, + ], + ]; + + variationsWith100PercentFee.forEach(([token0, token1, token2]) => { + + const label0 = `Token0: transfer fee bps = ${token0.transferFeeInitialBps ? ("100%" + (token0.transferFeeInitialMax? " with cap" : " without cap")) : "0%"}`; + const label1 = `Token1: transfer fee bps = ${token1.transferFeeInitialBps ? ("100%" + (token1.transferFeeInitialMax? " with cap" : " without cap")) : "0%"}`; + const label2 = `Token2: transfer fee bps = ${token2.transferFeeInitialBps ? ("100%" + (token2.transferFeeInitialMax? " with cap" : " without cap")) : "0%"}`; + + describe(`${label0}, ${label1}, ${label2}`, () => { + beforeEach(async () => { + aqConfig = getDefaultAquariumV2(); + // Add a third token and account and a second pool + aqConfig.initMintParams = [ + { tokenTrait: {isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0} }, + { tokenTrait: {isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0} }, + { tokenTrait: {isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 0} }, + ]; + aqConfig.initTokenAccParams.push({ mintIndex: 2 }); + aqConfig.initPoolParams.push({ mintIndices: [1, 2], tickSpacing: TickSpacing.Standard }); + + // Add tick arrays and positions + const aToB = false; + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 0, + startTickIndex: 22528, + arrayCount: 3, + aToB, + }); + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 1, + startTickIndex: 22528, + arrayCount: 3, + aToB, + }); + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + aqConfig.initPositionParams.push({ poolIndex: 0, fundParams }); + aqConfig.initPositionParams.push({ poolIndex: 1, fundParams }); + + aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { pools } = aquarium; + + whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE, + )) as WhirlpoolData; + whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE, + )) as WhirlpoolData; + + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + pools[0].tokenMintA, + token0.transferFeeInitialBps!, + token0.transferFeeInitialMax ?? BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ), + createSetTransferFeeInstruction( + pools[0].tokenMintB, + token1.transferFeeInitialBps!, + token1.transferFeeInitialMax ?? BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ), + createSetTransferFeeInstruction( + pools[1].tokenMintB, + token2.transferFeeInitialBps!, + token2.transferFeeInitialMax ?? BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ) + ] + }).buildAndExecute(); + + // wait for epoch to enable updated fee rate + const updatedFeeConfig0 = await fetchTransferFeeConfig(pools[0].tokenMintA); + await waitEpoch(Number(updatedFeeConfig0.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfig0.newerTransferFee.epoch); + + const transferFee0 = await getTransferFee(pools[0].tokenMintA); + const transferFee1 = await getTransferFee(pools[0].tokenMintB); + const transferFee2 = await getTransferFee(pools[1].tokenMintB); + + assert.equal(transferFee0!.transferFeeBasisPoints, token0.transferFeeInitialBps!); + assert.equal(transferFee0!.maximumFee, token0.transferFeeInitialMax ?? BigInt(U64_MAX.toString())); + assert.equal(transferFee1!.transferFeeBasisPoints, token1.transferFeeInitialBps!); + assert.equal(transferFee1!.maximumFee, token1.transferFeeInitialMax ?? BigInt(U64_MAX.toString())); + assert.equal(transferFee2!.transferFeeBasisPoints, token2.transferFeeInitialBps!); + assert.equal(transferFee2!.maximumFee, token2.transferFeeInitialMax ?? BigInt(U64_MAX.toString())); + }); + + it("T0 --> T1 --> T2, ExactIn", async () => { + const [inputToken, midToken, outputToken] = aquarium.mintKeys; + const [inputTokenTrait, midTokenTrait, outputTokenTrait] = [token0, token1, token2]; + + const transferFeeInput = inputTokenTrait.hasTransferFeeExtension ? await getTransferFee(inputToken) : null; + const transferFeeMid = midTokenTrait.hasTransferFeeExtension ? await getTransferFee(midToken) : null; + const transferFeeOutput = outputTokenTrait.hasTransferFeeExtension ? await getTransferFee(outputToken) : null; + + const inputAmount = new BN(1000); + const aToBOne = whirlpoolDataOne.tokenMintA.equals(inputToken); + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(midToken); + const pools = aquarium.pools; + + // edge-case + const inputWithoutCap = transferFeeInput!.transferFeeBasisPoints === MAX_FEE_BASIS_POINTS && transferFeeInput!.maximumFee === BigInt(U64_MAX.toString()); + const midWithoutCap = transferFeeMid!.transferFeeBasisPoints === MAX_FEE_BASIS_POINTS && transferFeeMid!.maximumFee === BigInt(U64_MAX.toString()); + if (inputWithoutCap || midWithoutCap) { + // we cannot determine input size because all amount will be collected as transfer fee + const tickArraysOne = await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE, + ); + const tickArraysTwo = await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE, + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + amountSpecifiedIsInput: true, + amount: inputAmount, + otherAmountThreshold: new BN(0), + sqrtPriceLimitOne: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + sqrtPriceLimitTwo: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + aToBOne, + aToBTwo, + tokenAuthority: ctx.wallet.publicKey, + whirlpoolOne: whirlpoolOneKey, + whirlpoolTwo: whirlpoolTwoKey, + tokenMintInput: inputToken, + tokenMintIntermediate: midToken, + tokenMintOutput: outputToken, + tokenProgramInput: TEST_TOKEN_2022_PROGRAM_ID, + tokenProgramIntermediate: TEST_TOKEN_2022_PROGRAM_ID, + tokenProgramOutput: TEST_TOKEN_2022_PROGRAM_ID, + tokenVaultOneInput: aToBOne ? whirlpoolDataOne.tokenVaultA : whirlpoolDataOne.tokenVaultB, + tokenVaultOneIntermediate: aToBOne ? whirlpoolDataOne.tokenVaultB : whirlpoolDataOne.tokenVaultA, + tokenVaultTwoIntermediate: aToBTwo ? whirlpoolDataTwo.tokenVaultA : whirlpoolDataTwo.tokenVaultB, + tokenVaultTwoOutput: aToBTwo ? whirlpoolDataTwo.tokenVaultB : whirlpoolDataTwo.tokenVaultA, + tokenOwnerAccountInput: aToBOne ? pools[0].tokenVaultAKeypair.publicKey : pools[0].tokenVaultBKeypair.publicKey, + tokenOwnerAccountOutput: aToBTwo ? pools[1].tokenVaultBKeypair.publicKey : pools[1].tokenVaultAKeypair.publicKey, + tickArrayOne0: tickArraysOne[0].address, + tickArrayOne1: tickArraysOne[0].address, + tickArrayOne2: tickArraysOne[0].address, + tickArrayTwo0: tickArraysTwo[0].address, + tickArrayTwo1: tickArraysTwo[0].address, + tickArrayTwo2: tickArraysTwo[0].address, + oracleOne: PDAUtil.getOracle(ctx.program.programId, whirlpoolOneKey).publicKey, + oracleTwo: PDAUtil.getOracle(ctx.program.programId, whirlpoolTwoKey).publicKey, + }), + ).buildAndExecute(), + inputWithoutCap + ? /0x1793/ // ZeroTradableAmount (All amount is collected as transfer fee...) + : /0x1793/, // ZeroTradableAmount (all intermediate token is collected as transfer fee...) + ); + + return; + } + + const transferFeeExcludedInputAmount = transferFeeInput + ? calculateTransferFeeExcludedAmount(transferFeeInput, inputAmount) + : { amount: inputAmount, fee: ZERO_BN }; + if (transferFeeInput && transferFeeInput.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedInputAmount.fee.gtn(0)); + + const quote = swapQuoteWithParams( + { + // T0 --> T1, ExactIn + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: transferFeeExcludedInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), + ); + + /* + // vault -> owner + const transferFeeExcludedMidOutputAmount = transferFeeMid + ? calculateTransferFeeExcludedAmount(transferFeeMid, quote.estimatedAmountOut) + : { amount: quote.estimatedAmountOut, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedMidOutputAmount.fee.gtn(0)); + + // owner -> vault + const transferFeeExcludedMidInputAmount = transferFeeMid + ? calculateTransferFeeExcludedAmount(transferFeeMid, transferFeeExcludedMidOutputAmount.amount) + : { amount: transferFeeExcludedMidOutputAmount.amount, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedMidInputAmount.fee.gtn(0)); + */ + + // vault to vault + const transferFeeExcludedMidInputAmount = transferFeeMid + ? calculateTransferFeeExcludedAmount(transferFeeMid, quote.estimatedAmountOut) + : { amount: quote.estimatedAmountOut, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedMidInputAmount.fee.gtn(0)); + + const quote2 = swapQuoteWithParams( + { + // T1 --> T2, ExactIn + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: transferFeeExcludedMidInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), + ); + + const transferFeeExcludedOutputAmount = transferFeeOutput + ? calculateTransferFeeExcludedAmount(transferFeeOutput, quote2.estimatedAmountOut) + : { amount: quote2.estimatedAmountOut, fee: ZERO_BN }; + if (transferFeeOutput && transferFeeOutput.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedOutputAmount.fee.gtn(0)); + + const expectedOwnerAccountInputDelta = inputAmount.neg(); // out + const expectedOwnerAccountMidDelta = ZERO_BN; // in = out + const expectedOwnerAccountOutputDelta = transferFeeExcludedOutputAmount.amount; // in + const [expectedVaultAccountOneADelta, expectedVaultAccountOneBDelta] = aToBOne + ? [transferFeeExcludedInputAmount.amount, quote.estimatedAmountOut.neg()] + : [quote.estimatedAmountOut.neg(), transferFeeExcludedInputAmount.amount]; + const [expectedVaultAccountTwoADelta, expectedVaultAccountTwoBDelta] = aToBTwo + ? [transferFeeExcludedMidInputAmount.amount, quote2.estimatedAmountOut.neg()] + : [quote2.estimatedAmountOut.neg(), transferFeeExcludedMidInputAmount.amount]; + assert.ok(expectedVaultAccountOneADelta.eq(aToBOne ? quote.estimatedAmountIn : quote.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountOneBDelta.eq(aToBOne ? quote.estimatedAmountOut.neg() : quote.estimatedAmountIn)); + assert.ok(expectedVaultAccountTwoADelta.eq(aToBTwo ? quote2.estimatedAmountIn : quote2.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountTwoBDelta.eq(aToBTwo ? quote2.estimatedAmountOut.neg() : quote2.estimatedAmountIn)); + + const tokenAccKeys = getTokenAccsForPoolsV2(pools, aquarium.tokenAccounts); + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + const baseIxParams: TwoHopSwapV2Params = { + ...twoHopQuote, + amount: inputAmount, // transfer fee included + otherAmountThreshold: transferFeeExcludedOutputAmount.amount, // transfer fee excluded + + tokenAuthority: ctx.wallet.publicKey, + whirlpoolOne: pools[0].whirlpoolPda.publicKey, + whirlpoolTwo: pools[1].whirlpoolPda.publicKey, + tokenMintInput: twoHopQuote.aToBOne ? pools[0].tokenMintA : pools[0].tokenMintB, + tokenMintIntermediate: twoHopQuote.aToBOne ? pools[0].tokenMintB : pools[0].tokenMintA, + tokenMintOutput: twoHopQuote.aToBTwo ? pools[1].tokenMintB : pools[1].tokenMintA, + tokenProgramInput: twoHopQuote.aToBOne ? pools[0].tokenProgramA : pools[0].tokenProgramB, + tokenProgramIntermediate: twoHopQuote.aToBOne ? pools[0].tokenProgramB : pools[0].tokenProgramA, + tokenProgramOutput: twoHopQuote.aToBTwo ? pools[1].tokenProgramB : pools[1].tokenProgramA, + tokenOwnerAccountInput: twoHopQuote.aToBOne ? tokenAccKeys[0] : tokenAccKeys[1], + tokenOwnerAccountOutput: twoHopQuote.aToBTwo ? tokenAccKeys[3] : tokenAccKeys[2], + tokenVaultOneInput: twoHopQuote.aToBOne ? pools[0].tokenVaultAKeypair.publicKey : pools[0].tokenVaultBKeypair.publicKey, + tokenVaultOneIntermediate: twoHopQuote.aToBOne ? pools[0].tokenVaultBKeypair.publicKey : pools[0].tokenVaultAKeypair.publicKey, + tokenVaultTwoIntermediate: twoHopQuote.aToBTwo ? pools[1].tokenVaultAKeypair.publicKey : pools[1].tokenVaultBKeypair.publicKey, + tokenVaultTwoOutput: twoHopQuote.aToBTwo ? pools[1].tokenVaultBKeypair.publicKey : pools[1].tokenVaultAKeypair.publicKey, + oracleOne: PDAUtil.getOracle(ctx.program.programId, pools[0].whirlpoolPda.publicKey) + .publicKey, + oracleTwo: PDAUtil.getOracle(ctx.program.programId, pools[1].whirlpoolPda.publicKey) + .publicKey, + }; + + const preVaultBalanceOneA = new BN(await getTokenBalance(provider, pools[0].tokenVaultAKeypair.publicKey)); + const preVaultBalanceOneB = new BN(await getTokenBalance(provider, pools[0].tokenVaultBKeypair.publicKey)); + const preVaultBalanceTwoA = new BN(await getTokenBalance(provider, pools[1].tokenVaultAKeypair.publicKey)); + const preVaultBalanceTwoB = new BN(await getTokenBalance(provider, pools[1].tokenVaultBKeypair.publicKey)); + const preOwnerAccountBalanceInput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountInput)); + const preOwnerAccountBalanceOutput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountOutput)); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...baseIxParams, + otherAmountThreshold: baseIxParams.otherAmountThreshold.addn(1), + }) + ).prependInstruction(useMaxCU()).buildAndExecute(), // add CU + /0x1794/, // AmountOutBelowMinimum + ); + + await toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, baseIxParams) + ).prependInstruction(useMaxCU()).buildAndExecute(); // add CU + + const postVaultBalanceOneA = new BN(await getTokenBalance(provider, pools[0].tokenVaultAKeypair.publicKey)); + const postVaultBalanceOneB = new BN(await getTokenBalance(provider, pools[0].tokenVaultBKeypair.publicKey)); + const postVaultBalanceTwoA = new BN(await getTokenBalance(provider, pools[1].tokenVaultAKeypair.publicKey)); + const postVaultBalanceTwoB = new BN(await getTokenBalance(provider, pools[1].tokenVaultBKeypair.publicKey)); + const postOwnerAccountBalanceInput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountInput)); + const postOwnerAccountBalanceOutput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountOutput)); + + assert.ok(postVaultBalanceOneA.sub(preVaultBalanceOneA).eq(expectedVaultAccountOneADelta)); + assert.ok(postVaultBalanceOneB.sub(preVaultBalanceOneB).eq(expectedVaultAccountOneBDelta)); + assert.ok(postVaultBalanceTwoA.sub(preVaultBalanceTwoA).eq(expectedVaultAccountTwoADelta)); + assert.ok(postVaultBalanceTwoB.sub(preVaultBalanceTwoB).eq(expectedVaultAccountTwoBDelta)); + assert.ok(postOwnerAccountBalanceInput.sub(preOwnerAccountBalanceInput).eq(expectedOwnerAccountInputDelta)); + assert.ok(postOwnerAccountBalanceOutput.sub(preOwnerAccountBalanceOutput).eq(expectedOwnerAccountOutputDelta)); + + //console.log(`aToB: ${aToBOne} ${aToBTwo}`); + //console.log("in", transferFeeExcludedInputAmount.amount.toString(), transferFeeExcludedInputAmount.fee.toString()); + //console.log("midout", transferFeeExcludedMidOutputAmount.amount.toString(), transferFeeExcludedMidOutputAmount.fee.toString()); + //console.log("midin", transferFeeExcludedMidInputAmount.amount.toString(), transferFeeExcludedMidInputAmount.fee.toString()); + //console.log("out", transferFeeExcludedOutputAmount.amount.toString(), transferFeeExcludedOutputAmount.fee.toString()); + //console.log("q1", quote.estimatedAmountIn.toString(), quote.estimatedAmountOut.toString()); + //console.log("q2", quote2.estimatedAmountIn.toString(), quote2.estimatedAmountOut.toString()); + }); + + it("T0 <-- T1 <-- T2, ExactIn", async () => { + const [outputToken, midToken, inputToken] = aquarium.mintKeys; + const [outputTokenTrait, midTokenTrait, inputTokenTrait] = [token0, token1, token2]; + + const transferFeeInput = inputTokenTrait.hasTransferFeeExtension ? await getTransferFee(inputToken) : null; + const transferFeeMid = midTokenTrait.hasTransferFeeExtension ? await getTransferFee(midToken) : null; + const transferFeeOutput = outputTokenTrait.hasTransferFeeExtension ? await getTransferFee(outputToken) : null; + + const inputAmount = new BN(100000); + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(inputToken); + const aToBOne = whirlpoolDataOne.tokenMintA.equals(midToken); + const pools = aquarium.pools; + + // edge-case + const inputWithoutCap = transferFeeInput!.transferFeeBasisPoints === MAX_FEE_BASIS_POINTS && transferFeeInput!.maximumFee === BigInt(U64_MAX.toString()); + const midWithoutCap = transferFeeMid!.transferFeeBasisPoints === MAX_FEE_BASIS_POINTS && transferFeeMid!.maximumFee === BigInt(U64_MAX.toString()); + if (inputWithoutCap || midWithoutCap) { + // we cannot determine input size because all amount will be collected as transfer fee + const tickArraysOne = await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE, + ); + const tickArraysTwo = await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE, + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + amountSpecifiedIsInput: true, + amount: inputAmount, + otherAmountThreshold: new BN(0), + sqrtPriceLimitOne: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + sqrtPriceLimitTwo: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + aToBOne: aToBTwo, + aToBTwo: aToBOne, + tokenAuthority: ctx.wallet.publicKey, + whirlpoolOne: whirlpoolTwoKey, + whirlpoolTwo: whirlpoolOneKey, + tokenMintInput: inputToken, + tokenMintIntermediate: midToken, + tokenMintOutput: outputToken, + tokenProgramInput: TEST_TOKEN_2022_PROGRAM_ID, + tokenProgramIntermediate: TEST_TOKEN_2022_PROGRAM_ID, + tokenProgramOutput: TEST_TOKEN_2022_PROGRAM_ID, + tokenVaultOneInput: aToBTwo ? whirlpoolDataTwo.tokenVaultA : whirlpoolDataTwo.tokenVaultB, + tokenVaultOneIntermediate: aToBTwo ? whirlpoolDataTwo.tokenVaultB : whirlpoolDataTwo.tokenVaultA, + tokenVaultTwoIntermediate: aToBOne ? whirlpoolDataOne.tokenVaultA : whirlpoolDataOne.tokenVaultB, + tokenVaultTwoOutput: aToBOne ? whirlpoolDataOne.tokenVaultB : whirlpoolDataOne.tokenVaultA, + tokenOwnerAccountInput: aToBTwo ? pools[1].tokenVaultAKeypair.publicKey : pools[1].tokenVaultBKeypair.publicKey, + tokenOwnerAccountOutput: aToBOne ? pools[0].tokenVaultBKeypair.publicKey : pools[0].tokenVaultAKeypair.publicKey, + tickArrayOne0: tickArraysTwo[0].address, + tickArrayOne1: tickArraysTwo[0].address, + tickArrayOne2: tickArraysTwo[0].address, + tickArrayTwo0: tickArraysOne[0].address, + tickArrayTwo1: tickArraysOne[0].address, + tickArrayTwo2: tickArraysOne[0].address, + oracleOne: PDAUtil.getOracle(ctx.program.programId, whirlpoolTwoKey).publicKey, + oracleTwo: PDAUtil.getOracle(ctx.program.programId, whirlpoolOneKey).publicKey, + }), + ).buildAndExecute(), + inputWithoutCap + ? /0x1793/ // ZeroTradableAmount (All amount is collected as transfer fee...) + : /0x1793/, // ZeroTradableAmount (all intermediate token is collected as transfer fee...) + ); + + return; + } + + const transferFeeExcludedInputAmount = transferFeeInput + ? calculateTransferFeeExcludedAmount(transferFeeInput, inputAmount) + : { amount: inputAmount, fee: ZERO_BN }; + if (transferFeeInput && transferFeeInput.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedInputAmount.fee.gtn(0)); + + const quote = swapQuoteWithParams( + { + // T1 <-- T2, ExactIn + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: transferFeeExcludedInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), + ); + + /* + // vault -> owner + const transferFeeExcludedMidOutputAmount = transferFeeMid + ? calculateTransferFeeExcludedAmount(transferFeeMid, quote.estimatedAmountOut) + : { amount: quote.estimatedAmountOut, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedMidOutputAmount.fee.gtn(0)); + + // owner -> vault + const transferFeeExcludedMidInputAmount = transferFeeMid + ? calculateTransferFeeExcludedAmount(transferFeeMid, transferFeeExcludedMidOutputAmount.amount) + : { amount: transferFeeExcludedMidOutputAmount.amount, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedMidInputAmount.fee.gtn(0)); + */ + + // vault to vault + const transferFeeExcludedMidInputAmount = transferFeeMid + ? calculateTransferFeeExcludedAmount(transferFeeMid, quote.estimatedAmountOut) + : { amount: quote.estimatedAmountOut, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedMidInputAmount.fee.gtn(0)); + + const quote2 = swapQuoteWithParams( + { + // T0 <-- T1, ExactIn + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: transferFeeExcludedMidInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), + ); + + const transferFeeExcludedOutputAmount = transferFeeOutput + ? calculateTransferFeeExcludedAmount(transferFeeOutput, quote2.estimatedAmountOut) + : { amount: quote2.estimatedAmountOut, fee: ZERO_BN }; + if (transferFeeOutput && transferFeeOutput.transferFeeBasisPoints > 0) + assert.ok(transferFeeExcludedOutputAmount.fee.gtn(0)); + + const expectedOwnerAccountInputDelta = inputAmount.neg(); // out + const expectedOwnerAccountMidDelta = ZERO_BN; // in = out + const expectedOwnerAccountOutputDelta = transferFeeExcludedOutputAmount.amount; // in + const [expectedVaultAccountOneADelta, expectedVaultAccountOneBDelta] = aToBOne + ? [transferFeeExcludedInputAmount.amount, quote.estimatedAmountOut.neg()] + : [quote.estimatedAmountOut.neg(), transferFeeExcludedInputAmount.amount]; + const [expectedVaultAccountTwoADelta, expectedVaultAccountTwoBDelta] = aToBTwo + ? [transferFeeExcludedMidInputAmount.amount, quote2.estimatedAmountOut.neg()] + : [quote2.estimatedAmountOut.neg(), transferFeeExcludedMidInputAmount.amount]; + assert.ok(expectedVaultAccountOneADelta.eq(aToBOne ? quote.estimatedAmountIn : quote.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountOneBDelta.eq(aToBOne ? quote.estimatedAmountOut.neg() : quote.estimatedAmountIn)); + assert.ok(expectedVaultAccountTwoADelta.eq(aToBTwo ? quote2.estimatedAmountIn : quote2.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountTwoBDelta.eq(aToBTwo ? quote2.estimatedAmountOut.neg() : quote2.estimatedAmountIn)); + + const tokenAccKeys = getTokenAccsForPoolsV2(pools, aquarium.tokenAccounts); + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + const baseIxParams: TwoHopSwapV2Params = { + ...twoHopQuote, + amount: inputAmount, // transfer fee included + otherAmountThreshold: transferFeeExcludedOutputAmount.amount, // transfer fee excluded + + tokenAuthority: ctx.wallet.publicKey, + whirlpoolOne: pools[1].whirlpoolPda.publicKey, + whirlpoolTwo: pools[0].whirlpoolPda.publicKey, + tokenMintInput: aToBTwo ? pools[1].tokenMintA : pools[1].tokenMintB, + tokenMintIntermediate: aToBTwo ? pools[1].tokenMintB : pools[1].tokenMintA, + tokenMintOutput: aToBOne ? pools[0].tokenMintB : pools[0].tokenMintA, + tokenProgramInput: aToBTwo ? pools[1].tokenProgramA : pools[1].tokenProgramB, + tokenProgramIntermediate: aToBTwo ? pools[1].tokenProgramB : pools[1].tokenProgramA, + tokenProgramOutput: aToBOne ? pools[0].tokenProgramB : pools[0].tokenProgramA, + tokenOwnerAccountInput: aToBTwo ? tokenAccKeys[2] : tokenAccKeys[3], + tokenOwnerAccountOutput: aToBOne ? tokenAccKeys[1] : tokenAccKeys[0], + tokenVaultOneInput: aToBTwo ? pools[1].tokenVaultAKeypair.publicKey : pools[1].tokenVaultBKeypair.publicKey, + tokenVaultOneIntermediate: aToBTwo ? pools[1].tokenVaultBKeypair.publicKey : pools[1].tokenVaultAKeypair.publicKey, + tokenVaultTwoIntermediate: aToBOne ? pools[0].tokenVaultAKeypair.publicKey : pools[0].tokenVaultBKeypair.publicKey, + tokenVaultTwoOutput: aToBOne ? pools[0].tokenVaultBKeypair.publicKey : pools[0].tokenVaultAKeypair.publicKey, + oracleOne: PDAUtil.getOracle(ctx.program.programId, pools[1].whirlpoolPda.publicKey) + .publicKey, + oracleTwo: PDAUtil.getOracle(ctx.program.programId, pools[0].whirlpoolPda.publicKey) + .publicKey, + }; + + const preVaultBalanceOneA = new BN(await getTokenBalance(provider, pools[1].tokenVaultAKeypair.publicKey)); + const preVaultBalanceOneB = new BN(await getTokenBalance(provider, pools[1].tokenVaultBKeypair.publicKey)); + const preVaultBalanceTwoA = new BN(await getTokenBalance(provider, pools[0].tokenVaultAKeypair.publicKey)); + const preVaultBalanceTwoB = new BN(await getTokenBalance(provider, pools[0].tokenVaultBKeypair.publicKey)); + const preOwnerAccountBalanceInput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountInput)); + const preOwnerAccountBalanceOutput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountOutput)); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...baseIxParams, + otherAmountThreshold: baseIxParams.otherAmountThreshold.addn(1), + }) + ).prependInstruction(useMaxCU()).buildAndExecute(), // add CU + /0x1794/, // AmountOutBelowMinimum + ); + + await toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, baseIxParams) + ).prependInstruction(useMaxCU()).buildAndExecute(); // add CU + + const postVaultBalanceOneA = new BN(await getTokenBalance(provider, pools[1].tokenVaultAKeypair.publicKey)); + const postVaultBalanceOneB = new BN(await getTokenBalance(provider, pools[1].tokenVaultBKeypair.publicKey)); + const postVaultBalanceTwoA = new BN(await getTokenBalance(provider, pools[0].tokenVaultAKeypair.publicKey)); + const postVaultBalanceTwoB = new BN(await getTokenBalance(provider, pools[0].tokenVaultBKeypair.publicKey)); + const postOwnerAccountBalanceInput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountInput)); + const postOwnerAccountBalanceOutput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountOutput)); + + assert.ok(postVaultBalanceOneA.sub(preVaultBalanceOneA).eq(expectedVaultAccountOneADelta)); + assert.ok(postVaultBalanceOneB.sub(preVaultBalanceOneB).eq(expectedVaultAccountOneBDelta)); + assert.ok(postVaultBalanceTwoA.sub(preVaultBalanceTwoA).eq(expectedVaultAccountTwoADelta)); + assert.ok(postVaultBalanceTwoB.sub(preVaultBalanceTwoB).eq(expectedVaultAccountTwoBDelta)); + assert.ok(postOwnerAccountBalanceInput.sub(preOwnerAccountBalanceInput).eq(expectedOwnerAccountInputDelta)); + assert.ok(postOwnerAccountBalanceOutput.sub(preOwnerAccountBalanceOutput).eq(expectedOwnerAccountOutputDelta)); + + //console.log(`aToB: ${aToBTwo} ${aToBOne}`); + //console.log("in", transferFeeExcludedInputAmount.amount.toString(), transferFeeExcludedInputAmount.fee.toString()); + //console.log("midout", transferFeeExcludedMidOutputAmount.amount.toString(), transferFeeExcludedMidOutputAmount.fee.toString()); + //console.log("midin", transferFeeExcludedMidInputAmount.amount.toString(), transferFeeExcludedMidInputAmount.fee.toString()); + //console.log("out", transferFeeExcludedOutputAmount.amount.toString(), transferFeeExcludedOutputAmount.fee.toString()); + //console.log("q1", quote.estimatedAmountIn.toString(), quote.estimatedAmountOut.toString()); + //console.log("q2", quote2.estimatedAmountIn.toString(), quote2.estimatedAmountOut.toString()); + }) + + it("T0 --> T1 --> T2, ExactOut", async () => { + const [inputToken, midToken, outputToken] = aquarium.mintKeys; + const [inputTokenTrait, midTokenTrait, outputTokenTrait] = [token0, token1, token2]; + + const transferFeeInput = inputTokenTrait.hasTransferFeeExtension ? await getTransferFee(inputToken) : null; + const transferFeeMid = midTokenTrait.hasTransferFeeExtension ? await getTransferFee(midToken) : null; + const transferFeeOutput = outputTokenTrait.hasTransferFeeExtension ? await getTransferFee(outputToken) : null; + + const outputAmount = new BN(500000); + const aToBTwo = whirlpoolDataTwo.tokenMintB.equals(outputToken); + const aToBOne = whirlpoolDataOne.tokenMintB.equals(midToken); + const pools = aquarium.pools; + + // edge-case + const inputWithoutCap = transferFeeInput!.transferFeeBasisPoints === MAX_FEE_BASIS_POINTS && transferFeeInput!.maximumFee === BigInt(U64_MAX.toString()); + const midWithoutCap = transferFeeMid!.transferFeeBasisPoints === MAX_FEE_BASIS_POINTS && transferFeeMid!.maximumFee === BigInt(U64_MAX.toString()); + const outputWithoutCap = transferFeeOutput!.transferFeeBasisPoints === MAX_FEE_BASIS_POINTS && transferFeeOutput!.maximumFee === BigInt(U64_MAX.toString()); + if (inputWithoutCap || outputWithoutCap || midWithoutCap) { + // we cannot determine input/output size because all amount will be collected as transfer fee + const tickArraysOne = await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE, + ); + const tickArraysTwo = await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE, + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + amountSpecifiedIsInput: false, + amount: outputAmount, + otherAmountThreshold: new BN(U64_MAX.toString()), + sqrtPriceLimitOne: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + sqrtPriceLimitTwo: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + aToBOne, + aToBTwo, + tokenAuthority: ctx.wallet.publicKey, + whirlpoolOne: whirlpoolOneKey, + whirlpoolTwo: whirlpoolTwoKey, + tokenMintInput: inputToken, + tokenMintIntermediate: midToken, + tokenMintOutput: outputToken, + tokenProgramInput: TEST_TOKEN_2022_PROGRAM_ID, + tokenProgramIntermediate: TEST_TOKEN_2022_PROGRAM_ID, + tokenProgramOutput: TEST_TOKEN_2022_PROGRAM_ID, + tokenVaultOneInput: aToBOne ? whirlpoolDataOne.tokenVaultA : whirlpoolDataOne.tokenVaultB, + tokenVaultOneIntermediate: aToBOne ? whirlpoolDataOne.tokenVaultB : whirlpoolDataOne.tokenVaultA, + tokenVaultTwoIntermediate: aToBTwo ? whirlpoolDataTwo.tokenVaultA : whirlpoolDataTwo.tokenVaultB, + tokenVaultTwoOutput: aToBTwo ? whirlpoolDataTwo.tokenVaultB : whirlpoolDataTwo.tokenVaultA, + tokenOwnerAccountInput: aToBOne ? pools[0].tokenVaultAKeypair.publicKey : pools[0].tokenVaultBKeypair.publicKey, + tokenOwnerAccountOutput: aToBTwo ? pools[1].tokenVaultBKeypair.publicKey : pools[1].tokenVaultAKeypair.publicKey, + tickArrayOne0: tickArraysOne[0].address, + tickArrayOne1: tickArraysOne[0].address, + tickArrayOne2: tickArraysOne[0].address, + tickArrayTwo0: tickArraysTwo[0].address, + tickArrayTwo1: tickArraysTwo[0].address, + tickArrayTwo2: tickArraysTwo[0].address, + oracleOne: PDAUtil.getOracle(ctx.program.programId, whirlpoolOneKey).publicKey, + oracleTwo: PDAUtil.getOracle(ctx.program.programId, whirlpoolTwoKey).publicKey, + }), + ).buildAndExecute(), + /0x17a4/, // TransferFeeCalculationError + ); + + return; + } + + const transferFeeIncludedOutputAmount = transferFeeOutput + ? calculateTransferFeeIncludedAmount(transferFeeOutput, outputAmount) + : { amount: outputAmount, fee: ZERO_BN }; + if (transferFeeOutput && transferFeeOutput.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedOutputAmount.fee.gtn(0)); + + const quote2 = swapQuoteWithParams( + { + // T1 --> T2, ExactOut + amountSpecifiedIsInput: false, + aToB: aToBTwo, + tokenAmount: transferFeeIncludedOutputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), + ); + + /* + // owner -> vault + const transferFeeIncludedMidInputAmount = transferFeeMid + ? calculateTransferFeeIncludedAmount(transferFeeMid, quote2.estimatedAmountIn) + : { amount: quote2.estimatedAmountIn, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedMidInputAmount.fee.gtn(0)); + + // vault -> owner + const transferFeeIncludedMidOutputAmount = transferFeeMid + ? calculateTransferFeeIncludedAmount(transferFeeMid, transferFeeIncludedMidInputAmount.amount) + : { amount: transferFeeIncludedMidInputAmount.amount, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedMidOutputAmount.fee.gtn(0)); + */ + + // vault to vault + const transferFeeIncludedMidOutputAmount = transferFeeMid + ? calculateTransferFeeIncludedAmount(transferFeeMid, quote2.estimatedAmountIn) + : { amount: quote2.estimatedAmountIn, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedMidOutputAmount.fee.gtn(0)); + + const quote = swapQuoteWithParams( + { + // T0 --> T1, ExactOut + amountSpecifiedIsInput: false, + aToB: aToBOne, + tokenAmount: transferFeeIncludedMidOutputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), + ); + + const transferFeeIncludedInputAmount = transferFeeInput + ? calculateTransferFeeIncludedAmount(transferFeeInput, quote.estimatedAmountIn) + : { amount: quote.estimatedAmountIn, fee: ZERO_BN }; + if (transferFeeInput && transferFeeInput.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedInputAmount.fee.gtn(0)); + + const expectedOwnerAccountInputDelta = transferFeeIncludedInputAmount.amount.neg(); // out + const expectedOwnerAccountMidDelta = ZERO_BN; // in = out + const expectedOwnerAccountOutputDelta = outputAmount; // in + const [expectedVaultAccountOneADelta, expectedVaultAccountOneBDelta] = aToBOne + ? [quote.estimatedAmountIn, transferFeeIncludedMidOutputAmount.amount.neg()] + : [transferFeeIncludedMidOutputAmount.amount.neg(), quote.estimatedAmountIn]; + const [expectedVaultAccountTwoADelta, expectedVaultAccountTwoBDelta] = aToBTwo + ? [quote2.estimatedAmountIn, transferFeeIncludedOutputAmount.amount.neg()] + : [transferFeeIncludedOutputAmount.amount.neg(), quote2.estimatedAmountIn]; + assert.ok(expectedVaultAccountOneADelta.eq(aToBOne ? quote.estimatedAmountIn : quote.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountOneBDelta.eq(aToBOne ? quote.estimatedAmountOut.neg() : quote.estimatedAmountIn)); + assert.ok(expectedVaultAccountTwoADelta.eq(aToBTwo ? quote2.estimatedAmountIn : quote2.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountTwoBDelta.eq(aToBTwo ? quote2.estimatedAmountOut.neg() : quote2.estimatedAmountIn)); + + const tokenAccKeys = getTokenAccsForPoolsV2(pools, aquarium.tokenAccounts); + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + const baseIxParams: TwoHopSwapV2Params = { + ...twoHopQuote, + amount: outputAmount, // transfer fee excluded + otherAmountThreshold: transferFeeIncludedInputAmount.amount, // transfer fee included + + tokenAuthority: ctx.wallet.publicKey, + whirlpoolOne: pools[0].whirlpoolPda.publicKey, + whirlpoolTwo: pools[1].whirlpoolPda.publicKey, + tokenMintInput: twoHopQuote.aToBOne ? pools[0].tokenMintA : pools[0].tokenMintB, + tokenMintIntermediate: twoHopQuote.aToBOne ? pools[0].tokenMintB : pools[0].tokenMintA, + tokenMintOutput: twoHopQuote.aToBTwo ? pools[1].tokenMintB : pools[1].tokenMintA, + tokenProgramInput: twoHopQuote.aToBOne ? pools[0].tokenProgramA : pools[0].tokenProgramB, + tokenProgramIntermediate: twoHopQuote.aToBOne ? pools[0].tokenProgramB : pools[0].tokenProgramA, + tokenProgramOutput: twoHopQuote.aToBTwo ? pools[1].tokenProgramB : pools[1].tokenProgramA, + tokenOwnerAccountInput: twoHopQuote.aToBOne ? tokenAccKeys[0] : tokenAccKeys[1], + tokenOwnerAccountOutput: twoHopQuote.aToBTwo ? tokenAccKeys[3] : tokenAccKeys[2], + tokenVaultOneInput: twoHopQuote.aToBOne ? pools[0].tokenVaultAKeypair.publicKey : pools[0].tokenVaultBKeypair.publicKey, + tokenVaultOneIntermediate: twoHopQuote.aToBOne ? pools[0].tokenVaultBKeypair.publicKey : pools[0].tokenVaultAKeypair.publicKey, + tokenVaultTwoIntermediate: twoHopQuote.aToBTwo ? pools[1].tokenVaultAKeypair.publicKey : pools[1].tokenVaultBKeypair.publicKey, + tokenVaultTwoOutput: twoHopQuote.aToBTwo ? pools[1].tokenVaultBKeypair.publicKey : pools[1].tokenVaultAKeypair.publicKey, + oracleOne: PDAUtil.getOracle(ctx.program.programId, pools[0].whirlpoolPda.publicKey) + .publicKey, + oracleTwo: PDAUtil.getOracle(ctx.program.programId, pools[1].whirlpoolPda.publicKey) + .publicKey, + }; + + const preVaultBalanceOneA = new BN(await getTokenBalance(provider, pools[0].tokenVaultAKeypair.publicKey)); + const preVaultBalanceOneB = new BN(await getTokenBalance(provider, pools[0].tokenVaultBKeypair.publicKey)); + const preVaultBalanceTwoA = new BN(await getTokenBalance(provider, pools[1].tokenVaultAKeypair.publicKey)); + const preVaultBalanceTwoB = new BN(await getTokenBalance(provider, pools[1].tokenVaultBKeypair.publicKey)); + const preOwnerAccountBalanceInput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountInput)); + const preOwnerAccountBalanceOutput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountOutput)); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...baseIxParams, + otherAmountThreshold: baseIxParams.otherAmountThreshold.subn(1), + }) + ).prependInstruction(useMaxCU()).buildAndExecute(), // add CU + /0x1795/, // AmountInAboveMaximum + ); + + await toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, baseIxParams) + ).prependInstruction(useMaxCU()).buildAndExecute(); // add CU + + const postVaultBalanceOneA = new BN(await getTokenBalance(provider, pools[0].tokenVaultAKeypair.publicKey)); + const postVaultBalanceOneB = new BN(await getTokenBalance(provider, pools[0].tokenVaultBKeypair.publicKey)); + const postVaultBalanceTwoA = new BN(await getTokenBalance(provider, pools[1].tokenVaultAKeypair.publicKey)); + const postVaultBalanceTwoB = new BN(await getTokenBalance(provider, pools[1].tokenVaultBKeypair.publicKey)); + const postOwnerAccountBalanceInput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountInput)); + const postOwnerAccountBalanceOutput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountOutput)); + + assert.ok(postVaultBalanceOneA.sub(preVaultBalanceOneA).eq(expectedVaultAccountOneADelta)); + assert.ok(postVaultBalanceOneB.sub(preVaultBalanceOneB).eq(expectedVaultAccountOneBDelta)); + assert.ok(postVaultBalanceTwoA.sub(preVaultBalanceTwoA).eq(expectedVaultAccountTwoADelta)); + assert.ok(postVaultBalanceTwoB.sub(preVaultBalanceTwoB).eq(expectedVaultAccountTwoBDelta)); + assert.ok(postOwnerAccountBalanceInput.sub(preOwnerAccountBalanceInput).eq(expectedOwnerAccountInputDelta)); + assert.ok(postOwnerAccountBalanceOutput.sub(preOwnerAccountBalanceOutput).eq(expectedOwnerAccountOutputDelta)); + + //console.log(`aToB: ${aToBOne} ${aToBTwo}`); + //console.log("out", transferFeeIncludedOutputAmount.amount.toString(), transferFeeIncludedOutputAmount.fee.toString()); + //console.log("midin", transferFeeIncludedMidInputAmount.amount.toString(), transferFeeIncludedMidInputAmount.fee.toString()); + //console.log("midout", transferFeeIncludedMidOutputAmount.amount.toString(), transferFeeIncludedMidOutputAmount.fee.toString()); + //console.log("in", transferFeeIncludedInputAmount.amount.toString(), transferFeeIncludedInputAmount.fee.toString()); + //console.log("q2", quote2.estimatedAmountIn.toString(), quote2.estimatedAmountOut.toString()); + //console.log("q1", quote.estimatedAmountIn.toString(), quote.estimatedAmountOut.toString()); + }); + + it("T0 <-- T1 <-- T2, ExactOut", async () => { + const [outputToken, midToken, inputToken] = aquarium.mintKeys; + const [outputTokenTrait, midTokenTrait, inputTokenTrait] = [token0, token1, token2]; + + const transferFeeInput = inputTokenTrait.hasTransferFeeExtension ? await getTransferFee(inputToken) : null; + const transferFeeMid = midTokenTrait.hasTransferFeeExtension ? await getTransferFee(midToken) : null; + const transferFeeOutput = outputTokenTrait.hasTransferFeeExtension ? await getTransferFee(outputToken) : null; + + const outputAmount = new BN(1000); + const aToBTwo = whirlpoolDataOne.tokenMintB.equals(outputToken); + const aToBOne = whirlpoolDataTwo.tokenMintB.equals(midToken); + const pools = aquarium.pools; + + // edge-case + const inputWithoutCap = transferFeeInput!.transferFeeBasisPoints === MAX_FEE_BASIS_POINTS && transferFeeInput!.maximumFee === BigInt(U64_MAX.toString()); + const midWithoutCap = transferFeeMid!.transferFeeBasisPoints === MAX_FEE_BASIS_POINTS && transferFeeMid!.maximumFee === BigInt(U64_MAX.toString()); + const outputWithoutCap = transferFeeOutput!.transferFeeBasisPoints === MAX_FEE_BASIS_POINTS && transferFeeOutput!.maximumFee === BigInt(U64_MAX.toString()); + if (inputWithoutCap || outputWithoutCap || midWithoutCap) { + // we cannot determine input/output size because all amount will be collected as transfer fee + const tickArraysOne = await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE, + ); + const tickArraysTwo = await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE, + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + amountSpecifiedIsInput: false, + amount: outputAmount, + otherAmountThreshold: new BN(U64_MAX.toString()), + sqrtPriceLimitOne: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + sqrtPriceLimitTwo: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + aToBOne: aToBTwo, + aToBTwo: aToBOne, + tokenAuthority: ctx.wallet.publicKey, + whirlpoolOne: whirlpoolTwoKey, + whirlpoolTwo: whirlpoolOneKey, + tokenMintInput: inputToken, + tokenMintIntermediate: midToken, + tokenMintOutput: outputToken, + tokenProgramInput: TEST_TOKEN_2022_PROGRAM_ID, + tokenProgramIntermediate: TEST_TOKEN_2022_PROGRAM_ID, + tokenProgramOutput: TEST_TOKEN_2022_PROGRAM_ID, + tokenVaultOneInput: aToBTwo ? whirlpoolDataTwo.tokenVaultA : whirlpoolDataTwo.tokenVaultB, + tokenVaultOneIntermediate: aToBTwo ? whirlpoolDataTwo.tokenVaultB : whirlpoolDataTwo.tokenVaultA, + tokenVaultTwoIntermediate: aToBOne ? whirlpoolDataOne.tokenVaultA : whirlpoolDataOne.tokenVaultB, + tokenVaultTwoOutput: aToBOne ? whirlpoolDataOne.tokenVaultB : whirlpoolDataOne.tokenVaultA, + tokenOwnerAccountInput: aToBTwo ? pools[1].tokenVaultAKeypair.publicKey : pools[1].tokenVaultBKeypair.publicKey, + tokenOwnerAccountOutput: aToBOne ? pools[0].tokenVaultBKeypair.publicKey : pools[0].tokenVaultAKeypair.publicKey, + tickArrayOne0: tickArraysTwo[0].address, + tickArrayOne1: tickArraysTwo[0].address, + tickArrayOne2: tickArraysTwo[0].address, + tickArrayTwo0: tickArraysOne[0].address, + tickArrayTwo1: tickArraysOne[0].address, + tickArrayTwo2: tickArraysOne[0].address, + oracleOne: PDAUtil.getOracle(ctx.program.programId, whirlpoolTwoKey).publicKey, + oracleTwo: PDAUtil.getOracle(ctx.program.programId, whirlpoolOneKey).publicKey, + }), + ).buildAndExecute(), + /0x17a4/, // TransferFeeCalculationError + ); + + return; + } + + const transferFeeIncludedOutputAmount = transferFeeOutput + ? calculateTransferFeeIncludedAmount(transferFeeOutput, outputAmount) + : { amount: outputAmount, fee: ZERO_BN }; + if (transferFeeOutput && transferFeeOutput.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedOutputAmount.fee.gtn(0)); + + const quote2 = swapQuoteWithParams( + { + // T0 <-- T1, ExactOut + amountSpecifiedIsInput: false, + aToB: aToBTwo, + tokenAmount: transferFeeIncludedOutputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), + ); + + /* + // owner -> vault + const transferFeeIncludedMidInputAmount = transferFeeMid + ? calculateTransferFeeIncludedAmount(transferFeeMid, quote2.estimatedAmountIn) + : { amount: quote2.estimatedAmountIn, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedMidInputAmount.fee.gtn(0)); + + // vault -> owner + const transferFeeIncludedMidOutputAmount = transferFeeMid + ? calculateTransferFeeIncludedAmount(transferFeeMid, transferFeeIncludedMidInputAmount.amount) + : { amount: transferFeeIncludedMidInputAmount.amount, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedMidOutputAmount.fee.gtn(0)); + */ + + // vault to vault + const transferFeeIncludedMidOutputAmount = transferFeeMid + ? calculateTransferFeeIncludedAmount(transferFeeMid, quote2.estimatedAmountIn) + : { amount: quote2.estimatedAmountIn, fee: ZERO_BN }; + if (transferFeeMid && transferFeeMid.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedMidOutputAmount.fee.gtn(0)); + + const quote = swapQuoteWithParams( + { + // T1 <-- T2, ExactOut + amountSpecifiedIsInput: false, + aToB: aToBOne, + tokenAmount: transferFeeIncludedMidOutputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), + ); + + const transferFeeIncludedInputAmount = transferFeeInput + ? calculateTransferFeeIncludedAmount(transferFeeInput, quote.estimatedAmountIn) + : { amount: quote.estimatedAmountIn, fee: ZERO_BN }; + if (transferFeeInput && transferFeeInput.transferFeeBasisPoints > 0) + assert.ok(transferFeeIncludedInputAmount.fee.gtn(0)); + + const expectedOwnerAccountInputDelta = transferFeeIncludedInputAmount.amount.neg(); // out + const expectedOwnerAccountMidDelta = ZERO_BN; // in = out + const expectedOwnerAccountOutputDelta = outputAmount; // in + const [expectedVaultAccountTwoADelta, expectedVaultAccountTwoBDelta] = aToBTwo + ? [quote2.estimatedAmountIn, transferFeeIncludedOutputAmount.amount.neg()] + : [transferFeeIncludedOutputAmount.amount.neg(), quote2.estimatedAmountIn]; + const [expectedVaultAccountOneADelta, expectedVaultAccountOneBDelta] = aToBOne + ? [quote.estimatedAmountIn, transferFeeIncludedMidOutputAmount.amount.neg()] + : [transferFeeIncludedMidOutputAmount.amount.neg(), quote.estimatedAmountIn]; + assert.ok(expectedVaultAccountTwoADelta.eq(aToBTwo ? quote2.estimatedAmountIn : quote2.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountTwoBDelta.eq(aToBTwo ? quote2.estimatedAmountOut.neg() : quote2.estimatedAmountIn)); + assert.ok(expectedVaultAccountOneADelta.eq(aToBOne ? quote.estimatedAmountIn : quote.estimatedAmountOut.neg())); + assert.ok(expectedVaultAccountOneBDelta.eq(aToBOne ? quote.estimatedAmountOut.neg() : quote.estimatedAmountIn)); + + const tokenAccKeys = getTokenAccsForPoolsV2(pools, aquarium.tokenAccounts); + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + const baseIxParams: TwoHopSwapV2Params = { + ...twoHopQuote, + amount: outputAmount, // transfer fee excluded + otherAmountThreshold: transferFeeIncludedInputAmount.amount, // transfer fee included + + tokenAuthority: ctx.wallet.publicKey, + whirlpoolOne: pools[1].whirlpoolPda.publicKey, + whirlpoolTwo: pools[0].whirlpoolPda.publicKey, + tokenMintInput: aToBTwo ? pools[1].tokenMintA : pools[1].tokenMintB, + tokenMintIntermediate: aToBTwo ? pools[1].tokenMintB : pools[1].tokenMintA, + tokenMintOutput: aToBOne ? pools[0].tokenMintB : pools[0].tokenMintA, + tokenProgramInput: aToBTwo ? pools[1].tokenProgramA : pools[1].tokenProgramB, + tokenProgramIntermediate: aToBTwo ? pools[1].tokenProgramB : pools[1].tokenProgramA, + tokenProgramOutput: aToBOne ? pools[0].tokenProgramB : pools[0].tokenProgramA, + tokenOwnerAccountInput: aToBTwo ? tokenAccKeys[2] : tokenAccKeys[3], + tokenOwnerAccountOutput: aToBOne ? tokenAccKeys[1] : tokenAccKeys[0], + tokenVaultOneInput: aToBTwo ? pools[1].tokenVaultAKeypair.publicKey : pools[1].tokenVaultBKeypair.publicKey, + tokenVaultOneIntermediate: aToBTwo ? pools[1].tokenVaultBKeypair.publicKey : pools[1].tokenVaultAKeypair.publicKey, + tokenVaultTwoIntermediate: aToBOne ? pools[0].tokenVaultAKeypair.publicKey : pools[0].tokenVaultBKeypair.publicKey, + tokenVaultTwoOutput: aToBOne ? pools[0].tokenVaultBKeypair.publicKey : pools[0].tokenVaultAKeypair.publicKey, + oracleOne: PDAUtil.getOracle(ctx.program.programId, pools[1].whirlpoolPda.publicKey) + .publicKey, + oracleTwo: PDAUtil.getOracle(ctx.program.programId, pools[0].whirlpoolPda.publicKey) + .publicKey, + }; + + const preVaultBalanceOneA = new BN(await getTokenBalance(provider, pools[1].tokenVaultAKeypair.publicKey)); + const preVaultBalanceOneB = new BN(await getTokenBalance(provider, pools[1].tokenVaultBKeypair.publicKey)); + const preVaultBalanceTwoA = new BN(await getTokenBalance(provider, pools[0].tokenVaultAKeypair.publicKey)); + const preVaultBalanceTwoB = new BN(await getTokenBalance(provider, pools[0].tokenVaultBKeypair.publicKey)); + const preOwnerAccountBalanceInput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountInput)); + const preOwnerAccountBalanceOutput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountOutput)); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...baseIxParams, + otherAmountThreshold: baseIxParams.otherAmountThreshold.subn(1), + }) + ).prependInstruction(useMaxCU()).buildAndExecute(), // add CU + /0x1795/, // AmountInAboveMaximum + ); + + await toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, baseIxParams) + ).prependInstruction(useMaxCU()).buildAndExecute(); // add CU + + const postVaultBalanceOneA = new BN(await getTokenBalance(provider, pools[1].tokenVaultAKeypair.publicKey)); + const postVaultBalanceOneB = new BN(await getTokenBalance(provider, pools[1].tokenVaultBKeypair.publicKey)); + const postVaultBalanceTwoA = new BN(await getTokenBalance(provider, pools[0].tokenVaultAKeypair.publicKey)); + const postVaultBalanceTwoB = new BN(await getTokenBalance(provider, pools[0].tokenVaultBKeypair.publicKey)); + const postOwnerAccountBalanceInput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountInput)); + const postOwnerAccountBalanceOutput = new BN(await getTokenBalance(provider, baseIxParams.tokenOwnerAccountOutput)); + + assert.ok(postVaultBalanceOneA.sub(preVaultBalanceOneA).eq(expectedVaultAccountOneADelta)); + assert.ok(postVaultBalanceOneB.sub(preVaultBalanceOneB).eq(expectedVaultAccountOneBDelta)); + assert.ok(postVaultBalanceTwoA.sub(preVaultBalanceTwoA).eq(expectedVaultAccountTwoADelta)); + assert.ok(postVaultBalanceTwoB.sub(preVaultBalanceTwoB).eq(expectedVaultAccountTwoBDelta)); + assert.ok(postOwnerAccountBalanceInput.sub(preOwnerAccountBalanceInput).eq(expectedOwnerAccountInputDelta)); + assert.ok(postOwnerAccountBalanceOutput.sub(preOwnerAccountBalanceOutput).eq(expectedOwnerAccountOutputDelta)); + + //console.log(`aToB: ${aToBTwo} ${aToBOne}`); + //console.log("out", transferFeeIncludedOutputAmount.amount.toString(), transferFeeIncludedOutputAmount.fee.toString()); + //console.log("midin", transferFeeIncludedMidInputAmount.amount.toString(), transferFeeIncludedMidInputAmount.fee.toString()); + //console.log("midout", transferFeeIncludedMidOutputAmount.amount.toString(), transferFeeIncludedMidOutputAmount.fee.toString()); + //console.log("in", transferFeeIncludedInputAmount.amount.toString(), transferFeeIncludedInputAmount.fee.toString()); + //console.log("q2", quote2.estimatedAmountIn.toString(), quote2.estimatedAmountOut.toString()); + //console.log("q1", quote.estimatedAmountIn.toString(), quote.estimatedAmountOut.toString()); + }); + }); + }); + }); + + describe("Special cases", () => { + // We know that all transfers are executed 2 functions depending on the direction, so 2 test cases. + + let fixture: WhirlpoolTestFixtureV2; + beforeEach(async () => { + const mintAmount = new BN(2_000_000_000); + const tickSpacing = 1; + const rangeLowerTickIndex = -64; + const rangeUpperTickIndex = +64; + const currentTickIndex = 0; + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currentTickIndex, + rangeLowerTickIndex, + rangeUpperTickIndex, + { + // half deposit (50:50) + tokenA: mintAmount.divn(2), + tokenB: mintAmount.divn(2), + } + ); + + fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 500, transferFeeInitialMax: 100_000n}, + tokenTraitB: { isToken2022: true, hasTransferFeeExtension: true, transferFeeInitialBps: 1000, transferFeeInitialMax: 200_000n}, + tickSpacing, + // pool has much liquidity in both direction + positions: [{ + tickLowerIndex: rangeLowerTickIndex, + tickUpperIndex: rangeUpperTickIndex, + liquidityAmount: liquidityAmount + }], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currentTickIndex), + mintAmount, + }); + }); + + describe("use current fee rate even if next fee rate exists", () => { + it("owner to vault", async () => { + // in A to B, owner to vault is input + + const { poolInitInfo, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const tokenA = poolInitInfo.tokenMintA; + const tokenB = poolInitInfo.tokenMintB; + + // fee config is initialized with older = newer state + const initialFeeConfigA = await fetchTransferFeeConfig(tokenA); + assert.equal(initialFeeConfigA.newerTransferFee.transferFeeBasisPoints, 500); + assert.equal(initialFeeConfigA.olderTransferFee.transferFeeBasisPoints, 500); + + const whirlpoolPubkey = poolInitInfo.whirlpoolPda.publicKey; + + let whirlpoolData = (await fetcher.getPool(whirlpoolPubkey, IGNORE_CACHE)) as WhirlpoolData; + + const aToB = true; + const inputAmount = new BN(1_000_000); + const transferFeeA = getEpochFee(initialFeeConfigA, BigInt(await getCurrentEpoch())); + const transferFeeExcludedInputAmount = calculateTransferFeeExcludedAmount(transferFeeA, inputAmount); + + // non zero, but not limited by maximum + assert.ok(transferFeeExcludedInputAmount.fee.gtn(0)); + assert.ok(transferFeeExcludedInputAmount.fee.lt(new BN(transferFeeA.maximumFee.toString()))); + + const quote = swapQuoteWithParams( + { + // A --> B, ExactIn + amountSpecifiedIsInput: true, + aToB, + tokenAmount: transferFeeExcludedInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolPubkey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), // 0% slippage + ); + + const tx = toTx(ctx, WhirlpoolIx.swapV2Ix(ctx.program, { + ...quote, + amount: inputAmount, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), // not interested in this case + + whirlpool: whirlpoolPubkey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: PDAUtil.getOracle(ctx.program.programId, whirlpoolPubkey).publicKey, + })); + + // PREPEND setTransferFee ix + tx.prependInstruction({ + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + tokenA, + 2000, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ) + ] + }); + + const preWithheldAmount = await fetchTransferFeeWithheldAmount(poolInitInfo.tokenVaultAKeypair.publicKey); + await tx.buildAndExecute(); + const postWithheldAmount = await fetchTransferFeeWithheldAmount(poolInitInfo.tokenVaultAKeypair.publicKey); + + // fee is based on the current bps + const withheldDelta = postWithheldAmount.sub(preWithheldAmount); + assert.ok(withheldDelta.eq(transferFeeExcludedInputAmount.fee)); + + // but newer fee bps have been updated + const updatedFeeConfigA = await fetchTransferFeeConfig(tokenA); + assert.equal(updatedFeeConfigA.newerTransferFee.transferFeeBasisPoints, 2000); + assert.equal(updatedFeeConfigA.olderTransferFee.transferFeeBasisPoints, 500); + }); + + it("vault to owner", async () => { + // in A to B, vault to owner is output + + const { poolInitInfo, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const tokenA = poolInitInfo.tokenMintA; + const tokenB = poolInitInfo.tokenMintB; + + // fee config is initialized with older = newer state + const initialFeeConfigB = await fetchTransferFeeConfig(tokenB); + assert.equal(initialFeeConfigB.newerTransferFee.transferFeeBasisPoints, 1000); + assert.equal(initialFeeConfigB.olderTransferFee.transferFeeBasisPoints, 1000); + + const whirlpoolPubkey = poolInitInfo.whirlpoolPda.publicKey; + + let whirlpoolData = (await fetcher.getPool(whirlpoolPubkey, IGNORE_CACHE)) as WhirlpoolData; + + const aToB = true; + const inputAmount = new BN(1_000_000); + const feeConfigA = await fetchTransferFeeConfig(tokenA); + const transferFeeA = getEpochFee(feeConfigA, BigInt(await getCurrentEpoch())); + const transferFeeExcludedInputAmount = calculateTransferFeeExcludedAmount(transferFeeA, inputAmount); + + const quote = swapQuoteWithParams( + { + // A --> B, ExactIn + amountSpecifiedIsInput: true, + aToB, + tokenAmount: transferFeeExcludedInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolPubkey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), // 0% slippage + ); + + const transferFeeB = getEpochFee(initialFeeConfigB, BigInt(await getCurrentEpoch())); + const transferFeeExcludedOutputAmount = calculateTransferFeeExcludedAmount(transferFeeB, quote.estimatedAmountOut); + + // non zero, but not limited by maximum + assert.ok(transferFeeExcludedOutputAmount.fee.gtn(0)); + assert.ok(transferFeeExcludedOutputAmount.fee.lt(new BN(transferFeeB.maximumFee.toString()))); + + const tx = toTx(ctx, WhirlpoolIx.swapV2Ix(ctx.program, { + ...quote, + amount: inputAmount, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), // not interested in this case + + whirlpool: whirlpoolPubkey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: PDAUtil.getOracle(ctx.program.programId, whirlpoolPubkey).publicKey, + })); + + // PREPEND setTransferFee ix + tx.prependInstruction({ + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + tokenB, + 1500, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ) + ] + }); + + const preWithheldAmount = await fetchTransferFeeWithheldAmount(tokenAccountB); + await tx.buildAndExecute(); + const postWithheldAmount = await fetchTransferFeeWithheldAmount(tokenAccountB); + + // fee is based on the current bps + const withheldDelta = postWithheldAmount.sub(preWithheldAmount); + assert.ok(withheldDelta.eq(transferFeeExcludedOutputAmount.fee)); + + // but newer fee bps have been updated + const updatedFeeConfigB = await fetchTransferFeeConfig(tokenB); + assert.equal(updatedFeeConfigB.newerTransferFee.transferFeeBasisPoints, 1500); + assert.equal(updatedFeeConfigB.olderTransferFee.transferFeeBasisPoints, 1000); + }); + }); + + describe("use updated fee rate once epoch comes", () => { + it("owner to vault", async () => { + // in A to B, owner to vault is input + + const { poolInitInfo, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const tokenA = poolInitInfo.tokenMintA; + const tokenB = poolInitInfo.tokenMintB; + + // fee config is initialized with older = newer state + const initialFeeConfigA = await fetchTransferFeeConfig(tokenA); + assert.equal(initialFeeConfigA.newerTransferFee.transferFeeBasisPoints, 500); + assert.equal(initialFeeConfigA.olderTransferFee.transferFeeBasisPoints, 500); + + const newBpsList = [2000, 3000]; + let oldBps = 500; + for (let i = 0; i < newBpsList.length; i++) { + const newBps = newBpsList[i]; + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + tokenA, + newBps, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ) + ] + }).buildAndExecute(); + + const updatedFeeConfigA = await fetchTransferFeeConfig(tokenA); + assert.equal(updatedFeeConfigA.newerTransferFee.transferFeeBasisPoints, newBps); + assert.equal(updatedFeeConfigA.olderTransferFee.transferFeeBasisPoints, oldBps); + + // wait for epoch to enable updated fee rate + await waitEpoch(Number(updatedFeeConfigA.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigA.newerTransferFee.epoch); + + const whirlpoolPubkey = poolInitInfo.whirlpoolPda.publicKey; + + const whirlpoolData = (await fetcher.getPool(whirlpoolPubkey, IGNORE_CACHE)) as WhirlpoolData; + + const aToB = true; + const inputAmount = new BN(1_000_000); + const transferFeeA = getEpochFee(updatedFeeConfigA, BigInt(await getCurrentEpoch())); + assert.ok(transferFeeA.transferFeeBasisPoints === newBps); + + // non zero, but not limited by maximum + const transferFeeExcludedInputAmount = calculateTransferFeeExcludedAmount(transferFeeA, inputAmount); + assert.ok(transferFeeExcludedInputAmount.fee.gtn(0)); + assert.ok(transferFeeExcludedInputAmount.fee.lt(new BN(transferFeeA.maximumFee.toString()))); + + const quote = swapQuoteWithParams( + { + // A --> B, ExactIn + amountSpecifiedIsInput: true, + aToB, + tokenAmount: transferFeeExcludedInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolPubkey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), // 0% slippage + ); + + const tx = toTx(ctx, WhirlpoolIx.swapV2Ix(ctx.program, { + ...quote, + amount: inputAmount, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), // not interested in this case + + whirlpool: whirlpoolPubkey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: PDAUtil.getOracle(ctx.program.programId, whirlpoolPubkey).publicKey, + })); + + const preWithheldAmount = await fetchTransferFeeWithheldAmount(poolInitInfo.tokenVaultAKeypair.publicKey); + await tx.buildAndExecute(); + const postWithheldAmount = await fetchTransferFeeWithheldAmount(poolInitInfo.tokenVaultAKeypair.publicKey); + + // fee is based on the current bps + const withheldDelta = postWithheldAmount.sub(preWithheldAmount); + assert.ok(withheldDelta.eq(transferFeeExcludedInputAmount.fee)); + + oldBps = newBps; + } + }); + + it("vault to owner", async () => { + const { poolInitInfo, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const tokenA = poolInitInfo.tokenMintA; + const tokenB = poolInitInfo.tokenMintB; + + // fee config is initialized with older = newer state + const initialFeeConfigB = await fetchTransferFeeConfig(tokenB); + assert.equal(initialFeeConfigB.newerTransferFee.transferFeeBasisPoints, 1000); + assert.equal(initialFeeConfigB.olderTransferFee.transferFeeBasisPoints, 1000); + + const newBpsList = [2000, 3000]; + let oldBps = 1000; + for (let i = 0; i < newBpsList.length; i++) { + const newBps = newBpsList[i]; + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + tokenB, + newBps, + BigInt(U64_MAX.toString()), + provider.wallet.publicKey, + ) + ] + }).buildAndExecute(); + + const updatedFeeConfigB = await fetchTransferFeeConfig(tokenB); + assert.equal(updatedFeeConfigB.newerTransferFee.transferFeeBasisPoints, newBps); + assert.equal(updatedFeeConfigB.olderTransferFee.transferFeeBasisPoints, oldBps); + + // wait for epoch to enable updated fee rate + await waitEpoch(Number(updatedFeeConfigB.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigB.newerTransferFee.epoch); + + const whirlpoolPubkey = poolInitInfo.whirlpoolPda.publicKey; + + const whirlpoolData = (await fetcher.getPool(whirlpoolPubkey, IGNORE_CACHE)) as WhirlpoolData; + + const aToB = true; + const inputAmount = new BN(1_000_000); + const feeConfigA = await fetchTransferFeeConfig(tokenA); + const transferFeeA = getEpochFee(feeConfigA, BigInt(await getCurrentEpoch())); + const transferFeeExcludedInputAmount = calculateTransferFeeExcludedAmount(transferFeeA, inputAmount); + + const quote = swapQuoteWithParams( + { + // A --> B, ExactIn + amountSpecifiedIsInput: true, + aToB, + tokenAmount: transferFeeExcludedInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolPubkey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), // 0% slippage + ); + + const transferFeeB = getEpochFee(updatedFeeConfigB, BigInt(await getCurrentEpoch())); + const transferFeeExcludedOutputAmount = calculateTransferFeeExcludedAmount(transferFeeB, quote.estimatedAmountOut); + + // non zero, but not limited by maximum + assert.ok(transferFeeExcludedOutputAmount.fee.gtn(0)); + assert.ok(transferFeeExcludedOutputAmount.fee.lt(new BN(transferFeeB.maximumFee.toString()))); + + const tx = toTx(ctx, WhirlpoolIx.swapV2Ix(ctx.program, { + ...quote, + amount: inputAmount, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), // not interested in this case + + whirlpool: whirlpoolPubkey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: PDAUtil.getOracle(ctx.program.programId, whirlpoolPubkey).publicKey, + })); + + const preWithheldAmount = await fetchTransferFeeWithheldAmount(tokenAccountB); + await tx.buildAndExecute(); + const postWithheldAmount = await fetchTransferFeeWithheldAmount(tokenAccountB); + + // fee is based on the current bps + const withheldDelta = postWithheldAmount.sub(preWithheldAmount); + assert.ok(withheldDelta.eq(transferFeeExcludedOutputAmount.fee)); + + oldBps = newBps; + } + }); + }); + + describe("use maximum limit", () => { + it("owner to vault", async () => { + // in A to B, owner to vault is input + + const { poolInitInfo, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const tokenA = poolInitInfo.tokenMintA; + const tokenB = poolInitInfo.tokenMintB; + + // fee config is initialized with older = newer state + const initialFeeConfigA = await fetchTransferFeeConfig(tokenA); + assert.equal(initialFeeConfigA.newerTransferFee.maximumFee, 100_000n); + assert.equal(initialFeeConfigA.olderTransferFee.maximumFee, 100_000n); + + const newMaximumFeeList = [10_000n, 1_000n]; + let oldMaximumFee = 100_000n; + for (let i = 0; i < newMaximumFeeList.length; i++) { + const newMaximumFee = newMaximumFeeList[i]; + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + tokenA, + initialFeeConfigA.newerTransferFee.transferFeeBasisPoints, // no change + newMaximumFee, + provider.wallet.publicKey, + ) + ] + }).buildAndExecute(); + + const updatedFeeConfigA = await fetchTransferFeeConfig(tokenA); + assert.equal(updatedFeeConfigA.newerTransferFee.maximumFee, newMaximumFee); + assert.equal(updatedFeeConfigA.olderTransferFee.maximumFee, oldMaximumFee); + + // wait for epoch to enable updated fee rate + await waitEpoch(Number(updatedFeeConfigA.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigA.newerTransferFee.epoch); + + const whirlpoolPubkey = poolInitInfo.whirlpoolPda.publicKey; + + const whirlpoolData = (await fetcher.getPool(whirlpoolPubkey, IGNORE_CACHE)) as WhirlpoolData; + + const aToB = true; + const inputAmount = new BN(1_000_000); + const transferFeeA = getEpochFee(updatedFeeConfigA, BigInt(await getCurrentEpoch())); + assert.ok(transferFeeA.maximumFee === newMaximumFee); + + // non zero, and limited by maximum + const transferFeeExcludedInputAmount = calculateTransferFeeExcludedAmount(transferFeeA, inputAmount); + assert.ok(transferFeeExcludedInputAmount.fee.gtn(0)); + assert.ok(transferFeeExcludedInputAmount.fee.eq(new BN(transferFeeA.maximumFee.toString()))); + + const quote = swapQuoteWithParams( + { + // A --> B, ExactIn + amountSpecifiedIsInput: true, + aToB, + tokenAmount: transferFeeExcludedInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolPubkey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), // 0% slippage + ); + + const tx = toTx(ctx, WhirlpoolIx.swapV2Ix(ctx.program, { + ...quote, + amount: inputAmount, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), // not interested in this case + + whirlpool: whirlpoolPubkey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: PDAUtil.getOracle(ctx.program.programId, whirlpoolPubkey).publicKey, + })); + + const preWithheldAmount = await fetchTransferFeeWithheldAmount(poolInitInfo.tokenVaultAKeypair.publicKey); + await tx.buildAndExecute(); + const postWithheldAmount = await fetchTransferFeeWithheldAmount(poolInitInfo.tokenVaultAKeypair.publicKey); + + // fee is based on the current maximum + const withheldDelta = postWithheldAmount.sub(preWithheldAmount); + assert.ok(withheldDelta.eq(transferFeeExcludedInputAmount.fee)); + + oldMaximumFee = newMaximumFee; + } + }); + + it("vault to owner", async () => { + const { poolInitInfo, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const tokenA = poolInitInfo.tokenMintA; + const tokenB = poolInitInfo.tokenMintB; + + // fee config is initialized with older = newer state + const initialFeeConfigB = await fetchTransferFeeConfig(tokenB); + assert.equal(initialFeeConfigB.newerTransferFee.maximumFee, 200_000n); + assert.equal(initialFeeConfigB.olderTransferFee.maximumFee, 200_000n); + + const newMaximumFeeList = [10_000n, 1_000n]; + let oldMaximumFee = 200_000n; + for (let i = 0; i < newMaximumFeeList.length; i++) { + const newMaximumFee = newMaximumFeeList[i]; + + // update fee config + await toTx(ctx, { + cleanupInstructions: [], + signers: [], // provider.wallet is authority & payer + instructions: [ + createSetTransferFeeInstruction( + tokenB, + initialFeeConfigB.newerTransferFee.transferFeeBasisPoints, // no change + newMaximumFee, + provider.wallet.publicKey, + ) + ] + }).buildAndExecute(); + + const updatedFeeConfigB = await fetchTransferFeeConfig(tokenB); + assert.equal(updatedFeeConfigB.newerTransferFee.maximumFee, newMaximumFee); + assert.equal(updatedFeeConfigB.olderTransferFee.maximumFee, oldMaximumFee); + + // wait for epoch to enable updated fee rate + await waitEpoch(Number(updatedFeeConfigB.newerTransferFee.epoch)); + assert.ok((await getCurrentEpoch()) >= updatedFeeConfigB.newerTransferFee.epoch); + + const whirlpoolPubkey = poolInitInfo.whirlpoolPda.publicKey; + + const whirlpoolData = (await fetcher.getPool(whirlpoolPubkey, IGNORE_CACHE)) as WhirlpoolData; + + const aToB = true; + const inputAmount = new BN(1_000_000); + const feeConfigA = await fetchTransferFeeConfig(tokenA); + const transferFeeA = getEpochFee(feeConfigA, BigInt(await getCurrentEpoch())); + const transferFeeExcludedInputAmount = calculateTransferFeeExcludedAmount(transferFeeA, inputAmount); + + const quote = swapQuoteWithParams( + { + // A --> B, ExactIn + amountSpecifiedIsInput: true, + aToB, + tokenAmount: transferFeeExcludedInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolPubkey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), // 0% slippage + ); + + const transferFeeB = getEpochFee(updatedFeeConfigB, BigInt(await getCurrentEpoch())); + const transferFeeExcludedOutputAmount = calculateTransferFeeExcludedAmount(transferFeeB, quote.estimatedAmountOut); + + // non zero, and limited by maximum + assert.ok(transferFeeExcludedOutputAmount.fee.gtn(0)); + assert.ok(transferFeeExcludedOutputAmount.fee.eq(new BN(transferFeeB.maximumFee.toString()))); + + const tx = toTx(ctx, WhirlpoolIx.swapV2Ix(ctx.program, { + ...quote, + amount: inputAmount, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), // not interested in this case + + whirlpool: whirlpoolPubkey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: PDAUtil.getOracle(ctx.program.programId, whirlpoolPubkey).publicKey, + })); + + const preWithheldAmount = await fetchTransferFeeWithheldAmount(tokenAccountB); + await tx.buildAndExecute(); + const postWithheldAmount = await fetchTransferFeeWithheldAmount(tokenAccountB); + + // fee is based on the current maximum + const withheldDelta = postWithheldAmount.sub(preWithheldAmount); + assert.ok(withheldDelta.eq(transferFeeExcludedOutputAmount.fee)); + + oldMaximumFee = newMaximumFee; + } + }); + }); + + it("logging applied TransferFee config info", async () => { + const { poolInitInfo, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const tokenA = poolInitInfo.tokenMintA; + const tokenB = poolInitInfo.tokenMintB; + + const feeConfigA = await fetchTransferFeeConfig(tokenA); + const feeConfigB = await fetchTransferFeeConfig(tokenB); + const transferFeeA = getEpochFee(feeConfigA, BigInt(await getCurrentEpoch())); + const transferFeeB = getEpochFee(feeConfigB, BigInt(await getCurrentEpoch())); + + const whirlpoolPubkey = poolInitInfo.whirlpoolPda.publicKey; + + let whirlpoolData = (await fetcher.getPool(whirlpoolPubkey, IGNORE_CACHE)) as WhirlpoolData; + + const aToB = true; + const inputAmount = new BN(1_000_000); + const transferFeeExcludedInputAmount = calculateTransferFeeExcludedAmount(transferFeeA, inputAmount); + + const quote = swapQuoteWithParams( + { + // A --> B, ExactIn + amountSpecifiedIsInput: true, + aToB, + tokenAmount: transferFeeExcludedInputAmount.amount, + + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpoolPubkey, + fetcher, + IGNORE_CACHE, + ), + tokenExtensionCtx: withNoExtension, // TransferFee is taken into account later + }, + Percentage.fromFraction(0, 100), // 0% slippage + ); + + const sig = await toTx(ctx, WhirlpoolIx.swapV2Ix(ctx.program, { + ...quote, + amount: inputAmount, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), // not interested in this case + + whirlpool: whirlpoolPubkey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: PDAUtil.getOracle(ctx.program.programId, whirlpoolPubkey).publicKey, + })).buildAndExecute(); + + const parsedTx = await provider.connection.getParsedTransaction( + sig, + {maxSupportedTransactionVersion: 0} + ); + + assert.ok(parsedTx?.meta?.innerInstructions); + assert.ok(parsedTx!.meta!.innerInstructions.length === 1); // twoHopSwap only (top-level ix) + const memoLogs = parsedTx!.meta!.innerInstructions[0].instructions + .filter((ix) => ix.programId.equals(MEMO_PROGRAM_ADDRESS)); + + assert.ok(memoLogs.length === 2); + assert.ok((memoLogs[0] as any).parsed === `TFe: ${transferFeeA.transferFeeBasisPoints}, ${transferFeeA.maximumFee}`); + assert.ok((memoLogs[1] as any).parsed === `TFe: ${transferFeeB.transferFeeBasisPoints}, ${transferFeeB.maximumFee}`); + }); + }); +}); diff --git a/sdk/tests/integration/v2/token-extensions/transfer-hook.test.ts b/sdk/tests/integration/v2/token-extensions/transfer-hook.test.ts new file mode 100644 index 000000000..b43febafb --- /dev/null +++ b/sdk/tests/integration/v2/token-extensions/transfer-hook.test.ts @@ -0,0 +1,2411 @@ +import * as anchor from "@coral-xyz/anchor"; +import { BN } from "@coral-xyz/anchor"; +import { MathUtil, PDA, Percentage } from "@orca-so/common-sdk"; +import * as assert from "assert"; +import Decimal from "decimal.js"; +import { + buildWhirlpoolClient, + collectRewardsQuote, + DecreaseLiquidityQuote, + decreaseLiquidityQuoteByLiquidityWithParams, + InitPoolV2Params, + MEMO_PROGRAM_ADDRESS, + NUM_REWARDS, + PDAUtil, + PoolUtil, + PositionData, + PriceMath, + SwapQuote, + swapQuoteWithParams, + SwapUtils, + toTokenAmount, + toTx, + twoHopSwapQuoteFromSwapQuotes, + TwoHopSwapV2Params, + WhirlpoolContext, + WhirlpoolData, + WhirlpoolIx, +} from "../../../../src"; +import { IGNORE_CACHE } from "../../../../src/network/public/fetcher"; +import { + getTokenBalance, + sleep, + TickSpacing, + ZERO_BN, +} from "../../../utils"; +import { defaultConfirmOptions } from "../../../utils/const"; +import { WhirlpoolTestFixtureV2 } from "../../../utils/v2/fixture-v2"; +import { + FundedPositionV2Params, + fundPositionsV2, + initTestPoolWithTokensV2, + useMaxCU, +} from "../../../utils/v2/init-utils-v2"; +import { + createTokenAccountV2, +} from "../../../utils/v2/token-2022"; +import { AccountMeta, ComputeBudgetProgram, PublicKey } from "@solana/web3.js"; +import { initTickArrayRange } from "../../../utils/init-utils"; +import { + InitAquariumV2Params, + buildTestAquariumsV2, + getDefaultAquariumV2, + getTokenAccsForPoolsV2, +} from "../../../utils/v2/aquarium-v2"; +import { getExtraAccountMetasForTestTransferHookProgram, getTestTransferHookCounter, updateTransferHookProgram } from "../../../utils/v2/test-transfer-hook-program"; +import { TokenExtensionUtil } from "../../../../src/utils/public/token-extension-util"; +import { RemainingAccountsBuilder, RemainingAccountsType } from "../../../../src/utils/remaining-accounts-util"; + +describe("TokenExtension/TransferHook", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + const client = buildWhirlpoolClient(ctx); + + describe("collect_fees_v2, collect_protocol_fees_v2", () => { + let fixture: WhirlpoolTestFixtureV2; + let feeAccountA: PublicKey; + let feeAccountB: PublicKey; + let tokenTransferHookAccountsA: AccountMeta[] | undefined; + let tokenTransferHookAccountsB: AccountMeta[] | undefined; + + beforeEach(async () => { + // In same tick array - start index 22528 + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + + const tickSpacing = TickSpacing.Standard; + fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true, hasTransferHookExtension: true }, + tokenTraitB: { isToken2022: true, hasTransferHookExtension: true }, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }, // In range position + { tickLowerIndex: 0, tickUpperIndex: 128, liquidityAmount: new anchor.BN(1_000_000) }, // Out of range position + ], + }); + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); + + const tickArrayPda = PDAUtil.getTickArray( + ctx.program.programId, + whirlpoolPda.publicKey, + 22528 + ); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + // TransferHook + tokenTransferHookAccountsA = await getExtraAccountMetasForTestTransferHookProgram(provider, tokenMintA); + tokenTransferHookAccountsB = await getExtraAccountMetasForTestTransferHookProgram(provider, tokenMintB); + + // Accrue fees in token A + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + tokenTransferHookAccountsA, // TransferHook + tokenTransferHookAccountsB, // TransferHook + }) + ).prependInstruction(useMaxCU()).buildAndExecute(); + + // Accrue fees in token B + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(5)), + amountSpecifiedIsInput: true, + aToB: false, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + tokenTransferHookAccountsA, // TransferHook + tokenTransferHookAccountsB, // TransferHook + }) + ).prependInstruction(useMaxCU()).buildAndExecute(); + + await toTx( + ctx, + WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + position: positions[0].publicKey, + tickArrayLower: tickArrayPda.publicKey, + tickArrayUpper: tickArrayPda.publicKey, + }) + ).buildAndExecute(); + + const whirlpoolData = (await fetcher.getPool(whirlpoolPda.publicKey, IGNORE_CACHE))!; + assert.ok(!whirlpoolData.protocolFeeOwedA.isZero()); + assert.ok(!whirlpoolData.protocolFeeOwedB.isZero()); + + const positionBeforeCollect = (await fetcher.getPosition( + positions[0].publicKey, + IGNORE_CACHE + )) as PositionData; + assert.ok(!positionBeforeCollect.feeOwedA.isZero()); + assert.ok(!positionBeforeCollect.feeOwedB.isZero()); + + feeAccountA = await createTokenAccountV2( + provider, + { isToken2022: true }, + tokenMintA, + provider.wallet.publicKey + ); + feeAccountB = await createTokenAccountV2( + provider, + { isToken2022: true }, + tokenMintB, + provider.wallet.publicKey + ); + }); + + it("collect_fees_v2: with transfer hook", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + const preCounterA = await getTestTransferHookCounter(provider, tokenMintA); + const preCounterB = await getTestTransferHookCounter(provider, tokenMintB); + + await toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenTransferHookAccountsA, // TransferHook + tokenTransferHookAccountsB, // TransferHook + }) + ).buildAndExecute(); + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(feeBalanceA).gtn(0)); + assert.ok(new BN(feeBalanceB).gtn(0)); + + const postCounterA = await getTestTransferHookCounter(provider, tokenMintA); + const postCounterB = await getTestTransferHookCounter(provider, tokenMintB); + assert.equal(postCounterA, preCounterA + 1); + assert.equal(postCounterB, preCounterB + 1); + }); + + it("collect_fees_v2: without transfer hook (has extension, but set null)", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + const preCounterA = await getTestTransferHookCounter(provider, tokenMintA); + const preCounterB = await getTestTransferHookCounter(provider, tokenMintB); + + await updateTransferHookProgram(provider, tokenMintA, PublicKey.default); + await updateTransferHookProgram(provider, tokenMintB, PublicKey.default); + + const sig = await toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenTransferHookAccountsA: undefined, // TransferHook + tokenTransferHookAccountsB: undefined, // TransferHook + }) + ).buildAndExecute(); + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(feeBalanceA).gtn(0)); + assert.ok(new BN(feeBalanceB).gtn(0)); + + const postCounterA = await getTestTransferHookCounter(provider, tokenMintA); + const postCounterB = await getTestTransferHookCounter(provider, tokenMintB); + + assert.equal(postCounterA, preCounterA); + assert.equal(postCounterB, preCounterB); + }); + + it("collect_fees_v2: [Fail] with transfer hook, but no extra accounts provided for A", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenTransferHookAccountsA: undefined, // TransferHook (not provided) + tokenTransferHookAccountsB, // TransferHook + }) + ).buildAndExecute(), + /0x17a2/ // NoExtraAccountsForTransferHook + ); + }); + + it("collect_fees_v2: [Fail] with transfer hook, but no extra accounts provided for B", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenTransferHookAccountsA, // TransferHook + tokenTransferHookAccountsB: undefined, // TransferHook (not provided) + }) + ).buildAndExecute(), + /0x17a2/ // NoExtraAccountsForTransferHook + ); + }); + + it("collect_fees_v2: [Fail] with transfer hook, but extra accounts provided for A is insufficient(counter)", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + // counter account is missing + const insufficientTransferHookAccountsA = tokenTransferHookAccountsA!.slice(1); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenTransferHookAccountsA: insufficientTransferHookAccountsA, + tokenTransferHookAccountsB, // TransferHook + }) + ).buildAndExecute(), + // Errors on tlv-account-resolution + // https://github.com/solana-labs/solana-program-library/blob/dbf609206a60ed5698644f4840ddbd117d2c83d8/libraries/tlv-account-resolution/src/error.rs#L6 + /0xa261c2c0/ // IncorrectAccount (2724315840) + ); + }); + + it("collect_fees_v2: [Fail] with transfer hook, but extra accounts provided for A is insufficient(ExtraAccountMetas)", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + // ExtraAccountMetas is missing + const insufficientTransferHookAccountsA = [ + ...tokenTransferHookAccountsA!.slice(0,1), + ...tokenTransferHookAccountsA!.slice(2) + ]; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenTransferHookAccountsA: insufficientTransferHookAccountsA, + tokenTransferHookAccountsB, // TransferHook + }) + ).buildAndExecute(), + // Errors on transfer-hook-interface + // https://github.com/solana-labs/solana-program-library/blob/dbf609206a60ed5698644f4840ddbd117d2c83d8/token/transfer-hook/interface/src/error.rs#L6 + /0x7dc8348c/ // IncorrectAccount (2110272652) + ); + }); + + it("collect_fees_v2: [Fail] with transfer hook, but extra accounts provided for A is insufficient(HookProgram)", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + // HookProgram is missing + const insufficientTransferHookAccountsA = tokenTransferHookAccountsA!.slice(0,2); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenTransferHookAccountsA: insufficientTransferHookAccountsA, + tokenTransferHookAccountsB, // TransferHook + }) + ).buildAndExecute(), + // Errors on transfer-hook-interface + // https://github.com/solana-labs/solana-program-library/blob/dbf609206a60ed5698644f4840ddbd117d2c83d8/token/transfer-hook/interface/src/error.rs#L6 + /0x7dc8348c/ // IncorrectAccount (2110272652) + ); + }); + + it("collect_fees_v2: [Fail] with TransferHookReward", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + const [remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder() + .addSlice(RemainingAccountsType.TransferHookA, tokenTransferHookAccountsA) + .addSlice(RemainingAccountsType.TransferHookB, tokenTransferHookAccountsB) + // invalid accounts type + .addSlice(RemainingAccountsType.TransferHookReward, tokenTransferHookAccountsA) + .build(); + + const ix = ctx.program.instruction.collectFeesV2(remainingAccountsInfo, { + accounts: { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + memoProgram: MEMO_PROGRAM_ADDRESS, + }, + remainingAccounts, + }); + + await assert.rejects( + toTx( + ctx, + { + instructions: [ix], + cleanupInstructions: [], + signers: [], + } + ).buildAndExecute(), + /0x17a0/ // RemainingAccountsInvalidSlice + ); + }); + + it("collect_fees_v2: [Fail] with insufficient remaining accounts", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + const [remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder() + .addSlice(RemainingAccountsType.TransferHookA, tokenTransferHookAccountsA) + .addSlice(RemainingAccountsType.TransferHookB, tokenTransferHookAccountsB) + .build(); + + const missingRemainingAccounts = remainingAccounts!.slice(0, remainingAccounts!.length - 1); // drop last 1 accounts + + const ix = ctx.program.instruction.collectFeesV2(remainingAccountsInfo, { + accounts: { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + memoProgram: MEMO_PROGRAM_ADDRESS, + }, + remainingAccounts: missingRemainingAccounts, + }); + + await assert.rejects( + toTx( + ctx, + { + instructions: [ix], + cleanupInstructions: [], + signers: [], + } + ).buildAndExecute(), + /0x17a1/ // RemainingAccountsInsufficient + ); + }); + + it("collect_fees_v2: [Fail] with duplicated remaining accounts (TransferHookA)", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + const [remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder() + .addSlice(RemainingAccountsType.TransferHookA, tokenTransferHookAccountsA) + .addSlice(RemainingAccountsType.TransferHookB, tokenTransferHookAccountsB) + // duplicated + .addSlice(RemainingAccountsType.TransferHookA, tokenTransferHookAccountsA) + .build(); + + const ix = ctx.program.instruction.collectFeesV2(remainingAccountsInfo, { + accounts: { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + memoProgram: MEMO_PROGRAM_ADDRESS, + }, + remainingAccounts, + }); + + await assert.rejects( + toTx( + ctx, + { + instructions: [ix], + cleanupInstructions: [], + signers: [], + } + ).buildAndExecute(), + /0x17a5/ // RemainingAccountsDuplicatedAccountsType + ); + }); + + it("collect_fees_v2: [Fail] with duplicated remaining accounts (TransferHookB)", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + positions, + } = fixture.getInfos(); + + const [remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder() + .addSlice(RemainingAccountsType.TransferHookA, tokenTransferHookAccountsA) + .addSlice(RemainingAccountsType.TransferHookB, tokenTransferHookAccountsB) + // duplicated + .addSlice(RemainingAccountsType.TransferHookB, tokenTransferHookAccountsB) + .build(); + + const ix = ctx.program.instruction.collectFeesV2(remainingAccountsInfo, { + accounts: { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + memoProgram: MEMO_PROGRAM_ADDRESS, + }, + remainingAccounts, + }); + + await assert.rejects( + toTx( + ctx, + { + instructions: [ix], + cleanupInstructions: [], + signers: [], + } + ).buildAndExecute(), + /0x17a5/ // RemainingAccountsDuplicatedAccountsType + ); + }); + + it("collect_protocol_fees_v2: with transfer hook", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKeypair }, + } = fixture.getInfos(); + + const preCounterA = await getTestTransferHookCounter(provider, tokenMintA); + const preCounterB = await getTestTransferHookCounter(provider, tokenMintB); + + await toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenTransferHookAccountsA, // TransferHook + tokenTransferHookAccountsB, // TransferHook + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(); + const feeBalanceA = await getTokenBalance(provider, feeAccountA); + const feeBalanceB = await getTokenBalance(provider, feeAccountB); + assert.ok(new BN(feeBalanceA).gtn(0)); + assert.ok(new BN(feeBalanceB).gtn(0)); + + const postCounterA = await getTestTransferHookCounter(provider, tokenMintA); + const postCounterB = await getTestTransferHookCounter(provider, tokenMintB); + assert.equal(postCounterA, preCounterA + 1); + assert.equal(postCounterB, preCounterB + 1); + }); + + it("collect_protocol_fees_v2: [Fail] with transfer hook, but no extra accounts provided for A", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKeypair }, + } = fixture.getInfos(); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenTransferHookAccountsA: undefined, // TransferHook (not provided) + tokenTransferHookAccountsB, // TransferHook + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(), + /0x17a2/ // NoExtraAccountsForTransferHook + ); + }); + + it("collect_protocol_fees_v2: [Fail] with transfer hook, but no extra accounts provided for B", async () => { + const { + poolInitInfo: { + whirlpoolPda, + tokenVaultAKeypair, + tokenVaultBKeypair, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + }, + configKeypairs: { collectProtocolFeesAuthorityKeypair }, + configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKeypair }, + } = fixture.getInfos(); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectProtocolFeesV2Ix(ctx.program, { + whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey, + whirlpool: whirlpoolPda.publicKey, + collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey, + tokenMintA, + tokenMintB, + tokenProgramA, + tokenProgramB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenOwnerAccountA: feeAccountA, + tokenOwnerAccountB: feeAccountB, + tokenTransferHookAccountsA, // TransferHook + tokenTransferHookAccountsB: undefined, // TransferHook (not provided) + }) + ) + .addSigner(collectProtocolFeesAuthorityKeypair) + .buildAndExecute(), + /0x17a2/ // NoExtraAccountsForTransferHook + ); + }); + }); + + describe("collect_reward_v2", () => { + let fixture: WhirlpoolTestFixtureV2; + let rewardAccounts: PublicKey[]; + let tokenTransferHookAccounts: (AccountMeta[] | undefined)[]; + + beforeEach(async () => { + const vaultStartBalance = 1_000_000; + const lowerTickIndex = -1280, + upperTickIndex = 1280, + tickSpacing = TickSpacing.Standard; + fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tickSpacing: tickSpacing, + initialSqrtPrice: MathUtil.toX64(new Decimal(1)), + positions: [ + { + tickLowerIndex: lowerTickIndex, + tickUpperIndex: upperTickIndex, + liquidityAmount: new anchor.BN(1_000_000), + }, + ], + rewards: [ + { + rewardTokenTrait: { isToken2022: true, hasTransferHookExtension: true }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: { isToken2022: true, hasTransferHookExtension: true }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: { isToken2022: true, hasTransferHookExtension: true }, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + // accrue rewards + await sleep(3000); + + await toTx( + ctx, + WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + position: positions[0].publicKey, + tickArrayLower: positions[0].tickArrayLower, + tickArrayUpper: positions[0].tickArrayUpper, + }) + ).buildAndExecute(); + + // Generate collect reward expectation + const whirlpoolData = (await fetcher.getPool(whirlpoolPda.publicKey)) as WhirlpoolData; + const positionPreCollect = await client.getPosition(positions[0].publicKey, IGNORE_CACHE); + + // Lock the collectRewards quote to the last time we called updateFeesAndRewards + const expectation = collectRewardsQuote({ + whirlpool: whirlpoolData, + position: positionPreCollect.getData(), + tickLower: positionPreCollect.getLowerTickData(), + tickUpper: positionPreCollect.getUpperTickData(), + timeStampInSeconds: whirlpoolData.rewardLastUpdatedTimestamp, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }); + + // Check that the expectation is not zero + for (let i = 0; i < NUM_REWARDS; i++) { + assert.ok(!expectation.rewardOwed[i]!.isZero()); + } + + rewardAccounts = await Promise.all( + rewards.map((reward) => { + return createTokenAccountV2( + provider, + { isToken2022: true }, + reward.rewardMint, + provider.wallet.publicKey + ); + }) + ); + + tokenTransferHookAccounts = await Promise.all( + rewards.map((reward) => { + return getExtraAccountMetasForTestTransferHookProgram(provider, reward.rewardMint) + }) + ); + }); + + it("collect_reward_v2: with transfer hook", async () => { + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + for (let i = 0; i < NUM_REWARDS; i++) { + const preCounter = await getTestTransferHookCounter(provider, rewards[i].rewardMint); + + await toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[i].rewardMint, + rewardTokenProgram: rewards[i].tokenProgram, + rewardOwnerAccount: rewardAccounts[i], + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + rewardIndex: i, + rewardTransferHookAccounts: tokenTransferHookAccounts[i], // TransferHook + }) + ).buildAndExecute(); + const rewardBalance = await getTokenBalance(provider, rewardAccounts[i]); + assert.ok(new BN(rewardBalance).gtn(0)); + + const postCounter = await getTestTransferHookCounter(provider, rewards[i].rewardMint); + assert.equal(postCounter, preCounter + 1); + } + }); + + it("collect_reward_v2: [Fail] with transfer hook, but no extra accounts provided for rewardToken", async () => { + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + for (let i = 0; i < NUM_REWARDS; i++) { + const preCounter = await getTestTransferHookCounter(provider, rewards[i].rewardMint); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[i].rewardMint, + rewardTokenProgram: rewards[i].tokenProgram, + rewardOwnerAccount: rewardAccounts[i], + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + rewardIndex: i, + rewardTransferHookAccounts: undefined, // TransferHook (not provided) + }) + ).buildAndExecute(), + /0x17a2/ // NoExtraAccountsForTransferHook + ); + } + }); + + it("collect_reward_v2: [Fail] with duplicated remaining accounts (TransferHookReward)", async () => { + const { + poolInitInfo: { whirlpoolPda }, + positions, + rewards, + } = fixture.getInfos(); + + for (let i = 0; i < NUM_REWARDS; i++) { + const [remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder() + .addSlice(RemainingAccountsType.TransferHookReward, tokenTransferHookAccounts[i]) + // duplicated + .addSlice(RemainingAccountsType.TransferHookReward, tokenTransferHookAccounts[i]) + .build(); + + const ix = ctx.program.instruction.collectRewardV2(i, remainingAccountsInfo, { + accounts: { + whirlpool: whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + rewardMint: rewards[i].rewardMint, + rewardTokenProgram: rewards[i].tokenProgram, + rewardOwnerAccount: rewardAccounts[i], + rewardVault: rewards[i].rewardVaultKeypair.publicKey, + memoProgram: MEMO_PROGRAM_ADDRESS, + }, + remainingAccounts, + }); + + await assert.rejects( + toTx( + ctx, + { + instructions: [ix], + cleanupInstructions: [], + signers: [], + } + ).buildAndExecute(), + /0x17a5/ // RemainingAccountsDuplicatedAccountsType + ); + } + }); + }); + + describe("increase_liquidity_v2", () => { + const tickLowerIndex = 7168; + const tickUpperIndex = 8960; + const currTick = Math.round((tickLowerIndex + tickUpperIndex) / 2); + + let fixture: WhirlpoolTestFixtureV2; + let tokenTransferHookAccountsA: AccountMeta[] | undefined; + let tokenTransferHookAccountsB: AccountMeta[] | undefined; + + beforeEach(async () => { + fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true, hasTransferHookExtension: true}, + tokenTraitB: { isToken2022: true, hasTransferHookExtension: true}, + tickSpacing: TickSpacing.Standard, + positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick), + }); + const { poolInitInfo } = fixture.getInfos(); + + // TransferHook + tokenTransferHookAccountsA = await getExtraAccountMetasForTestTransferHookProgram(provider, poolInitInfo.tokenMintA); + tokenTransferHookAccountsB = await getExtraAccountMetasForTestTransferHookProgram(provider, poolInitInfo.tokenMintB); + }); + + it("increase_liquidity_v2: with transfer hook", async () => { + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 1_000_000); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + const preCounterA = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintA); + const preCounterB = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintB); + + const preVaultBalanceA = await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey); + const preVaultBalanceB = await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey); + + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + tokenTransferHookAccountsA, // TransferHook + tokenTransferHookAccountsB, // TransferHook + }) + ).buildAndExecute(); + + const postVaultBalanceA = await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey); + const postVaultBalanceB = await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey); + assert.ok(new BN(postVaultBalanceA).gt(new BN(preVaultBalanceA))); + assert.ok(new BN(postVaultBalanceB).gt(new BN(preVaultBalanceB))); + + const postCounterA = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintA); + const postCounterB = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintB); + assert.equal(postCounterA, preCounterA + 1); + assert.equal(postCounterB, preCounterB + 1); + }); + + it("increase_liquidity_v2: without transfer hook (has extension, but set null)", async () => { + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 1_000_000); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + const preCounterA = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintA); + const preCounterB = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintB); + + const preVaultBalanceA = await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey); + const preVaultBalanceB = await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey); + + await updateTransferHookProgram(provider, poolInitInfo.tokenMintA, PublicKey.default); + await updateTransferHookProgram(provider, poolInitInfo.tokenMintB, PublicKey.default); + + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + tokenTransferHookAccountsA: undefined, // TransferHook + tokenTransferHookAccountsB: undefined, // TransferHook + }) + ).buildAndExecute(); + + const postVaultBalanceA = await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey); + const postVaultBalanceB = await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey); + assert.ok(new BN(postVaultBalanceA).gt(new BN(preVaultBalanceA))); + assert.ok(new BN(postVaultBalanceB).gt(new BN(preVaultBalanceB))); + + const postCounterA = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintA); + const postCounterB = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintB); + assert.equal(postCounterA, preCounterA); + assert.equal(postCounterB, preCounterB); + }); + + it("increase_liquidity_v2: [Fail] with transfer hook, but no extra accounts provided for A", async () => { + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 1_000_000); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + tokenTransferHookAccountsA: undefined, // TransferHook (not provided) + tokenTransferHookAccountsB, // TransferHook + }) + ).buildAndExecute(), + /0x17a2/ // NoExtraAccountsForTransferHook + ); + }); + + it("increase_liquidity_v2: [Fail] with transfer hook, but no extra accounts provided for B", async () => { + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 1_000_000); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + tokenTransferHookAccountsA, // TransferHook + tokenTransferHookAccountsB: undefined, // TransferHook (not provided) + }) + ).buildAndExecute(), + /0x17a2/ // NoExtraAccountsForTransferHook + ); + }); + + it("increase_liquidity_v2: [Fail] with transfer hook, but extra accounts provided for A is insufficient(counter)", async () => { + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 1_000_000); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + // counter account is missing + const insufficientTransferHookAccountsA = tokenTransferHookAccountsA!.slice(1); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + tokenTransferHookAccountsA: insufficientTransferHookAccountsA, + tokenTransferHookAccountsB, // TransferHook + }) + ).buildAndExecute(), + // Errors on tlv-account-resolution + // https://github.com/solana-labs/solana-program-library/blob/dbf609206a60ed5698644f4840ddbd117d2c83d8/libraries/tlv-account-resolution/src/error.rs#L6 + /0xa261c2c0/ // IncorrectAccount (2724315840) + ); + }); + + it("increase_liquidity_v2: [Fail] with transfer hook, but extra accounts provided for A is insufficient(ExtraAccountMetas)", async () => { + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 1_000_000); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + // ExtraAccountMetas is missing + const insufficientTransferHookAccountsA = [ + ...tokenTransferHookAccountsA!.slice(0,1), + ...tokenTransferHookAccountsA!.slice(2) + ]; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + tokenTransferHookAccountsA: insufficientTransferHookAccountsA, + tokenTransferHookAccountsB, // TransferHook + }) + ).buildAndExecute(), + // Errors on transfer-hook-interface + // https://github.com/solana-labs/solana-program-library/blob/dbf609206a60ed5698644f4840ddbd117d2c83d8/token/transfer-hook/interface/src/error.rs#L6 + /0x7dc8348c/ // IncorrectAccount (2110272652) + ); + }); + + it("increase_liquidity_v2: [Fail] with transfer hook, but extra accounts provided for A is insufficient(HookProgram)", async () => { + const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos(); + const positionInitInfo = positions[0]; + + const tokenAmount = toTokenAmount(1_000_000, 1_000_000); + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currTick, + tickLowerIndex, + tickUpperIndex, + tokenAmount + ); + + // HookProgram is missing + const insufficientTransferHookAccountsA = tokenTransferHookAccountsA!.slice(0,2); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount, + tokenMaxA: tokenAmount.tokenA, + tokenMaxB: tokenAmount.tokenB, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positionInitInfo.publicKey, + positionTokenAccount: positionInitInfo.tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positionInitInfo.tickArrayLower, + tickArrayUpper: positionInitInfo.tickArrayUpper, + tokenTransferHookAccountsA: insufficientTransferHookAccountsA, + tokenTransferHookAccountsB, // TransferHook + }) + ).buildAndExecute(), + // Errors on transfer-hook-interface + // https://github.com/solana-labs/solana-program-library/blob/dbf609206a60ed5698644f4840ddbd117d2c83d8/token/transfer-hook/interface/src/error.rs#L6 + /0x7dc8348c/ // IncorrectAccount (2110272652) + ); + }); + }); + + describe("decrease_liquidity_v2", () => { + let fixture: WhirlpoolTestFixtureV2; + let removalQuote: DecreaseLiquidityQuote; + let destAccountA: PublicKey; + let destAccountB: PublicKey; + let tokenTransferHookAccountsA: AccountMeta[] | undefined; + let tokenTransferHookAccountsB: AccountMeta[] | undefined; + + beforeEach(async () => { + const liquidityAmount = new anchor.BN(1_250_000); + const tickLower = 7168, + tickUpper = 8960; + fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: { isToken2022: true, hasTransferHookExtension: true }, + tokenTraitB: { isToken2022: true, hasTransferHookExtension: true }, + tickSpacing: TickSpacing.Standard, + initialSqrtPrice: MathUtil.toX64(new Decimal(1.48)), + positions: [{ tickLowerIndex: tickLower, tickUpperIndex: tickUpper, liquidityAmount }], + }); + const { poolInitInfo } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + const poolBefore = (await fetcher.getPool( + whirlpoolPda.publicKey, + IGNORE_CACHE + )) as WhirlpoolData; + + removalQuote = decreaseLiquidityQuoteByLiquidityWithParams({ + liquidity: new anchor.BN(1_000_000), + sqrtPrice: poolBefore.sqrtPrice, + slippageTolerance: Percentage.fromFraction(1, 100), + tickCurrentIndex: poolBefore.tickCurrentIndex, + tickLowerIndex: tickLower, + tickUpperIndex: tickUpper, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolBefore, IGNORE_CACHE), + }); + assert.ok(!removalQuote.tokenEstA.isZero()); + assert.ok(!removalQuote.tokenEstB.isZero()); + + destAccountA = await createTokenAccountV2( + provider, + { isToken2022: true }, + poolInitInfo.tokenMintA, + provider.wallet.publicKey + ); + destAccountB = await createTokenAccountV2( + provider, + { isToken2022: true }, + poolInitInfo.tokenMintB, + provider.wallet.publicKey + ); + + // TransferHook + tokenTransferHookAccountsA = await getExtraAccountMetasForTestTransferHookProgram(provider, poolInitInfo.tokenMintA); + tokenTransferHookAccountsB = await getExtraAccountMetasForTestTransferHookProgram(provider, poolInitInfo.tokenMintB); + }); + + it("decrease_liquidity_v2: with transfer hook", async () => { + const { poolInitInfo, positions } = fixture.getInfos(); + + const preCounterA = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintA); + const preCounterB = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintB); + + await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + ...removalQuote, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: destAccountA, + tokenOwnerAccountB: destAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positions[0].tickArrayLower, + tickArrayUpper: positions[0].tickArrayUpper, + tokenTransferHookAccountsA, // TransferHook + tokenTransferHookAccountsB, // TransferHook + }) + ).buildAndExecute(); + const destBalanceA = await getTokenBalance(provider, destAccountA); + const destBalanceB = await getTokenBalance(provider, destAccountB); + assert.ok(new BN(destBalanceA).gtn(0)); + assert.ok(new BN(destBalanceB).gtn(0)); + + const postCounterA = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintA); + const postCounterB = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintB); + assert.equal(postCounterA, preCounterA + 1); + assert.equal(postCounterB, preCounterB + 1); + }); + + it("decrease_liquidity_v2: [Fail] with transfer hook, but no extra accounts provided for A", async () => { + const { poolInitInfo, positions } = fixture.getInfos(); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + ...removalQuote, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: destAccountA, + tokenOwnerAccountB: destAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positions[0].tickArrayLower, + tickArrayUpper: positions[0].tickArrayUpper, + tokenTransferHookAccountsA: undefined, // TransferHook (not provided) + tokenTransferHookAccountsB, // TransferHook + }) + ).buildAndExecute(), + /0x17a2/ // NoExtraAccountsForTransferHook + ); + }); + + it("decrease_liquidity_v2: [Fail] with transfer hook, but no extra accounts provided for B", async () => { + const { poolInitInfo, positions } = fixture.getInfos(); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + ...removalQuote, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + positionAuthority: provider.wallet.publicKey, + position: positions[0].publicKey, + positionTokenAccount: positions[0].tokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: destAccountA, + tokenOwnerAccountB: destAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArrayLower: positions[0].tickArrayLower, + tickArrayUpper: positions[0].tickArrayUpper, + tokenTransferHookAccountsA, // TransferHook + tokenTransferHookAccountsB: undefined, // TransferHook (not provided) + }) + ).buildAndExecute(), + /0x17a2/ // NoExtraAccountsForTransferHook + ); + }); + }); + + describe("swap_v2", () => { + let poolInitInfo: InitPoolV2Params; + let whirlpoolPda: PDA; + let tokenAccountA: PublicKey; + let tokenAccountB: PublicKey; + let oraclePubkey: PublicKey; + let quoteAToB: SwapQuote; + let quoteBToA: SwapQuote; + let tokenTransferHookAccountsA: AccountMeta[] | undefined; + let tokenTransferHookAccountsB: AccountMeta[] | undefined; + + beforeEach(async () => { + const init = await initTestPoolWithTokensV2( + ctx, + { isToken2022: true, hasTransferHookExtension: true }, + { isToken2022: true, hasTransferHookExtension: true }, + TickSpacing.Standard + ); + poolInitInfo = init.poolInitInfo; + whirlpoolPda = init.whirlpoolPda; + tokenAccountA = init.tokenAccountA; + tokenAccountB = init.tokenAccountB; + + const aToB = false; + await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, // to 33792 + 3, + TickSpacing.Standard, + aToB + ); + + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + + await fundPositionsV2(ctx, poolInitInfo, tokenAccountA, tokenAccountB, fundParams); + + oraclePubkey = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey).publicKey; + + const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey; + const whirlpoolData = (await fetcher.getPool(whirlpoolKey, IGNORE_CACHE)) as WhirlpoolData; + + quoteAToB = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: true, + tokenAmount: new BN(100000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(true), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + true, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }, + Percentage.fromFraction(100, 100) // 100% slippage + ); + + quoteBToA = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: false, + tokenAmount: new BN(100000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(false), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + false, + ctx.program.programId, + whirlpoolKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }, + Percentage.fromFraction(100, 100) // 100% slippage + ); + + // TransferHook + tokenTransferHookAccountsA = await getExtraAccountMetasForTestTransferHookProgram(provider, poolInitInfo.tokenMintA); + tokenTransferHookAccountsB = await getExtraAccountMetasForTestTransferHookProgram(provider, poolInitInfo.tokenMintB); + }); + + it("swap_v2: with transfer hook, a to b", async () => { + const preCounterA = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintA); + const preCounterB = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintB); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteAToB, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + tokenTransferHookAccountsA, // TransferHook + tokenTransferHookAccountsB, // TransferHook + }) + ).buildAndExecute(); + + const postCounterA = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintA); + const postCounterB = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintB); + assert.equal(postCounterA, preCounterA + 1); + assert.equal(postCounterB, preCounterB + 1); + }); + + it("swap_v2: with transfer hook, b to a", async () => { + const preCounterA = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintA); + const preCounterB = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintB); + + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteBToA, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + tokenTransferHookAccountsA, // TransferHook + tokenTransferHookAccountsB, // TransferHook + }) + ).buildAndExecute(); + + const postCounterA = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintA); + const postCounterB = await getTestTransferHookCounter(provider, poolInitInfo.tokenMintB); + assert.equal(postCounterA, preCounterA + 1); + assert.equal(postCounterB, preCounterB + 1); + }); + + it("swap_v2: [Fail] with transfer hook, a to b, but no extra accounts provided for A", async () => { + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteAToB, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + tokenTransferHookAccountsA: undefined, // TransferHook (not provided) + tokenTransferHookAccountsB, // TransferHook + }) + ).buildAndExecute(), + /0x17a2/ // NoExtraAccountsForTransferHook + ); + }); + + it("swap_v2: [Fail] with transfer hook, a to b, but no extra accounts provided for B", async () => { + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteAToB, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + tokenTransferHookAccountsA, // TransferHook + tokenTransferHookAccountsB: undefined, // TransferHook (not provided) + }) + ).buildAndExecute(), + /0x17a2/ // NoExtraAccountsForTransferHook + ); + }); + + it("swap_v2: [Fail] with transfer hook, b to a, but no extra accounts provided for A", async () => { + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteBToA, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + tokenTransferHookAccountsA: undefined, // TransferHook (not provided) + tokenTransferHookAccountsB, // TransferHook + }) + ).buildAndExecute(), + /0x17a2/ // NoExtraAccountsForTransferHook + ); + }); + + it("swap_v2: [Fail] with transfer hook, b to a, but no extra accounts provided for B", async () => { + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quoteBToA, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: oraclePubkey, + tokenTransferHookAccountsA, // TransferHook + tokenTransferHookAccountsB: undefined, // TransferHook (not provided) + }) + ).buildAndExecute(), + /0x17a2/ // NoExtraAccountsForTransferHook + ); + }); + }); + + describe("two_hop_swap", () => { + let aqConfig: InitAquariumV2Params; + let baseIxParams: TwoHopSwapV2Params; + let tokenMintIn: PublicKey; + let tokenMintOut: PublicKey; + let tokenMintMid: PublicKey; + let tokenTransferHookAccountsInput: AccountMeta[] | undefined; + let tokenTransferHookAccountsMid: AccountMeta[] | undefined; + let tokenTransferHookAccountsOutput: AccountMeta[] | undefined; + + beforeEach(async () => { + aqConfig = getDefaultAquariumV2(); + // Add a third token and account and a second pool + aqConfig.initMintParams = [ + { tokenTrait: { isToken2022: true, hasTransferHookExtension: true } }, + { tokenTrait: { isToken2022: true, hasTransferHookExtension: true } }, + { tokenTrait: { isToken2022: true, hasTransferHookExtension: true } }, + ]; + aqConfig.initTokenAccParams.push({ mintIndex: 2 }); + aqConfig.initPoolParams.push({ mintIndices: [1, 2], tickSpacing: TickSpacing.Standard }); + + // Add tick arrays and positions + const aToB = false; + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 0, + startTickIndex: 22528, + arrayCount: 3, + aToB, + }); + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 1, + startTickIndex: 22528, + arrayCount: 3, + aToB, + }); + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + aqConfig.initPositionParams.push({ poolIndex: 0, fundParams }); + aqConfig.initPositionParams.push({ poolIndex: 1, fundParams }); + + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [inputToken, intermediaryToken, _outputToken] = mintKeys; + const aToBOne = whirlpoolDataOne.tokenMintA.equals(inputToken); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(intermediaryToken); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: quote.estimatedAmountOut, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const tokenAccKeys = getTokenAccsForPoolsV2(pools, tokenAccounts); + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + baseIxParams = { + ...twoHopQuote, + tokenAuthority: ctx.wallet.publicKey, + whirlpoolOne: pools[0].whirlpoolPda.publicKey, + whirlpoolTwo: pools[1].whirlpoolPda.publicKey, + tokenMintInput: twoHopQuote.aToBOne ? pools[0].tokenMintA : pools[0].tokenMintB, + tokenMintIntermediate: twoHopQuote.aToBOne ? pools[0].tokenMintB : pools[0].tokenMintA, + tokenMintOutput: twoHopQuote.aToBTwo ? pools[1].tokenMintB : pools[1].tokenMintA, + tokenProgramInput: twoHopQuote.aToBOne ? pools[0].tokenProgramA : pools[0].tokenProgramB, + tokenProgramIntermediate: twoHopQuote.aToBOne ? pools[0].tokenProgramB : pools[0].tokenProgramA, + tokenProgramOutput: twoHopQuote.aToBTwo ? pools[1].tokenProgramB : pools[1].tokenProgramA, + tokenOwnerAccountInput: twoHopQuote.aToBOne ? tokenAccKeys[0] : tokenAccKeys[1], + tokenOwnerAccountOutput: twoHopQuote.aToBTwo ? tokenAccKeys[3] : tokenAccKeys[2], + tokenVaultOneInput: twoHopQuote.aToBOne ? pools[0].tokenVaultAKeypair.publicKey : pools[0].tokenVaultBKeypair.publicKey, + tokenVaultOneIntermediate: twoHopQuote.aToBOne ? pools[0].tokenVaultBKeypair.publicKey : pools[0].tokenVaultAKeypair.publicKey, + tokenVaultTwoIntermediate: twoHopQuote.aToBTwo ? pools[1].tokenVaultAKeypair.publicKey : pools[1].tokenVaultBKeypair.publicKey, + tokenVaultTwoOutput: twoHopQuote.aToBTwo ? pools[1].tokenVaultBKeypair.publicKey : pools[1].tokenVaultAKeypair.publicKey, + oracleOne: PDAUtil.getOracle(ctx.program.programId, pools[0].whirlpoolPda.publicKey) + .publicKey, + oracleTwo: PDAUtil.getOracle(ctx.program.programId, pools[1].whirlpoolPda.publicKey) + .publicKey, + }; + + // TransferHook + tokenMintIn = baseIxParams.tokenMintInput; + tokenMintOut = baseIxParams.tokenMintOutput; + tokenMintMid = baseIxParams.tokenMintIntermediate; + tokenTransferHookAccountsInput = await getExtraAccountMetasForTestTransferHookProgram(provider, baseIxParams.tokenMintInput); + tokenTransferHookAccountsMid = await getExtraAccountMetasForTestTransferHookProgram(provider, baseIxParams.tokenMintIntermediate); + tokenTransferHookAccountsOutput = await getExtraAccountMetasForTestTransferHookProgram(provider, baseIxParams.tokenMintOutput); + }); + + it("two_hop_swap_v2: with transfer hook", async () => { + const preCounterIn = await getTestTransferHookCounter(provider, tokenMintIn); + const preCounterOut = await getTestTransferHookCounter(provider, tokenMintOut); + const preCounterMid = await getTestTransferHookCounter(provider, tokenMintMid); + + const tx = toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix( + ctx.program, { + ...baseIxParams, + // TransferHook + tokenTransferHookAccountsInput, + tokenTransferHookAccountsIntermediate: tokenTransferHookAccountsMid, + tokenTransferHookAccountsOutput, + } + ) + ); + + // add Compute units (because it calls 4 external hooks) + tx.prependInstruction(useMaxCU()); + + await tx.buildAndExecute(); + + const postCounterIn = await getTestTransferHookCounter(provider, tokenMintIn); + const postCounterOut = await getTestTransferHookCounter(provider, tokenMintOut); + const postCounterMid = await getTestTransferHookCounter(provider, tokenMintMid); + assert.equal(postCounterIn, preCounterIn + 1); + assert.equal(postCounterOut, preCounterOut + 1); + assert.equal(postCounterMid, preCounterMid + 1 /* must be 1 (vault to vault) */); + }); + + it("two_hop_swap_v2: without transfer hook (has extension, but set null)", async () => { + const preCounterIn = await getTestTransferHookCounter(provider, tokenMintIn); + const preCounterOut = await getTestTransferHookCounter(provider, tokenMintOut); + const preCounterMid = await getTestTransferHookCounter(provider, tokenMintMid); + + await updateTransferHookProgram(provider, tokenMintIn, PublicKey.default); + await updateTransferHookProgram(provider, tokenMintOut, PublicKey.default); + await updateTransferHookProgram(provider, tokenMintMid, PublicKey.default); + + const tx = toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix( + ctx.program, { + ...baseIxParams, + // TransferHook + tokenTransferHookAccountsInput: undefined, + tokenTransferHookAccountsIntermediate: undefined, + tokenTransferHookAccountsOutput: undefined, + } + ) + ); + + // add Compute units (because it calls 4 external hooks) + tx.prependInstruction({ + cleanupInstructions: [], + signers: [], + instructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 400_000 + }) + ] + }); + + await tx.buildAndExecute(); + + const postCounterIn = await getTestTransferHookCounter(provider, tokenMintIn); + const postCounterOut = await getTestTransferHookCounter(provider, tokenMintOut); + const postCounterMid = await getTestTransferHookCounter(provider, tokenMintMid); + assert.equal(postCounterIn, preCounterIn); + assert.equal(postCounterOut, preCounterOut); + assert.equal(postCounterMid, preCounterMid); + }); + + it("two_hop_swap_v2: [Fail] with transfer hook, but no extra accounts provided for tokenInput", async () => { + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix( + ctx.program, { + ...baseIxParams, + // TransferHook + tokenTransferHookAccountsInput: undefined, + tokenTransferHookAccountsIntermediate: tokenTransferHookAccountsMid, + tokenTransferHookAccountsOutput, + } + ) + // add Compute units (because it calls 4 external hooks) + ).prependInstruction(useMaxCU()).buildAndExecute(), + /0x17a2/ // NoExtraAccountsForTransferHook + ); + }); + + it("two_hop_swap_v2: [Fail] with transfer hook, but no extra accounts provided for tokenIntermediate", async () => { + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix( + ctx.program, { + ...baseIxParams, + // TransferHook + tokenTransferHookAccountsInput, + tokenTransferHookAccountsIntermediate: undefined, + tokenTransferHookAccountsOutput, + } + ) + // add Compute units (because it calls 4 external hooks) + ).prependInstruction(useMaxCU()).buildAndExecute(), + /0x17a2/ // NoExtraAccountsForTransferHook + ); + }); + + it("two_hop_swap_v2: [Fail] with transfer hook, but no extra accounts provided for tokenOutput", async () => { + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix( + ctx.program, { + ...baseIxParams, + // TransferHook + tokenTransferHookAccountsInput, + tokenTransferHookAccountsIntermediate: tokenTransferHookAccountsMid, + tokenTransferHookAccountsOutput: undefined, + } + ) + // add Compute units (because it calls 4 external hooks) + ).prependInstruction(useMaxCU()).buildAndExecute(), + /0x17a2/ // NoExtraAccountsForTransferHook + ); + }); + + it("two_hop_swap_v2: [Fail] with duplicated remaining accounts (TransferHookInput)", async () => { + const [remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder() + .addSlice(RemainingAccountsType.TransferHookInput, tokenTransferHookAccountsInput) + .addSlice(RemainingAccountsType.TransferHookIntermediate, tokenTransferHookAccountsMid) + .addSlice(RemainingAccountsType.TransferHookOutput, tokenTransferHookAccountsOutput) + // duplicated + .addSlice(RemainingAccountsType.TransferHookInput, tokenTransferHookAccountsInput) + .build(); + + const ix = ctx.program.instruction.twoHopSwapV2( + baseIxParams.amount, + baseIxParams.otherAmountThreshold, + baseIxParams.amountSpecifiedIsInput, + baseIxParams.aToBOne, + baseIxParams.aToBTwo, + baseIxParams.sqrtPriceLimitOne, + baseIxParams.sqrtPriceLimitTwo, + remainingAccountsInfo, { + accounts: { + ...baseIxParams, + memoProgram: MEMO_PROGRAM_ADDRESS, + }, + remainingAccounts, + }); + + await assert.rejects( + toTx( + ctx, + { + instructions: [ix], + cleanupInstructions: [], + signers: [], + } + ).buildAndExecute(), + /0x17a5/ // RemainingAccountsDuplicatedAccountsType + ); + }); + + it("two_hop_swap_v2: [Fail] with duplicated remaining accounts (TransferHookIntermediate)", async () => { + const [remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder() + .addSlice(RemainingAccountsType.TransferHookInput, tokenTransferHookAccountsInput) + .addSlice(RemainingAccountsType.TransferHookIntermediate, tokenTransferHookAccountsMid) + .addSlice(RemainingAccountsType.TransferHookOutput, tokenTransferHookAccountsOutput) + // duplicated + .addSlice(RemainingAccountsType.TransferHookIntermediate, tokenTransferHookAccountsMid) + .build(); + + const ix = ctx.program.instruction.twoHopSwapV2( + baseIxParams.amount, + baseIxParams.otherAmountThreshold, + baseIxParams.amountSpecifiedIsInput, + baseIxParams.aToBOne, + baseIxParams.aToBTwo, + baseIxParams.sqrtPriceLimitOne, + baseIxParams.sqrtPriceLimitTwo, + remainingAccountsInfo, { + accounts: { + ...baseIxParams, + memoProgram: MEMO_PROGRAM_ADDRESS, + }, + remainingAccounts, + }); + + await assert.rejects( + toTx( + ctx, + { + instructions: [ix], + cleanupInstructions: [], + signers: [], + } + ).buildAndExecute(), + /0x17a5/ // RemainingAccountsDuplicatedAccountsType + ); + }); + + it("two_hop_swap_v2: [Fail] with duplicated remaining accounts (TransferHookOutput)", async () => { + const [remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder() + .addSlice(RemainingAccountsType.TransferHookInput, tokenTransferHookAccountsInput) + .addSlice(RemainingAccountsType.TransferHookIntermediate, tokenTransferHookAccountsMid) + .addSlice(RemainingAccountsType.TransferHookOutput, tokenTransferHookAccountsOutput) + // duplicated + .addSlice(RemainingAccountsType.TransferHookOutput, tokenTransferHookAccountsOutput) + .build(); + + const ix = ctx.program.instruction.twoHopSwapV2( + baseIxParams.amount, + baseIxParams.otherAmountThreshold, + baseIxParams.amountSpecifiedIsInput, + baseIxParams.aToBOne, + baseIxParams.aToBTwo, + baseIxParams.sqrtPriceLimitOne, + baseIxParams.sqrtPriceLimitTwo, + remainingAccountsInfo, { + accounts: { + ...baseIxParams, + memoProgram: MEMO_PROGRAM_ADDRESS, + }, + remainingAccounts, + }); + + await assert.rejects( + toTx( + ctx, + { + instructions: [ix], + cleanupInstructions: [], + signers: [], + } + ).buildAndExecute(), + /0x17a5/ // RemainingAccountsDuplicatedAccountsType + ); + }); + }); + + describe("Special Errors", () => { + describe("TransferHook program rejects transfer", () => { + const TOO_LARGE_THRESHOLD_U64 = new BN(1_000_000_000_000); + + // We know that all transfers are executed 2 functions depending on the direction, so 2 test cases. + + it("[FAIL] owner to vault, amount too large", async () => { + // tokenA has transfer hook (so increase liquidity with large tokenB amount will not fail) + const mintAmount = TOO_LARGE_THRESHOLD_U64.muln(2); + + const tickSpacing = 1; + const rangeLowerTickIndex = -1; + const rangeUpperTickIndex = +1; + const currentTickIndex = +2; + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currentTickIndex, // price is above range ([-1, +1] p) + rangeLowerTickIndex, + rangeUpperTickIndex, + { + tokenA: mintAmount, + tokenB: mintAmount, + } + ); + + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + // tokenA has transfer hook + tokenTraitA: { isToken2022: true, hasTransferHookExtension: true}, + tokenTraitB: { isToken2022: true, hasTransferHookExtension: false}, + tickSpacing, + positions: [{ + tickLowerIndex: rangeLowerTickIndex, + tickUpperIndex: rangeUpperTickIndex, + liquidityAmount: liquidityAmount + }], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currentTickIndex), + mintAmount, + }); + const { poolInitInfo, tokenAccountA, tokenAccountB } = fixture.getInfos(); + + const inputTokenAmount = TOO_LARGE_THRESHOLD_U64.addn(1); // exceed threshold by 1 + const whirlpoolData = await fetcher.getPool(poolInitInfo.whirlpoolPda.publicKey, IGNORE_CACHE) as WhirlpoolData; + const aToB = true; + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB, + tokenAmount: inputTokenAmount, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + poolInitInfo.whirlpoolPda.publicKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + assert.ok(quote.estimatedAmountIn.gt(TOO_LARGE_THRESHOLD_U64)); + + // TransferHook + const tokenTransferHookAccountsA = await getExtraAccountMetasForTestTransferHookProgram(provider, poolInitInfo.tokenMintA); + const tokenTransferHookAccountsB = undefined; + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quote, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: PDAUtil.getOracle(ctx.program.programId, poolInitInfo.whirlpoolPda.publicKey).publicKey, + tokenTransferHookAccountsA, + tokenTransferHookAccountsB, + }) + ).buildAndExecute(), + (err) => { + // error code is 0x1770 from transfer hook program and it is ambiguous, so use message string + return JSON.stringify(err).includes("AmountTooBig"); + } + ); + }); + + it("[FAIL] vault to owner, amount too large", async () => { + // all tokenB is deposited into [-1, +1] (one side) + const mintAmount = TOO_LARGE_THRESHOLD_U64.muln(2); + + const tickSpacing = 1; + const rangeLowerTickIndex = -1; + const rangeUpperTickIndex = +1; + const currentTickIndex = +2; + const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts( + currentTickIndex, // price is above range ([-1, +1] p) + rangeLowerTickIndex, + rangeUpperTickIndex, + { + tokenA: TOO_LARGE_THRESHOLD_U64.muln(3).divn(4), // 3/4 of threshold + tokenB: TOO_LARGE_THRESHOLD_U64.muln(3).divn(4), // 3/4 of threshold + } + ); + + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + // tokenB has transfer hook + tokenTraitA: { isToken2022: true, hasTransferHookExtension: false}, + tokenTraitB: { isToken2022: true, hasTransferHookExtension: true}, + tickSpacing, + positions: [ + // to avoid large amount increase liquidity, 2 3/4 deposit will be made. + { + tickLowerIndex: rangeLowerTickIndex, + tickUpperIndex: rangeUpperTickIndex, + liquidityAmount: liquidityAmount + }, + { + tickLowerIndex: rangeLowerTickIndex, + tickUpperIndex: rangeUpperTickIndex, + liquidityAmount: liquidityAmount + }, + ], + initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currentTickIndex), + mintAmount, + }); + const { poolInitInfo, tokenAccountA, tokenAccountB } = fixture.getInfos(); + + const inputTokenAmount = TOO_LARGE_THRESHOLD_U64.muln(130).divn(100); // 130% of threshold + const whirlpoolData = await fetcher.getPool(poolInitInfo.whirlpoolPda.publicKey, IGNORE_CACHE) as WhirlpoolData; + const aToB = true; + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB, + tokenAmount: inputTokenAmount, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + whirlpoolData, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + poolInitInfo.whirlpoolPda.publicKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + assert.ok(quote.estimatedAmountOut.gt(TOO_LARGE_THRESHOLD_U64)); + + // TransferHook + const tokenTransferHookAccountsA = undefined; + const tokenTransferHookAccountsB = await getExtraAccountMetasForTestTransferHookProgram(provider, poolInitInfo.tokenMintB); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + ...quote, + whirlpool: poolInitInfo.whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + oracle: PDAUtil.getOracle(ctx.program.programId, poolInitInfo.whirlpoolPda.publicKey).publicKey, + tokenTransferHookAccountsA, + tokenTransferHookAccountsB, + }) + ).buildAndExecute(), + (err) => { + // error code is 0x1770 from transfer hook program and it is ambiguous, so use message string + return JSON.stringify(err).includes("AmountTooBig"); + } + ); + }) + }) + }); +}); diff --git a/sdk/tests/integration/v2/two_hop_swap_v2.test.ts b/sdk/tests/integration/v2/two_hop_swap_v2.test.ts new file mode 100644 index 000000000..b2d6188bc --- /dev/null +++ b/sdk/tests/integration/v2/two_hop_swap_v2.test.ts @@ -0,0 +1,2069 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Percentage } from "@orca-so/common-sdk"; +import { PublicKey } from "@solana/web3.js"; +import * as assert from "assert"; +import { BN } from "bn.js"; +import { + buildWhirlpoolClient, + InitPoolParams, + METADATA_PROGRAM_ADDRESS, + PDAUtil, + swapQuoteWithParams, + SwapUtils, + toTx, + twoHopSwapQuoteFromSwapQuotes, + WhirlpoolContext, + WhirlpoolData, + WhirlpoolIx, +} from "../../../src"; +import { InitPoolV2Params, TwoHopSwapV2Params } from "../../../src/instructions"; +import { IGNORE_CACHE } from "../../../src/network/public/fetcher"; +import { + getTokenBalance, + TEST_TOKEN_2022_PROGRAM_ID, + TEST_TOKEN_PROGRAM_ID, + TickSpacing, +} from "../../utils"; +import { defaultConfirmOptions } from "../../utils/const"; +import { + buildTestAquariumsV2, + getDefaultAquariumV2, + getTokenAccsForPoolsV2, + InitAquariumV2Params, +} from "../../utils/v2/aquarium-v2"; +import { FundedPositionV2Params, TokenTrait } from "../../utils/v2/init-utils-v2"; +import { asyncAssertOwnerProgram, createMintV2 } from "../../utils/v2/token-2022"; +import { TokenExtensionUtil } from "../../../src/utils/public/token-extension-util"; + +describe("two_hop_swap_v2", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + const client = buildWhirlpoolClient(ctx); + + describe("v1 parity", () => { + // 8 patterns for tokenTraitA, tokenTraitB, tokenTraitC + const tokenTraitVariations: { + tokenTraitA: TokenTrait; + tokenTraitB: TokenTrait; + tokenTraitC: TokenTrait; + }[] = [ + { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + tokenTraitC: { isToken2022: false }, + }, + { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + tokenTraitC: { isToken2022: true }, + }, + { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: true }, + tokenTraitC: { isToken2022: false }, + }, + { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: true }, + tokenTraitC: { isToken2022: true }, + }, + { + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: false }, + tokenTraitC: { isToken2022: false }, + }, + { + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: false }, + tokenTraitC: { isToken2022: true }, + }, + { + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tokenTraitC: { isToken2022: false }, + }, + { + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tokenTraitC: { isToken2022: true }, + }, + ]; + tokenTraitVariations.forEach((tokenTraits) => { + describe(`tokenTraitA: ${ + tokenTraits.tokenTraitA.isToken2022 ? "Token2022" : "Token" + }, tokenTraitB: ${ + tokenTraits.tokenTraitB.isToken2022 ? "Token2022" : "Token" + }, tokenTraitC: ${tokenTraits.tokenTraitC.isToken2022 ? "Token2022" : "Token"}`, () => { + let aqConfig: InitAquariumV2Params; + beforeEach(async () => { + aqConfig = getDefaultAquariumV2(); + // Add a third token and account and a second pool + aqConfig.initMintParams = [ + { tokenTrait: tokenTraits.tokenTraitA }, + { tokenTrait: tokenTraits.tokenTraitB }, + { tokenTrait: tokenTraits.tokenTraitC }, + ]; + aqConfig.initTokenAccParams.push({ mintIndex: 2 }); + aqConfig.initPoolParams.push({ mintIndices: [1, 2], tickSpacing: TickSpacing.Standard }); + + // Add tick arrays and positions + const aToB = false; + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 0, + startTickIndex: 22528, + arrayCount: 3, + aToB, + }); + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 1, + startTickIndex: 22528, + arrayCount: 3, + aToB, + }); + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + aqConfig.initPositionParams.push({ poolIndex: 0, fundParams }); + aqConfig.initPositionParams.push({ poolIndex: 1, fundParams }); + }); + + describe("fails [2] with two-hop swap, invalid accounts", () => { + let baseIxParams: TwoHopSwapV2Params; + beforeEach(async () => { + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + await asyncAssertOwnerProgram( + ctx.provider, + mintKeys[0], + tokenTraits.tokenTraitA.isToken2022 + ? TEST_TOKEN_2022_PROGRAM_ID + : TEST_TOKEN_PROGRAM_ID + ); + await asyncAssertOwnerProgram( + ctx.provider, + mintKeys[1], + tokenTraits.tokenTraitB.isToken2022 + ? TEST_TOKEN_2022_PROGRAM_ID + : TEST_TOKEN_PROGRAM_ID + ); + await asyncAssertOwnerProgram( + ctx.provider, + mintKeys[2], + tokenTraits.tokenTraitC.isToken2022 + ? TEST_TOKEN_2022_PROGRAM_ID + : TEST_TOKEN_PROGRAM_ID + ); + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + //const whirlpoolOne = await client.getPool(whirlpoolOneKey, IGNORE_CACHE); + //const whirlpoolTwo = await client.getPool(whirlpoolTwoKey, IGNORE_CACHE); + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [inputToken, intermediaryToken, _outputToken] = mintKeys; + + /* replaced by swapQuoteWithParams to avoid using whirlpool client + const quote = await swapQuoteByInputToken( + whirlpoolOne, + inputToken, + new BN(1000), + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + + const quote2 = await swapQuoteByInputToken( + whirlpoolTwo, + intermediaryToken, + quote.estimatedAmountOut, + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + */ + + const aToBOne = whirlpoolDataOne.tokenMintA.equals(inputToken); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(intermediaryToken); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: quote.estimatedAmountOut, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + baseIxParams = { + ...twoHopQuote, + ...getParamsFromPools([pools[0], pools[1]], [twoHopQuote.aToBOne, twoHopQuote.aToBTwo], tokenAccounts), + tokenAuthority: ctx.wallet.publicKey, + }; + }); + + it("fails invalid whirlpool", async () => { + await rejectParams( + { + ...baseIxParams, + whirlpoolOne: baseIxParams.whirlpoolTwo, + }, + ///0x7d3/ // ConstraintRaw + // V2 has token_mint_one_a and it has address constraint + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails invalid token account", async () => { + await rejectParams( + { + ...baseIxParams, + tokenOwnerAccountInput: baseIxParams.tokenOwnerAccountOutput, + }, + /0x7d3/ // ConstraintRaw + ); + await rejectParams( + { + ...baseIxParams, + tokenOwnerAccountOutput: baseIxParams.tokenOwnerAccountInput, + }, + /0x7d3/ // ConstraintRaw + ); + }); + + it("fails invalid token vault", async () => { + await rejectParams( + { + ...baseIxParams, + tokenVaultOneInput: baseIxParams.tokenVaultOneIntermediate, + }, + /0x7dc/ // ConstraintAddress + ); + await rejectParams( + { + ...baseIxParams, + tokenVaultOneIntermediate: baseIxParams.tokenVaultOneInput, + }, + /0x7dc/ // ConstraintAddress + ); + await rejectParams( + { + ...baseIxParams, + tokenVaultTwoIntermediate: baseIxParams.tokenVaultTwoOutput, + }, + /0x7dc/ // ConstraintAddress + ); + await rejectParams( + { + ...baseIxParams, + tokenVaultTwoOutput: baseIxParams.tokenVaultTwoIntermediate, + }, + /0x7dc/ // ConstraintAddress + ); + }); + + it("fails invalid oracle one address", async () => { + await rejectParams( + { + ...baseIxParams, + oracleOne: PublicKey.unique(), + }, + /0x7d6/ // Constraint Seeds + ); + }); + + it("fails invalid oracle two address", async () => { + await rejectParams( + { + ...baseIxParams, + oracleTwo: PublicKey.unique(), + }, + /0x7d6/ // Constraint Seeds + ); + }); + + it("fails invalid tick array one", async () => { + await rejectParams( + { + ...baseIxParams, + tickArrayOne0: PublicKey.unique(), + }, + /0xbbf/ // AccountOwnedByWrongProgram + ); + await rejectParams( + { + ...baseIxParams, + tickArrayOne1: PublicKey.unique(), + }, + /0xbbf/ // AccountOwnedByWrongProgram + ); + await rejectParams( + { + ...baseIxParams, + tickArrayOne2: PublicKey.unique(), + }, + /0xbbf/ // AccountOwnedByWrongProgram + ); + }); + + it("fails invalid tick array two", async () => { + await rejectParams( + { + ...baseIxParams, + tickArrayTwo0: PublicKey.unique(), + }, + /0xbbf/ // AccountOwnedByWrongProgram + ); + await rejectParams( + { + ...baseIxParams, + tickArrayTwo1: PublicKey.unique(), + }, + /0xbbf/ // AccountOwnedByWrongProgram + ); + await rejectParams( + { + ...baseIxParams, + tickArrayTwo2: PublicKey.unique(), + }, + /0xbbf/ // AccountOwnedByWrongProgram + ); + }); + }); + + it("swaps [2] with two-hop swap, amountSpecifiedIsInput=true", async () => { + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + let tokenBalances = await getTokenBalances(tokenAccounts.map((acc) => acc.account)); + + const tokenVaultBalances = await getTokenBalancesForVaults(pools); + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + //let whirlpoolOne = await client.getPool(whirlpoolOneKey, IGNORE_CACHE); + //let whirlpoolTwo = await client.getPool(whirlpoolTwoKey, IGNORE_CACHE); + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [inputToken, intermediaryToken, _outputToken] = mintKeys; + + /* replaced by swapQuoteWithParams to avoid using whirlpool client + const quote = await swapQuoteByInputToken( + whirlpoolOne, + inputToken, + new BN(1000), + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + + const quote2 = await swapQuoteByInputToken( + whirlpoolTwo, + intermediaryToken, + quote.estimatedAmountOut, + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + */ + + const aToBOne = whirlpoolDataOne.tokenMintA.equals(inputToken); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(intermediaryToken); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: quote.estimatedAmountOut, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + + await toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...twoHopQuote, + ...getParamsFromPools([pools[0], pools[1]], [twoHopQuote.aToBOne, twoHopQuote.aToBTwo], tokenAccounts), + tokenAuthority: ctx.wallet.publicKey, + }) + ).buildAndExecute(); + + assert.deepEqual(await getTokenBalancesForVaults(pools), [ + tokenVaultBalances[0].add(quote.estimatedAmountIn), + tokenVaultBalances[1].sub(quote.estimatedAmountOut), + tokenVaultBalances[2].add(quote2.estimatedAmountIn), + tokenVaultBalances[3].sub(quote2.estimatedAmountOut), + ]); + + const prevTbs = [...tokenBalances]; + tokenBalances = await getTokenBalances(tokenAccounts.map((acc) => acc.account)); + + assert.deepEqual(tokenBalances, [ + prevTbs[0].sub(quote.estimatedAmountIn), + prevTbs[1], + prevTbs[2].add(quote2.estimatedAmountOut), + ]); + + //whirlpoolOne = await client.getPool(whirlpoolOneKey, IGNORE_CACHE); + //whirlpoolTwo = await client.getPool(whirlpoolTwoKey, IGNORE_CACHE); + }); + + it("swaps [2] with two-hop swap, amountSpecifiedIsInput=true, A->B->A", async () => { + // Add another mint and update pool so there is no overlapping mint + aqConfig.initFeeTierParams.push({ tickSpacing: TickSpacing.ThirtyTwo }); + aqConfig.initPoolParams[1] = { + mintIndices: [0, 1], + tickSpacing: TickSpacing.ThirtyTwo, + feeTierIndex: 1, + }; + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 1, + startTickIndex: 22528, + arrayCount: 12, + aToB: true, + }); + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 1, + startTickIndex: 22528, + arrayCount: 12, + aToB: false, + }); + + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + let tokenBalances = await getTokenBalances(tokenAccounts.map((acc) => acc.account)); + + const tokenVaultBalances = await getTokenBalancesForVaults(pools); + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + //let whirlpoolOne = await client.getPool(whirlpoolOneKey, IGNORE_CACHE); + //let whirlpoolTwo = await client.getPool(whirlpoolTwoKey, IGNORE_CACHE); + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [tokenA, tokenB, _outputToken] = mintKeys; + + /* replaced by swapQuoteWithParams to avoid using whirlpool client + const quote = await swapQuoteByInputToken( + whirlpoolOne, + tokenA, + new BN(1000), + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + + const quote2 = await swapQuoteByInputToken( + whirlpoolTwo, + tokenB, + quote.estimatedAmountOut, + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + */ + + const aToBOne = whirlpoolDataOne.tokenMintA.equals(tokenA); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(tokenB); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: quote.estimatedAmountOut, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + + await toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...twoHopQuote, + ...getParamsFromPools([pools[0], pools[1]], [twoHopQuote.aToBOne, twoHopQuote.aToBTwo], tokenAccounts), + tokenAuthority: ctx.wallet.publicKey, + }) + ).buildAndExecute(); + + assert.deepEqual(await getTokenBalancesForVaults(pools), [ + tokenVaultBalances[0].add(quote.estimatedAmountIn), + tokenVaultBalances[1].sub(quote.estimatedAmountOut), + tokenVaultBalances[2].sub(quote2.estimatedAmountOut), + tokenVaultBalances[3].add(quote2.estimatedAmountIn), + ]); + + const prevTbs = [...tokenBalances]; + tokenBalances = await getTokenBalances(tokenAccounts.map((acc) => acc.account)); + + assert.deepEqual(tokenBalances, [ + prevTbs[0].sub(quote.estimatedAmountIn).add(quote2.estimatedAmountOut), + prevTbs[1].add(quote.estimatedAmountOut).sub(quote2.estimatedAmountIn), + prevTbs[2], + ]); + }); + + it("fails swaps [2] with top-hop swap, amountSpecifiedIsInput=true, slippage", async () => { + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + //const whirlpoolOne = await client.getPool(whirlpoolOneKey, IGNORE_CACHE); + //const whirlpoolTwo = await client.getPool(whirlpoolTwoKey, IGNORE_CACHE); + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [inputToken, intermediaryToken, _outputToken] = mintKeys; + + /* replaced by swapQuoteWithParams to avoid using whirlpool client + const quote = await swapQuoteByInputToken( + whirlpoolOne, + inputToken, + new BN(1000), + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + + const quote2 = await swapQuoteByInputToken( + whirlpoolTwo, + intermediaryToken, + quote.estimatedAmountOut, + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + */ + + const aToBOne = whirlpoolDataOne.tokenMintA.equals(inputToken); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(intermediaryToken); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: quote.estimatedAmountOut, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...twoHopQuote, + ...getParamsFromPools([pools[0], pools[1]], [twoHopQuote.aToBOne, twoHopQuote.aToBTwo], tokenAccounts), + otherAmountThreshold: new BN(613309), + tokenAuthority: ctx.wallet.publicKey, + }) + ).buildAndExecute(), + /0x1794/ // Above Out Below Minimum + ); + }); + + it("swaps [2] with two-hop swap, amountSpecifiedIsInput=false", async () => { + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + const preSwapBalances = await getTokenBalances(tokenAccounts.map((acc) => acc.account)); + const tokenVaultBalances = await getTokenBalancesForVaults(pools); + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + //const whirlpoolOne = await client.getPool(whirlpoolOneKey, IGNORE_CACHE); + //const whirlpoolTwo = await client.getPool(whirlpoolTwoKey, IGNORE_CACHE); + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [_inputToken, intermediaryToken, outputToken] = mintKeys; + + /* replaced by swapQuoteWithParams to avoid using whirlpool client + const quote2 = await swapQuoteByOutputToken( + whirlpoolTwo, + outputToken, + new BN(1000), + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + + const quote = await swapQuoteByOutputToken( + whirlpoolOne, + intermediaryToken, + quote2.estimatedAmountIn, + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + */ + + const aToBTwo = whirlpoolDataTwo.tokenMintB.equals(outputToken); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: false, + aToB: aToBTwo, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBOne = whirlpoolDataOne.tokenMintB.equals(intermediaryToken); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: false, + aToB: aToBOne, + tokenAmount: quote2.estimatedAmountIn, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + + await toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...twoHopQuote, + ...getParamsFromPools([pools[0], pools[1]], [twoHopQuote.aToBOne, twoHopQuote.aToBTwo], tokenAccounts), + tokenAuthority: ctx.wallet.publicKey, + }) + ).buildAndExecute(); + + assert.deepEqual(await getTokenBalancesForVaults(pools), [ + tokenVaultBalances[0].add(quote.estimatedAmountIn), + tokenVaultBalances[1].sub(quote.estimatedAmountOut), + tokenVaultBalances[2].add(quote2.estimatedAmountIn), + tokenVaultBalances[3].sub(quote2.estimatedAmountOut), + ]); + + assert.deepEqual(await getTokenBalances(tokenAccounts.map((acc) => acc.account)), [ + preSwapBalances[0].sub(quote.estimatedAmountIn), + preSwapBalances[1], + preSwapBalances[2].add(quote2.estimatedAmountOut), + ]); + }); + + it("fails swaps [2] with two-hop swap, amountSpecifiedIsInput=false slippage", async () => { + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + const preSwapBalances = await getTokenBalances(tokenAccounts.map((acc) => acc.account)); + const tokenVaultBalances = await getTokenBalancesForVaults(pools); + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + //const whirlpoolOne = await client.getPool(whirlpoolOneKey, IGNORE_CACHE); + //const whirlpoolTwo = await client.getPool(whirlpoolTwoKey, IGNORE_CACHE); + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [_inputToken, intermediaryToken, outputToken] = mintKeys; + + /* + const quote2 = await swapQuoteByOutputToken( + whirlpoolTwo, + outputToken, + new BN(1000), + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + + const quote = await swapQuoteByOutputToken( + whirlpoolOne, + intermediaryToken, + quote2.estimatedAmountIn, + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + */ + + const aToBTwo = whirlpoolDataTwo.tokenMintB.equals(outputToken); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: false, + aToB: aToBTwo, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBOne = whirlpoolDataOne.tokenMintB.equals(intermediaryToken); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: false, + aToB: aToBOne, + tokenAmount: quote2.estimatedAmountIn, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...twoHopQuote, + ...getParamsFromPools([pools[0], pools[1]], [twoHopQuote.aToBOne, twoHopQuote.aToBTwo], tokenAccounts), + otherAmountThreshold: new BN(2), + tokenAuthority: ctx.wallet.publicKey, + }) + ).buildAndExecute(), + /0x1795/ // Above In Above Maximum + ); + }); + + it("fails swaps [2] with two-hop swap, no overlapping mints", async () => { + // Add another mint and update pool so there is no overlapping mint + aqConfig.initMintParams.push({ tokenTrait: { isToken2022: true } }); + aqConfig.initTokenAccParams.push({ mintIndex: 3 }); + aqConfig.initPoolParams[1].mintIndices = [2, 3]; + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + //const whirlpoolOne = await client.getPool(whirlpoolOneKey, IGNORE_CACHE); + //const whirlpoolTwo = await client.getPool(whirlpoolTwoKey, IGNORE_CACHE); + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [_inputToken, intermediaryToken, outputToken] = mintKeys; + + /* replaced by swapQuoteWithParams to avoid using whirlpool client + const quote2 = await swapQuoteByOutputToken( + whirlpoolTwo, + outputToken, + new BN(1000), + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + + const quote = await swapQuoteByOutputToken( + whirlpoolOne, + intermediaryToken, + quote2.estimatedAmountIn, + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + */ + + const aToBTwo = whirlpoolDataTwo.tokenMintB.equals(outputToken); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: false, + aToB: aToBTwo, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBOne = whirlpoolDataOne.tokenMintB.equals(intermediaryToken); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: false, + aToB: aToBOne, + tokenAmount: quote2.estimatedAmountIn, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...twoHopQuote, + ...getParamsFromPools([pools[0], pools[1]], [twoHopQuote.aToBOne, twoHopQuote.aToBTwo], tokenAccounts), + tokenAuthority: ctx.wallet.publicKey, + }) + ).buildAndExecute(), + /0x1799/ // Invalid intermediary mint + ); + }); + + it("swaps [2] with two-hop swap, amount_specified_is_input=true, first swap price limit", async () => { + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + //let whirlpoolOne = await client.getPool(whirlpoolOneKey, IGNORE_CACHE); + //let whirlpoolTwo = await client.getPool(whirlpoolTwoKey, IGNORE_CACHE); + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [inputToken, intermediaryToken, _outputToken] = mintKeys; + + /* replaced by swapQuoteWithParams to avoid using whirlpool client + const quote = await swapQuoteByInputToken( + whirlpoolOne, + inputToken, + new BN(1000), + Percentage.fromFraction(0, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + + const quote2 = await swapQuoteByInputToken( + whirlpoolTwo, + intermediaryToken, + quote.estimatedAmountOut, + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + */ + + const aToBOne = whirlpoolDataOne.tokenMintA.equals(inputToken); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(intermediaryToken); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: quote.estimatedAmountOut, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + // Set a price limit that is less than the 1% slippage threshold, + // which will allow the swap to go through + quote.sqrtPriceLimit = quote.estimatedEndSqrtPrice.add( + whirlpoolDataOne.sqrtPrice + .sub(quote.estimatedEndSqrtPrice) + .mul(new anchor.BN("5")) + .div(new anchor.BN("1000")) + ); + + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + + await toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...twoHopQuote, + ...getParamsFromPools([pools[0], pools[1]], [twoHopQuote.aToBOne, twoHopQuote.aToBTwo], tokenAccounts), + tokenAuthority: ctx.wallet.publicKey, + }) + ).buildAndExecute(); + + const postWhirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + //const postWhirlpoolDataTwo = await fetcher.getPool(whirlpoolTwoKey, IGNORE_CACHE) as WhirlpoolData; + + assert.equal(postWhirlpoolDataOne.sqrtPrice.eq(quote.sqrtPriceLimit), true); + }); + + it("fails: swaps [2] with two-hop swap, amount_specified_is_input=true, second swap price limit", async () => { + // ATTENTION: v1 and v2 are different + // v2 use vault to vault transfer, so the output of first swap MUST be equal to the input of the second swap. + // So not-full-filled swap will be rejected. + + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + //let whirlpoolOne = await client.getPool(whirlpoolOneKey, IGNORE_CACHE); + //let whirlpoolTwo = await client.getPool(whirlpoolTwoKey, IGNORE_CACHE); + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [inputToken, intermediaryToken, _outputToken] = mintKeys; + + /* + const quote = await swapQuoteByInputToken( + whirlpoolOne, + inputToken, + new BN(1000), + Percentage.fromFraction(0, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + + const quote2 = await swapQuoteByInputToken( + whirlpoolTwo, + intermediaryToken, + quote.estimatedAmountOut, + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + */ + + const aToBOne = whirlpoolDataOne.tokenMintA.equals(inputToken); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(intermediaryToken); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: quote.estimatedAmountOut, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + // Set a price limit that is less than the 1% slippage threshold, + // which will result non-full-filled second swap + quote2.sqrtPriceLimit = quote2.estimatedEndSqrtPrice.add( + whirlpoolDataTwo.sqrtPrice + .sub(quote2.estimatedEndSqrtPrice) + .mul(new anchor.BN("5")) + .div(new anchor.BN("1000")) + ); + + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...twoHopQuote, + ...getParamsFromPools([pools[0], pools[1]], [twoHopQuote.aToBOne, twoHopQuote.aToBTwo], tokenAccounts), + tokenAuthority: ctx.wallet.publicKey, + }) + ).buildAndExecute(), + /0x17a3/ // IntermediateTokenAmountMismatch + ); + }); + + it("fails: swaps [2] with two-hop swap, amount_specified_is_input=false, first swap price limit", async () => { + // ATTENTION: v1 and v2 are different + // v2 use vault to vault transfer, so the output of first swap MUST be equal to the input of the second swap. + // So not-full-filled swap will be rejected. + + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [_inputToken, intermediaryToken, outputToken] = mintKeys; + + const aToBTwo = whirlpoolDataTwo.tokenMintB.equals(outputToken); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: false, + aToB: aToBTwo, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBOne = whirlpoolDataOne.tokenMintB.equals(intermediaryToken); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: false, + aToB: aToBOne, + tokenAmount: quote2.estimatedAmountIn, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(false), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + // add sqrtPriceLimit on quote + quote.sqrtPriceLimit = aToBOne + ? quote.estimatedEndSqrtPrice.addn(1) + : quote.estimatedEndSqrtPrice.subn(1); + + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...twoHopQuote, + ...getParamsFromPools([pools[0], pools[1]], [twoHopQuote.aToBOne, twoHopQuote.aToBTwo], tokenAccounts), + tokenAuthority: ctx.wallet.publicKey, + }) + ).buildAndExecute(), + /0x17a3/ // IntermediateTokenAmountMismatch + ); + }); + + it("fails swaps [2] with two-hop swap, amount_specified_is_input=true, first swap price limit", async () => { + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + //let whirlpoolOne = await client.getPool(whirlpoolOneKey, IGNORE_CACHE); + //let whirlpoolTwo = await client.getPool(whirlpoolTwoKey, IGNORE_CACHE); + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [inputToken, intermediaryToken, _outputToken] = mintKeys; + + /* + const quote = await swapQuoteByInputToken( + whirlpoolOne, + inputToken, + new BN(1000), + Percentage.fromFraction(0, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + + const quote2 = await swapQuoteByInputToken( + whirlpoolTwo, + intermediaryToken, + quote.estimatedAmountOut, + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + */ + + const aToBOne = whirlpoolDataOne.tokenMintA.equals(inputToken); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(intermediaryToken); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: quote.estimatedAmountOut, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + // Set a price limit that is less than the 1% slippage threshold, + // which will allow the swap to go through + quote.sqrtPriceLimit = quote.estimatedEndSqrtPrice.add( + whirlpoolDataOne.sqrtPrice + .sub(quote.estimatedEndSqrtPrice) + .mul(new anchor.BN("15")) + .div(new anchor.BN("1000")) + ); + + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...twoHopQuote, + ...getParamsFromPools([pools[0], pools[1]], [twoHopQuote.aToBOne, twoHopQuote.aToBTwo], tokenAccounts), + tokenAuthority: ctx.wallet.publicKey, + }) + ).buildAndExecute() + ); + }); + + it("fails swaps [2] with two-hop swap, amount_specified_is_input=true, second swap price limit", async () => { + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + //let whirlpoolOne = await client.getPool(whirlpoolOneKey, IGNORE_CACHE); + //let whirlpoolTwo = await client.getPool(whirlpoolTwoKey, IGNORE_CACHE); + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [inputToken, intermediaryToken, _outputToken] = mintKeys; + + /* replaced by swapQuoteWithParams to avoid using whirlpool client + const quote = await swapQuoteByInputToken( + whirlpoolOne, + inputToken, + new BN(1000), + Percentage.fromFraction(0, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + + const quote2 = await swapQuoteByInputToken( + whirlpoolTwo, + intermediaryToken, + quote.estimatedAmountOut, + Percentage.fromFraction(1, 100), + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + */ + + const aToBOne = whirlpoolDataOne.tokenMintA.equals(inputToken); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(intermediaryToken); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: quote.estimatedAmountOut, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + // Set a price limit that is greater than the 1% slippage threshold, + // which will cause the swap to fail + quote2.sqrtPriceLimit = quote2.estimatedEndSqrtPrice.add( + whirlpoolDataTwo.sqrtPrice + .sub(quote2.estimatedEndSqrtPrice) + .mul(new anchor.BN("15")) + .div(new anchor.BN("1000")) + ); + + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.twoHopSwapV2Ix(ctx.program, { + ...twoHopQuote, + ...getParamsFromPools([pools[0], pools[1]], [twoHopQuote.aToBOne, twoHopQuote.aToBTwo], tokenAccounts), + tokenAuthority: ctx.wallet.publicKey, + }) + ).buildAndExecute() + ); + }); + }); + }); + }); + + describe("v2 specific accounts", () => { + describe("with Token-2022", () => { + const tokenTraits = { + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + tokenTraitC: { isToken2022: true }, + }; + + let aqConfig: InitAquariumV2Params; + let baseIxParams: TwoHopSwapV2Params; + let otherTokenPublicKey: PublicKey; + + beforeEach(async () => { + otherTokenPublicKey = await createMintV2(provider, { isToken2022: true }); + + aqConfig = getDefaultAquariumV2(); + // Add a third token and account and a second pool + aqConfig.initMintParams = [ + { tokenTrait: tokenTraits.tokenTraitA }, + { tokenTrait: tokenTraits.tokenTraitB }, + { tokenTrait: tokenTraits.tokenTraitC }, + ]; + aqConfig.initTokenAccParams.push({ mintIndex: 2 }); + aqConfig.initPoolParams.push({ mintIndices: [1, 2], tickSpacing: TickSpacing.Standard }); + + // Add tick arrays and positions + const aToB = false; + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 0, + startTickIndex: 22528, + arrayCount: 3, + aToB, + }); + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 1, + startTickIndex: 22528, + arrayCount: 3, + aToB, + }); + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + aqConfig.initPositionParams.push({ poolIndex: 0, fundParams }); + aqConfig.initPositionParams.push({ poolIndex: 1, fundParams }); + + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [inputToken, intermediaryToken, _outputToken] = mintKeys; + + const aToBOne = whirlpoolDataOne.tokenMintA.equals(inputToken); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(intermediaryToken); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: quote.estimatedAmountOut, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + baseIxParams = { + ...twoHopQuote, + ...getParamsFromPools([pools[0], pools[1]], [twoHopQuote.aToBOne, twoHopQuote.aToBTwo], tokenAccounts), + tokenAuthority: ctx.wallet.publicKey, + }; + }); + + describe("fails when passed token_mint_* does not match whirlpool's token_mint_*_*", () => { + it("token_mint_input", async () => { + await rejectParams( + { + ...baseIxParams, + tokenMintInput: otherTokenPublicKey, + }, + /0x7dc/ // ConstraintAddress + ); + }); + it("token_mint_intermediate", async () => { + await rejectParams( + { + ...baseIxParams, + tokenMintIntermediate: otherTokenPublicKey, + }, + /0x7dc/ // ConstraintAddress + ); + }); + it("token_mint_output", async () => { + await rejectParams( + { + ...baseIxParams, + tokenMintOutput: otherTokenPublicKey, + }, + /0x7dc/ // ConstraintAddress + ); + }); + }); + + describe("fails when passed token_program_* is not token-2022 program (token is passed)", () => { + it("token_program_input", async () => { + await rejectParams( + { + ...baseIxParams, + tokenProgramInput: TEST_TOKEN_PROGRAM_ID, + }, + /0x7dc/ // ConstraintAddress + ); + }); + it("token_program_intermediate", async () => { + await rejectParams( + { + ...baseIxParams, + tokenProgramIntermediate: TEST_TOKEN_PROGRAM_ID, + }, + /0x7dc/ // ConstraintAddress + ); + }); + it("token_program_output", async () => { + await rejectParams( + { + ...baseIxParams, + tokenProgramOutput: TEST_TOKEN_PROGRAM_ID, + }, + /0x7dc/ // ConstraintAddress + ); + }); + }); + + describe("fails when passed token_program_*_* is token_metadata", () => { + it("token_program_input", async () => { + await rejectParams( + { + ...baseIxParams, + tokenProgramInput: METADATA_PROGRAM_ADDRESS, + }, + /0xbc0/ // InvalidProgramId + ); + }); + it("token_program_intermediate", async () => { + await rejectParams( + { + ...baseIxParams, + tokenProgramIntermediate: METADATA_PROGRAM_ADDRESS, + }, + /0xbc0/ // InvalidProgramId + ); + }); + it("token_program_output", async () => { + await rejectParams( + { + ...baseIxParams, + tokenProgramOutput: METADATA_PROGRAM_ADDRESS, + }, + /0xbc0/ // InvalidProgramId + ); + }); + }); + + it("fails when passed memo_program is token_metadata", async () => { + await assert.rejects( + toTx(ctx, { + cleanupInstructions: [], + signers: [], + instructions: [ + ctx.program.instruction.twoHopSwapV2( + baseIxParams.amount, + baseIxParams.otherAmountThreshold, + baseIxParams.amountSpecifiedIsInput, + baseIxParams.aToBOne, + baseIxParams.aToBTwo, + baseIxParams.sqrtPriceLimitOne, + baseIxParams.sqrtPriceLimitTwo, + { slices: [] }, + { + accounts: { + ...baseIxParams, + memoProgram: METADATA_PROGRAM_ADDRESS, + }, + } + ), + ], + }).buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + + }); + }); + + describe("with Token", () => { + const tokenTraits = { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + tokenTraitC: { isToken2022: false }, + }; + + let aqConfig: InitAquariumV2Params; + let baseIxParams: TwoHopSwapV2Params; + let otherTokenPublicKey: PublicKey; + + beforeEach(async () => { + otherTokenPublicKey = await createMintV2(provider, { isToken2022: false }); + + aqConfig = getDefaultAquariumV2(); + // Add a third token and account and a second pool + aqConfig.initMintParams = [ + { tokenTrait: tokenTraits.tokenTraitA }, + { tokenTrait: tokenTraits.tokenTraitB }, + { tokenTrait: tokenTraits.tokenTraitC }, + ]; + aqConfig.initTokenAccParams.push({ mintIndex: 2 }); + aqConfig.initPoolParams.push({ mintIndices: [1, 2], tickSpacing: TickSpacing.Standard }); + + // Add tick arrays and positions + const aToB = false; + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 0, + startTickIndex: 22528, + arrayCount: 3, + aToB, + }); + aqConfig.initTickArrayRangeParams.push({ + poolIndex: 1, + startTickIndex: 22528, + arrayCount: 3, + aToB, + }); + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(10_000_000), + tickLowerIndex: 29440, + tickUpperIndex: 33536, + }, + ]; + aqConfig.initPositionParams.push({ poolIndex: 0, fundParams }); + aqConfig.initPositionParams.push({ poolIndex: 1, fundParams }); + + const aquarium = (await buildTestAquariumsV2(ctx, [aqConfig]))[0]; + const { tokenAccounts, mintKeys, pools } = aquarium; + + const whirlpoolOneKey = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwoKey = pools[1].whirlpoolPda.publicKey; + const whirlpoolDataOne = (await fetcher.getPool( + whirlpoolOneKey, + IGNORE_CACHE + )) as WhirlpoolData; + const whirlpoolDataTwo = (await fetcher.getPool( + whirlpoolTwoKey, + IGNORE_CACHE + )) as WhirlpoolData; + + const [inputToken, intermediaryToken, _outputToken] = mintKeys; + + const aToBOne = whirlpoolDataOne.tokenMintA.equals(inputToken); + const quote = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBOne, + tokenAmount: new BN(1000), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBOne), + whirlpoolData: whirlpoolDataOne, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataOne.tickCurrentIndex, + whirlpoolDataOne.tickSpacing, + aToBOne, + ctx.program.programId, + whirlpoolOneKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataOne, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const aToBTwo = whirlpoolDataTwo.tokenMintA.equals(intermediaryToken); + const quote2 = swapQuoteWithParams( + { + amountSpecifiedIsInput: true, + aToB: aToBTwo, + tokenAmount: quote.estimatedAmountOut, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToBTwo), + whirlpoolData: whirlpoolDataTwo, + tickArrays: await SwapUtils.getTickArrays( + whirlpoolDataTwo.tickCurrentIndex, + whirlpoolDataTwo.tickSpacing, + aToBTwo, + ctx.program.programId, + whirlpoolTwoKey, + fetcher, + IGNORE_CACHE + ), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolDataTwo, IGNORE_CACHE), + }, + Percentage.fromFraction(1, 100) + ); + + const twoHopQuote = twoHopSwapQuoteFromSwapQuotes(quote, quote2); + baseIxParams = { + ...twoHopQuote, + ...getParamsFromPools([pools[0], pools[1]], [twoHopQuote.aToBOne, twoHopQuote.aToBTwo], tokenAccounts), + tokenAuthority: ctx.wallet.publicKey, + }; + }); + + describe("fails when passed token_program_* is not token program (token-2022 is passed)", () => { + it("token_program_input", async () => { + await rejectParams( + { + ...baseIxParams, + tokenProgramInput: TEST_TOKEN_2022_PROGRAM_ID, + }, + /0x7dc/ // ConstraintAddress + ); + }); + it("token_program_intermediate", async () => { + await rejectParams( + { + ...baseIxParams, + tokenProgramIntermediate: TEST_TOKEN_2022_PROGRAM_ID, + }, + /0x7dc/ // ConstraintAddress + ); + }); + it("token_program_output", async () => { + await rejectParams( + { + ...baseIxParams, + tokenProgramOutput: TEST_TOKEN_2022_PROGRAM_ID, + }, + /0x7dc/ // ConstraintAddress + ); + }); + }); + }); + }); + + async function rejectParams(params: TwoHopSwapV2Params, error: assert.AssertPredicate) { + await assert.rejects( + toTx(ctx, WhirlpoolIx.twoHopSwapV2Ix(ctx.program, params)).buildAndExecute(), + error + ); + } + + function getParamsFromPools( + pools: [InitPoolV2Params, InitPoolV2Params], + aToBs: boolean[], + tokenAccounts: { mint: PublicKey; account: PublicKey; tokenTrait: TokenTrait }[] + ) { + const [aToBOne, aToBTwo] = aToBs; + const tokenAccKeys = getTokenAccsForPoolsV2(pools, tokenAccounts); + + const whirlpoolOne = pools[0].whirlpoolPda.publicKey; + const whirlpoolTwo = pools[1].whirlpoolPda.publicKey; + const tokenMintOneA = pools[0].tokenMintA; + const tokenMintOneB = pools[0].tokenMintB; + const tokenMintTwoA = pools[1].tokenMintA; + const tokenMintTwoB = pools[1].tokenMintB; + const tokenProgramOneA = pools[0].tokenProgramA; + const tokenProgramOneB = pools[0].tokenProgramB; + const tokenProgramTwoA = pools[1].tokenProgramA; + const tokenProgramTwoB = pools[1].tokenProgramB; + const oracleOne = PDAUtil.getOracle(ctx.program.programId, whirlpoolOne).publicKey; + const oracleTwo = PDAUtil.getOracle(ctx.program.programId, whirlpoolTwo).publicKey; + return { + whirlpoolOne: pools[0].whirlpoolPda.publicKey, + whirlpoolTwo: pools[1].whirlpoolPda.publicKey, + oracleOne, + oracleTwo, + // mints + tokenMintInput: aToBOne ? tokenMintOneA : tokenMintOneB, + tokenMintIntermediate: aToBOne ? tokenMintOneB : tokenMintOneA, + tokenMintOutput: aToBTwo ? tokenMintTwoB : tokenMintTwoA, + // token programs + tokenProgramInput: aToBOne ? tokenProgramOneA : tokenProgramOneB, + tokenProgramIntermediate: aToBOne ? tokenProgramOneB : tokenProgramOneA, + tokenProgramOutput: aToBTwo ? tokenProgramTwoB : tokenProgramTwoA, + // accounts + tokenOwnerAccountInput: aToBOne ? tokenAccKeys[0] : tokenAccKeys[1], + tokenVaultOneInput: aToBOne ? pools[0].tokenVaultAKeypair.publicKey : pools[0].tokenVaultBKeypair.publicKey, + tokenVaultOneIntermediate: aToBOne ? pools[0].tokenVaultBKeypair.publicKey : pools[0].tokenVaultAKeypair.publicKey, + tokenVaultTwoIntermediate: aToBTwo ? pools[1].tokenVaultAKeypair.publicKey : pools[1].tokenVaultBKeypair.publicKey, + tokenVaultTwoOutput: aToBTwo ? pools[1].tokenVaultBKeypair.publicKey : pools[1].tokenVaultAKeypair.publicKey, + tokenOwnerAccountOutput: aToBTwo ? tokenAccKeys[3] : tokenAccKeys[2], + }; + } + + async function getTokenBalancesForVaults(pools: InitPoolParams[]) { + const accs = []; + for (const pool of pools) { + accs.push(pool.tokenVaultAKeypair.publicKey); + accs.push(pool.tokenVaultBKeypair.publicKey); + } + return getTokenBalances(accs); + } + + async function getTokenBalances(keys: PublicKey[]) { + return Promise.all( + keys.map(async (key) => new anchor.BN(await getTokenBalance(provider, key))) + ); + } +}); diff --git a/sdk/tests/sdk/router/router-util#priceImpact.test.ts b/sdk/tests/sdk/router/router-util#priceImpact.test.ts index 59d845af4..6f7671650 100644 --- a/sdk/tests/sdk/router/router-util#priceImpact.test.ts +++ b/sdk/tests/sdk/router/router-util#priceImpact.test.ts @@ -255,6 +255,10 @@ describe("RouterUtil - Price Impact tests", () => { estimatedEndTickIndex: 0, estimatedEndSqrtPrice: new BN(0), estimatedFeeAmount: new BN(0), + transferFee: { + deductingFromEstimatedAmountIn: new BN(0), + deductedFromEstimatedAmountOut: new BN(0), + }, }, snapshot: { aToB: hopParam.aToB, diff --git a/sdk/tests/sdk/types/anchor-types.test.ts b/sdk/tests/sdk/types/anchor-types.test.ts index b7db61cd9..4bedc72f0 100644 --- a/sdk/tests/sdk/types/anchor-types.test.ts +++ b/sdk/tests/sdk/types/anchor-types.test.ts @@ -11,6 +11,8 @@ describe("anchor-types", () => { [AccountName.Whirlpool]: 653, [AccountName.FeeTier]: 44, [AccountName.PositionBundle]: 136, + [AccountName.WhirlpoolsConfigExtension]: 616, + [AccountName.TokenBadge]: 200, }; Object.values(AccountName).forEach((name) => { try { diff --git a/sdk/tests/sdk/whirlpools/position-impl#collectFees.test.ts b/sdk/tests/sdk/whirlpools/position-impl#collectFees.test.ts index e34e44e87..c11f47e4c 100644 --- a/sdk/tests/sdk/whirlpools/position-impl#collectFees.test.ts +++ b/sdk/tests/sdk/whirlpools/position-impl#collectFees.test.ts @@ -15,9 +15,11 @@ import { toTx } from "../../../src"; import { IGNORE_CACHE } from "../../../src/network/public/fetcher"; -import { TickSpacing, ZERO_BN } from "../../utils"; +import { TEST_TOKEN_2022_PROGRAM_ID, TickSpacing, ZERO_BN } from "../../utils"; import { defaultConfirmOptions } from "../../utils/const"; import { WhirlpoolTestFixture } from "../../utils/fixture"; +import { TokenExtensionUtil } from "../../../src/utils/public/token-extension-util"; +import { WhirlpoolTestFixtureV2 } from "../../utils/v2/fixture-v2"; interface SharedTestContext { provider: anchor.AnchorProvider; @@ -120,6 +122,7 @@ describe("PositionImpl#collectFees()", () => { position: positionData, tickLower: tickLowerData, tickUpper: tickUpperData, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(ctx.fetcher, poolData, IGNORE_CACHE), }); assert.ok(quote.feeOwedA.gtn(0) || quote.feeOwedB.gtn(0)); @@ -158,6 +161,7 @@ describe("PositionImpl#collectFees()", () => { position: positionData, tickLower: tickLowerData, tickUpper: tickUpperData, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(testCtx.whirlpoolCtx.fetcher, poolData, IGNORE_CACHE), }); assert.notEqual(positionDataBefore, null); @@ -224,6 +228,7 @@ describe("PositionImpl#collectFees()", () => { position: positionData, tickLower: tickLowerData, tickUpper: tickUpperData, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(testCtx.whirlpoolCtx.fetcher, poolData, IGNORE_CACHE), }); const solBalanceBefore = await testCtx.provider.connection.getBalance(otherWallet.publicKey); @@ -259,4 +264,180 @@ describe("PositionImpl#collectFees()", () => { assert.ok(accountB && new BN(accountB.amount.toString()).eq(quote.feeOwedB)); }); }); + + async function accrueFeesV2(fixture: WhirlpoolTestFixtureV2) { + const ctx = testCtx.whirlpoolCtx; + const { + poolInitInfo, + positions: [positionInfo], + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + + const { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair } = poolInitInfo; + + const tickArrayPda = PDAUtil.getTickArray(ctx.program.programId, whirlpoolPda.publicKey, 22528); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + const pool = await testCtx.whirlpoolClient.getPool(whirlpoolPda.publicKey); + const position = await testCtx.whirlpoolClient.getPosition(positionInfo.publicKey); + + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(ctx.fetcher, pool.getData(), IGNORE_CACHE); + + // Accrue fees in token A + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + tokenMintA: tokenExtensionCtx.tokenMintWithProgramA.address, + tokenMintB: tokenExtensionCtx.tokenMintWithProgramB.address, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + ...await TokenExtensionUtil.getExtraAccountMetasForTransferHookForPool( + ctx.connection, + tokenExtensionCtx, + tokenAccountA, + tokenVaultAKeypair.publicKey, + ctx.wallet.publicKey, + tokenVaultBKeypair.publicKey, + tokenAccountB, + whirlpoolPda.publicKey, + ), + }) + ).buildAndExecute(); + + // Accrue fees in token B + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(5)), + amountSpecifiedIsInput: true, + aToB: false, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + tokenMintA: tokenExtensionCtx.tokenMintWithProgramA.address, + tokenMintB: tokenExtensionCtx.tokenMintWithProgramB.address, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + ...await TokenExtensionUtil.getExtraAccountMetasForTransferHookForPool( + ctx.connection, + tokenExtensionCtx, + tokenVaultAKeypair.publicKey, + tokenAccountA, + whirlpoolPda.publicKey, + tokenAccountB, + tokenVaultBKeypair.publicKey, + ctx.wallet.publicKey, + ), + }) + ).buildAndExecute(); + + const poolData = await pool.refreshData(); + const positionData = await position.refreshData(); + const tickLowerData = position.getLowerTickData(); + const tickUpperData = position.getLowerTickData(); + + const quote = collectFeesQuote({ + whirlpool: poolData, + position: positionData, + tickLower: tickLowerData, + tickUpper: tickUpperData, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(ctx.fetcher, poolData, IGNORE_CACHE), + }); + + assert.ok(quote.feeOwedA.gtn(0) || quote.feeOwedB.gtn(0)); + } + + context("when the whirlpool is SPL-only (TokenExtension)", () => { + it("should collect fees", async () => { + const fixture = await new WhirlpoolTestFixtureV2(testCtx.whirlpoolCtx).init({ + tokenTraitA: {isToken2022: true, hasTransferHookExtension: true}, + tokenTraitB: {isToken2022: true, hasTransferHookExtension: true}, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position + ], + }); + + await accrueFeesV2(fixture); + + const { positions, poolInitInfo } = fixture.getInfos(); + + const pool = await testCtx.whirlpoolClient.getPool(poolInitInfo.whirlpoolPda.publicKey); + const position = await testCtx.whirlpoolClient.getPosition(positions[0].publicKey); + + const positionDataBefore = await testCtx.whirlpoolCtx.fetcher.getPosition( + position.getAddress(), + IGNORE_CACHE + ); + + const otherWallet = anchor.web3.Keypair.generate(); + + const poolData = await pool.refreshData(); + const positionData = await position.refreshData(); + const tickLowerData = position.getLowerTickData(); + const tickUpperData = position.getLowerTickData(); + + const quote = collectFeesQuote({ + whirlpool: poolData, + position: positionData, + tickLower: tickLowerData, + tickUpper: tickUpperData, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(testCtx.whirlpoolCtx.fetcher, poolData, IGNORE_CACHE), + }); + + assert.notEqual(positionDataBefore, null); + + const tx = await position.collectFees( + true, + undefined, + otherWallet.publicKey, + testCtx.provider.wallet.publicKey, + testCtx.provider.wallet.publicKey, + IGNORE_CACHE + ); + + await tx.buildAndExecute(); + + const positionDataAfter = await testCtx.whirlpoolCtx.fetcher.getPosition( + position.getAddress(), + IGNORE_CACHE + ); + + assert.notEqual(positionDataAfter, null); + + const accountAPubkey = getAssociatedTokenAddressSync(poolInitInfo.tokenMintA, otherWallet.publicKey, undefined, TEST_TOKEN_2022_PROGRAM_ID); + const accountA = await testCtx.whirlpoolCtx.fetcher.getTokenInfo(accountAPubkey, IGNORE_CACHE); + assert.ok(accountA && new BN(accountA.amount.toString()).eq(quote.feeOwedA)); + + const accountBPubkey = getAssociatedTokenAddressSync(poolInitInfo.tokenMintB, otherWallet.publicKey, undefined, TEST_TOKEN_2022_PROGRAM_ID); + const accountB = await testCtx.whirlpoolCtx.fetcher.getTokenInfo(accountBPubkey, IGNORE_CACHE); + assert.ok(accountB && new BN(accountB.amount.toString()).eq(quote.feeOwedB)); + }); + }); + }); diff --git a/sdk/tests/sdk/whirlpools/position-impl#collectRewards.test.ts b/sdk/tests/sdk/whirlpools/position-impl#collectRewards.test.ts index d55619ccc..080f7bfa2 100644 --- a/sdk/tests/sdk/whirlpools/position-impl#collectRewards.test.ts +++ b/sdk/tests/sdk/whirlpools/position-impl#collectRewards.test.ts @@ -13,9 +13,11 @@ import { collectRewardsQuote } from "../../../src"; import { IGNORE_CACHE } from "../../../src/network/public/fetcher"; -import { TickSpacing, sleep } from "../../utils"; +import { TEST_TOKEN_2022_PROGRAM_ID, TickSpacing, sleep } from "../../utils"; import { defaultConfirmOptions } from "../../utils/const"; import { WhirlpoolTestFixture } from "../../utils/fixture"; +import { TokenExtensionUtil } from "../../../src/utils/public/token-extension-util"; +import { WhirlpoolTestFixtureV2 } from "../../utils/v2/fixture-v2"; interface SharedTestContext { provider: anchor.AnchorProvider; @@ -79,19 +81,20 @@ describe("PositionImpl#collectRewards()", () => { const preCollectPoolData = pool.getData(); // accrue rewards - await sleep(1200); - - await ( - await position.collectRewards( - rewards.map((r) => r.rewardMint), - true, - undefined, - otherWallet.publicKey, - testCtx.provider.wallet.publicKey, - testCtx.provider.wallet.publicKey, - IGNORE_CACHE - ) - ).buildAndExecute(); + await sleep(2000); + + const txs = await position.collectRewards( + rewards.map((r) => r.rewardMint), + true, + undefined, + otherWallet.publicKey, + testCtx.provider.wallet.publicKey, + testCtx.provider.wallet.publicKey, + IGNORE_CACHE + ); + for (const tx of txs) { + await tx.buildAndExecute(); + } // Verify the results fetched is the same as SDK estimate if the timestamp is the same const postCollectPoolData = await pool.refreshData(); @@ -100,18 +103,19 @@ describe("PositionImpl#collectRewards()", () => { position: position.getData(), tickLower: position.getLowerTickData(), tickUpper: position.getUpperTickData(), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(testCtx.whirlpoolCtx.fetcher, pool.getData(), IGNORE_CACHE), timeStampInSeconds: postCollectPoolData.rewardLastUpdatedTimestamp, }); // Check that the expectation is not zero for (let i = 0; i < NUM_REWARDS; i++) { - assert.ok(!quote[i]!.isZero()); + assert.ok(!quote.rewardOwed[i]!.isZero()); } for (let i = 0; i < NUM_REWARDS; i++) { const rewardATA = getAssociatedTokenAddressSync(rewards[i].rewardMint, otherWallet.publicKey); const rewardTokenAccount = await testCtx.whirlpoolCtx.fetcher.getTokenInfo(rewardATA, IGNORE_CACHE); - assert.equal(rewardTokenAccount?.amount.toString(), quote[i]?.toString()); + assert.equal(rewardTokenAccount?.amount.toString(), quote.rewardOwed[i]?.toString()); } }); }); @@ -148,19 +152,20 @@ describe("PositionImpl#collectRewards()", () => { const preCollectPoolData = pool.getData(); // accrue rewards - await sleep(1200); - - await ( - await position.collectRewards( - rewards.map((r) => r.rewardMint), - true, - undefined, - otherWallet.publicKey, - testCtx.provider.wallet.publicKey, - testCtx.provider.wallet.publicKey, - IGNORE_CACHE - ) - ).buildAndExecute(); + await sleep(2000); + + const txs = await position.collectRewards( + rewards.map((r) => r.rewardMint), + true, + undefined, + otherWallet.publicKey, + testCtx.provider.wallet.publicKey, + testCtx.provider.wallet.publicKey, + IGNORE_CACHE + ); + for (const tx of txs) { + await tx.buildAndExecute(); + } // Verify the results fetched is the same as SDK estimate if the timestamp is the same const postCollectPoolData = await pool.refreshData(); @@ -169,19 +174,98 @@ describe("PositionImpl#collectRewards()", () => { position: position.getData(), tickLower: position.getLowerTickData(), tickUpper: position.getUpperTickData(), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(testCtx.whirlpoolCtx.fetcher, pool.getData(), IGNORE_CACHE), timeStampInSeconds: postCollectPoolData.rewardLastUpdatedTimestamp, }); // Check that the expectation is not zero for (let i = 0; i < NUM_REWARDS; i++) { - assert.ok(!quote[i]!.isZero()); + assert.ok(!quote.rewardOwed[i]!.isZero()); } for (let i = 0; i < NUM_REWARDS; i++) { const rewardATA = getAssociatedTokenAddressSync(rewards[i].rewardMint, otherWallet.publicKey); const rewardTokenAccount = await testCtx.whirlpoolCtx.fetcher.getTokenInfo(rewardATA, IGNORE_CACHE); - assert.equal(rewardTokenAccount?.amount.toString(), quote[i]?.toString()); + assert.equal(rewardTokenAccount?.amount.toString(), quote.rewardOwed[i]?.toString()); + } + }); + }); + + + context("when the whirlpool is SPL-only (TokenExtension)", () => { + it("should collect rewards", async () => { + const fixture = await new WhirlpoolTestFixtureV2(testCtx.whirlpoolCtx).init({ + tokenTraitA: {isToken2022: true}, + tokenTraitB: {isToken2022: true}, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position + ], + rewards: [ + { + rewardTokenTrait: {isToken2022:true, hasTransferHookExtension: true}, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: {isToken2022:true, hasTransferHookExtension: true}, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: {isToken2022:true, hasTransferHookExtension: true}, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + + const { positions, poolInitInfo, rewards } = fixture.getInfos(); + + const pool = await testCtx.whirlpoolClient.getPool(poolInitInfo.whirlpoolPda.publicKey, IGNORE_CACHE); + const position = await testCtx.whirlpoolClient.getPosition(positions[0].publicKey, IGNORE_CACHE); + + const otherWallet = anchor.web3.Keypair.generate(); + const preCollectPoolData = pool.getData(); + + // accrue rewards + await sleep(2000); + + const txs = await position.collectRewards( + rewards.map((r) => r.rewardMint), + true, + undefined, + otherWallet.publicKey, + testCtx.provider.wallet.publicKey, + testCtx.provider.wallet.publicKey, + IGNORE_CACHE + ); + for (const tx of txs) { + await tx.buildAndExecute(); + } + + // Verify the results fetched is the same as SDK estimate if the timestamp is the same + const postCollectPoolData = await pool.refreshData(); + const quote = collectRewardsQuote({ + whirlpool: preCollectPoolData, + position: position.getData(), + tickLower: position.getLowerTickData(), + tickUpper: position.getUpperTickData(), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(testCtx.whirlpoolCtx.fetcher, pool.getData(), IGNORE_CACHE), + timeStampInSeconds: postCollectPoolData.rewardLastUpdatedTimestamp, + }); + + // Check that the expectation is not zero + for (let i = 0; i < NUM_REWARDS; i++) { + assert.ok(!quote.rewardOwed[i]!.isZero()); + } + + for (let i = 0; i < NUM_REWARDS; i++) { + const rewardATA = getAssociatedTokenAddressSync(rewards[i].rewardMint, otherWallet.publicKey, undefined, TEST_TOKEN_2022_PROGRAM_ID); + const rewardTokenAccount = await testCtx.whirlpoolCtx.fetcher.getTokenInfo(rewardATA, IGNORE_CACHE); + assert.equal(rewardTokenAccount?.amount.toString(), quote.rewardOwed[i]?.toString()); } }); }); + }); diff --git a/sdk/tests/sdk/whirlpools/position-impl.test.ts b/sdk/tests/sdk/whirlpools/position-impl.test.ts index beefb4378..30b704b8a 100644 --- a/sdk/tests/sdk/whirlpools/position-impl.test.ts +++ b/sdk/tests/sdk/whirlpools/position-impl.test.ts @@ -15,6 +15,9 @@ import { createAssociatedTokenAccount, TickSpacing, transferToken } from "../../ import { defaultConfirmOptions } from "../../utils/const"; import { initTestPool } from "../../utils/init-utils"; import { initPosition, mintTokensToTestAccount } from "../../utils/test-builders"; +import { TokenExtensionUtil } from "../../../src/utils/public/token-extension-util"; +import { initTestPoolV2, TokenTrait } from "../../utils/v2/init-utils-v2"; +import { mintTokensToTestAccountV2 } from "../../utils/v2/token-2022"; describe("position-impl", () => { const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); @@ -24,221 +27,268 @@ describe("position-impl", () => { const fetcher = ctx.fetcher; const client = buildWhirlpoolClient(ctx); - it("increase and decrease liquidity on position", async () => { - const { poolInitInfo } = await initTestPool( - ctx, - TickSpacing.Standard, - PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6) - ); - - // Create and mint tokens in this wallet - await mintTokensToTestAccount( - ctx.provider, - poolInitInfo.tokenMintA, - 10_500_000_000, - poolInitInfo.tokenMintB, - 10_500_000_000 - ); - - const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey); - const lowerTick = PriceMath.priceToTickIndex( - new Decimal(89), - pool.getTokenAInfo().decimals, - pool.getTokenBInfo().decimals - ); - const upperTick = PriceMath.priceToTickIndex( - new Decimal(120), - pool.getTokenAInfo().decimals, - pool.getTokenBInfo().decimals - ); - - // [Action] Initialize Tick Arrays - const initTickArrayTx = (await pool.initTickArrayForTicks([lowerTick, upperTick]))!; - await initTickArrayTx.buildAndExecute(); - - // [Action] Create a position at price 89, 120 with 50 token A - const lowerPrice = new Decimal(89); - const upperPrice = new Decimal(120); - const { positionAddress } = await initPosition( - ctx, - pool, - lowerPrice, - upperPrice, - poolInitInfo.tokenMintA, - 50 - ); - - // [Action] Increase liquidity by 70 tokens of tokenB - const position = await client.getPosition(positionAddress.publicKey, IGNORE_CACHE); - const preIncreaseData = position.getData(); - const increase_quote = increaseLiquidityQuoteByInputTokenUsingPriceSlippage( - poolInitInfo.tokenMintB, - new Decimal(70), - lowerTick, - upperTick, - Percentage.fromFraction(1, 100), - pool - ); - - await ( - await position.increaseLiquidity(increase_quote, false, ctx.wallet.publicKey) - ).buildAndExecute(); - - const postIncreaseData = await position.refreshData(); - const expectedPostIncreaseLiquidity = preIncreaseData.liquidity.add( - increase_quote.liquidityAmount - ); - assert.equal(postIncreaseData.liquidity.toString(), expectedPostIncreaseLiquidity.toString()); - - // [Action] Withdraw half of the liquidity away from the position and verify - const withdrawHalf = postIncreaseData.liquidity.div(new anchor.BN(2)); - const decrease_quote = decreaseLiquidityQuoteByLiquidity( - withdrawHalf, - Percentage.fromFraction(0, 100), - position, - pool - ); - - await (await position.decreaseLiquidity(decrease_quote, false)).buildAndExecute(); - - const postWithdrawData = await position.refreshData(); - const expectedPostWithdrawLiquidity = postIncreaseData.liquidity.sub( - decrease_quote.liquidityAmount - ); - assert.equal(postWithdrawData.liquidity.toString(), expectedPostWithdrawLiquidity.toString()); - }); + const tokenTraitVariations: { + tokenTraitA: TokenTrait; + tokenTraitB: TokenTrait; + }[] = [ + { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + }, + { + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: false }, + }, + { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: true }, + }, + { + // TransferHook is most difficult extension in transaction size + tokenTraitA: { isToken2022: true, hasTransferHookExtension: true }, + tokenTraitB: { isToken2022: true, hasTransferHookExtension: true }, + }, + ]; + tokenTraitVariations.forEach((tokenTraits) => { + describe(`tokenTraitA: ${ + tokenTraits.tokenTraitA.isToken2022 ? "Token2022" : "Token" + }, tokenTraitB: ${ + tokenTraits.tokenTraitB.isToken2022 ? "Token2022" : "Token" + }`, () => { + + it("increase and decrease liquidity on position", async () => { + const { poolInitInfo } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard, + PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6) + ); + + // Create and mint tokens in this wallet + await mintTokensToTestAccountV2( + ctx.provider, + poolInitInfo.tokenMintA, + tokenTraits.tokenTraitA, + 10_500_000_000, + poolInitInfo.tokenMintB, + tokenTraits.tokenTraitB, + 10_500_000_000 + ); + + const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey); + const lowerTick = PriceMath.priceToTickIndex( + new Decimal(89), + pool.getTokenAInfo().decimals, + pool.getTokenBInfo().decimals + ); + const upperTick = PriceMath.priceToTickIndex( + new Decimal(120), + pool.getTokenAInfo().decimals, + pool.getTokenBInfo().decimals + ); + + // [Action] Initialize Tick Arrays + const initTickArrayTx = (await pool.initTickArrayForTicks([lowerTick, upperTick]))!; + await initTickArrayTx.buildAndExecute(); + + // [Action] Create a position at price 89, 120 with 50 token A + const lowerPrice = new Decimal(89); + const upperPrice = new Decimal(120); + const { positionAddress } = await initPosition( + ctx, + pool, + lowerPrice, + upperPrice, + poolInitInfo.tokenMintA, + 50 + ); + + // [Action] Increase liquidity by 70 tokens of tokenB + const position = await client.getPosition(positionAddress.publicKey, IGNORE_CACHE); + const preIncreaseData = position.getData(); + const increase_quote = increaseLiquidityQuoteByInputTokenUsingPriceSlippage( + poolInitInfo.tokenMintB, + new Decimal(70), + lowerTick, + upperTick, + Percentage.fromFraction(1, 100), + pool, + await TokenExtensionUtil.buildTokenExtensionContext(fetcher, pool.getData(), IGNORE_CACHE), + ); + + await ( + await position.increaseLiquidity(increase_quote, false, ctx.wallet.publicKey) + ).buildAndExecute(); + + const postIncreaseData = await position.refreshData(); + const expectedPostIncreaseLiquidity = preIncreaseData.liquidity.add( + increase_quote.liquidityAmount + ); + assert.equal(postIncreaseData.liquidity.toString(), expectedPostIncreaseLiquidity.toString()); + + // [Action] Withdraw half of the liquidity away from the position and verify + const withdrawHalf = postIncreaseData.liquidity.div(new anchor.BN(2)); + const decrease_quote = decreaseLiquidityQuoteByLiquidity( + withdrawHalf, + Percentage.fromFraction(0, 100), + position, + pool, + await TokenExtensionUtil.buildTokenExtensionContext(fetcher, pool.getData(), IGNORE_CACHE), + ); + + await (await position.decreaseLiquidity(decrease_quote, false)).buildAndExecute(); + + const postWithdrawData = await position.refreshData(); + const expectedPostWithdrawLiquidity = postIncreaseData.liquidity.sub( + decrease_quote.liquidityAmount + ); + assert.equal(postWithdrawData.liquidity.toString(), expectedPostWithdrawLiquidity.toString()); + }); + + it("increase & decrease liquidity on position with a different destination, position wallet", async () => { + const { poolInitInfo } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard, + PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6) + ); + + // Create and mint tokens in this wallet + await mintTokensToTestAccountV2( + ctx.provider, + poolInitInfo.tokenMintA, + tokenTraits.tokenTraitA, + 10_500_000_000, + poolInitInfo.tokenMintB, + tokenTraits.tokenTraitB, + 10_500_000_000 + ); + + const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey); + const lowerTick = PriceMath.priceToTickIndex( + new Decimal(89), + pool.getTokenAInfo().decimals, + pool.getTokenBInfo().decimals + ); + const upperTick = PriceMath.priceToTickIndex( + new Decimal(120), + pool.getTokenAInfo().decimals, + pool.getTokenBInfo().decimals + ); + + // [Action] Initialize Tick Arrays + const initTickArrayTx = (await pool.initTickArrayForTicks([lowerTick, upperTick]))!; + await initTickArrayTx.buildAndExecute(); + + // [Action] Create a position at price 89, 120 with 50 token A + const lowerPrice = new Decimal(89); + const upperPrice = new Decimal(120); + const { positionMint, positionAddress } = await initPosition( + ctx, + pool, + lowerPrice, + upperPrice, + poolInitInfo.tokenMintA, + 50 + ); + + // [Action] Increase liquidity by 70 tokens of tokenB & create the ATA in the new source Wallet + const position = await client.getPosition(positionAddress.publicKey, IGNORE_CACHE); + const preIncreaseData = position.getData(); + const increase_quote = increaseLiquidityQuoteByInputTokenUsingPriceSlippage( + poolInitInfo.tokenMintB, + new Decimal(70), + lowerTick, + upperTick, + Percentage.fromFraction(1, 100), + pool, + await TokenExtensionUtil.buildTokenExtensionContext(fetcher, pool.getData(), IGNORE_CACHE), + ); + + await (await position.increaseLiquidity(increase_quote, false)).buildAndExecute(); + + const postIncreaseData = await position.refreshData(); + const expectedPostIncreaseLiquidity = preIncreaseData.liquidity.add( + increase_quote.liquidityAmount + ); + assert.equal(postIncreaseData.liquidity.toString(), expectedPostIncreaseLiquidity.toString()); + + // [Action] Withdraw half of the liquidity away from the position and verify + const withdrawHalf = postIncreaseData.liquidity.div(new anchor.BN(2)); + const decrease_quote = await decreaseLiquidityQuoteByLiquidity( + withdrawHalf, + Percentage.fromFraction(0, 100), + position, + pool, + await TokenExtensionUtil.buildTokenExtensionContext(fetcher, pool.getData(), IGNORE_CACHE), + ); + + // Transfer the position token to another wallet + const otherWallet = anchor.web3.Keypair.generate(); + const walletPositionTokenAccount = getAssociatedTokenAddressSync(positionMint, ctx.wallet.publicKey); + const newOwnerPositionTokenAccount = await createAssociatedTokenAccount( + ctx.provider, + positionMint, + otherWallet.publicKey, + ctx.wallet.publicKey + ); + await transferToken(provider, walletPositionTokenAccount, newOwnerPositionTokenAccount, 1); + + // Mint to this other wallet and increase more tokens + await mintTokensToTestAccountV2( + ctx.provider, + poolInitInfo.tokenMintA, + tokenTraits.tokenTraitA, + 10_500_000_000, + poolInitInfo.tokenMintB, + tokenTraits.tokenTraitB, + 10_500_000_000, + otherWallet.publicKey + ); + const increaseQuoteFromOtherWallet = increaseLiquidityQuoteByInputTokenUsingPriceSlippage( + poolInitInfo.tokenMintB, + new Decimal(80), + lowerTick, + upperTick, + Percentage.fromFraction(1, 100), + pool, + await TokenExtensionUtil.buildTokenExtensionContext(fetcher, pool.getData(), IGNORE_CACHE), + ); + await ( + await position.increaseLiquidity( + increaseQuoteFromOtherWallet, + true, + otherWallet.publicKey, + otherWallet.publicKey + ) + ) + .addSigner(otherWallet) + .buildAndExecute(); + + const postSecondIncreaseData = await position.refreshData(); + + // Withdraw liquidity into another wallet + const destinationWallet = anchor.web3.Keypair.generate(); + await ( + await position.decreaseLiquidity( + decrease_quote, + true, + destinationWallet.publicKey, + otherWallet.publicKey + ) + ) + .addSigner(otherWallet) + .buildAndExecute(); + + const postWithdrawData = await position.refreshData(); + const expectedPostWithdrawLiquidity = postSecondIncreaseData.liquidity.sub( + decrease_quote.liquidityAmount + ); + assert.equal(postWithdrawData.liquidity.toString(), expectedPostWithdrawLiquidity.toString()); + }); - it("increase & decrease liquidity on position with a different destination, position wallet", async () => { - const { poolInitInfo } = await initTestPool( - ctx, - TickSpacing.Standard, - PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6) - ); - - // Create and mint tokens in this wallet - await mintTokensToTestAccount( - ctx.provider, - poolInitInfo.tokenMintA, - 10_500_000_000, - poolInitInfo.tokenMintB, - 10_500_000_000 - ); - - const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey); - const lowerTick = PriceMath.priceToTickIndex( - new Decimal(89), - pool.getTokenAInfo().decimals, - pool.getTokenBInfo().decimals - ); - const upperTick = PriceMath.priceToTickIndex( - new Decimal(120), - pool.getTokenAInfo().decimals, - pool.getTokenBInfo().decimals - ); - - // [Action] Initialize Tick Arrays - const initTickArrayTx = (await pool.initTickArrayForTicks([lowerTick, upperTick]))!; - await initTickArrayTx.buildAndExecute(); - - // [Action] Create a position at price 89, 120 with 50 token A - const lowerPrice = new Decimal(89); - const upperPrice = new Decimal(120); - const { positionMint, positionAddress } = await initPosition( - ctx, - pool, - lowerPrice, - upperPrice, - poolInitInfo.tokenMintA, - 50 - ); - - // [Action] Increase liquidity by 70 tokens of tokenB & create the ATA in the new source Wallet - const position = await client.getPosition(positionAddress.publicKey, IGNORE_CACHE); - const preIncreaseData = position.getData(); - const increase_quote = increaseLiquidityQuoteByInputTokenUsingPriceSlippage( - poolInitInfo.tokenMintB, - new Decimal(70), - lowerTick, - upperTick, - Percentage.fromFraction(1, 100), - pool - ); - - await (await position.increaseLiquidity(increase_quote, false)).buildAndExecute(); - - const postIncreaseData = await position.refreshData(); - const expectedPostIncreaseLiquidity = preIncreaseData.liquidity.add( - increase_quote.liquidityAmount - ); - assert.equal(postIncreaseData.liquidity.toString(), expectedPostIncreaseLiquidity.toString()); - - // [Action] Withdraw half of the liquidity away from the position and verify - const withdrawHalf = postIncreaseData.liquidity.div(new anchor.BN(2)); - const decrease_quote = await decreaseLiquidityQuoteByLiquidity( - withdrawHalf, - Percentage.fromFraction(0, 100), - position, - pool - ); - - // Transfer the position token to another wallet - const otherWallet = anchor.web3.Keypair.generate(); - const walletPositionTokenAccount = getAssociatedTokenAddressSync(positionMint, ctx.wallet.publicKey); - const newOwnerPositionTokenAccount = await createAssociatedTokenAccount( - ctx.provider, - positionMint, - otherWallet.publicKey, - ctx.wallet.publicKey - ); - await transferToken(provider, walletPositionTokenAccount, newOwnerPositionTokenAccount, 1); - - // Mint to this other wallet and increase more tokens - await mintTokensToTestAccount( - ctx.provider, - poolInitInfo.tokenMintA, - 10_500_000_000, - poolInitInfo.tokenMintB, - 10_500_000_000, - otherWallet.publicKey - ); - const increaseQuoteFromOtherWallet = increaseLiquidityQuoteByInputTokenUsingPriceSlippage( - poolInitInfo.tokenMintB, - new Decimal(80), - lowerTick, - upperTick, - Percentage.fromFraction(1, 100), - pool - ); - await ( - await position.increaseLiquidity( - increaseQuoteFromOtherWallet, - true, - otherWallet.publicKey, - otherWallet.publicKey - ) - ) - .addSigner(otherWallet) - .buildAndExecute(); - - const postSecondIncreaseData = await position.refreshData(); - - // Withdraw liquidity into another wallet - const destinationWallet = anchor.web3.Keypair.generate(); - await ( - await position.decreaseLiquidity( - decrease_quote, - true, - destinationWallet.publicKey, - otherWallet.publicKey - ) - ) - .addSigner(otherWallet) - .buildAndExecute(); - - const postWithdrawData = await position.refreshData(); - const expectedPostWithdrawLiquidity = postSecondIncreaseData.liquidity.sub( - decrease_quote.liquidityAmount - ); - assert.equal(postWithdrawData.liquidity.toString(), expectedPostWithdrawLiquidity.toString()); + }); }); }); diff --git a/sdk/tests/sdk/whirlpools/quote/decrease-liquidity-quote.test.ts b/sdk/tests/sdk/whirlpools/quote/decrease-liquidity-quote.test.ts index 411da690a..13d61a192 100644 --- a/sdk/tests/sdk/whirlpools/quote/decrease-liquidity-quote.test.ts +++ b/sdk/tests/sdk/whirlpools/quote/decrease-liquidity-quote.test.ts @@ -2,7 +2,9 @@ import * as assert from "assert"; import { PriceMath, decreaseLiquidityQuoteByLiquidityWithParams } from "../../../../src"; import { BN } from "bn.js"; import { PublicKey } from "@solana/web3.js"; -import { Percentage } from "@orca-so/common-sdk"; +import { MintWithTokenProgram, Percentage } from "@orca-so/common-sdk"; +import { NO_TOKEN_EXTENSION_CONTEXT, TokenExtensionContextForPool } from "../../../../src/utils/public/token-extension-util"; +import { TEST_TOKEN_PROGRAM_ID } from "../../../utils"; describe("edge cases", () => { const tokenMintA = new PublicKey("orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE"); @@ -16,6 +18,7 @@ describe("edge cases", () => { tickUpperIndex: 64, tickCurrentIndex: 0, slippageTolerance: Percentage.fromFraction(0, 100), + tokenExtensionCtx: NO_TOKEN_EXTENSION_CONTEXT, // TokenExtension is not related to this test }); assert.ok(quote.tokenEstA.gtn(0)); @@ -34,6 +37,7 @@ describe("edge cases", () => { tickUpperIndex: 64, tickCurrentIndex: 0, slippageTolerance: Percentage.fromFraction(0, 100), + tokenExtensionCtx: NO_TOKEN_EXTENSION_CONTEXT, // TokenExtension is not related to this test }); assert.ok(quote.tokenEstA.gtn(0)); @@ -50,6 +54,7 @@ describe("edge cases", () => { tickUpperIndex: 64, tickCurrentIndex: 64, slippageTolerance: Percentage.fromFraction(0, 100), + tokenExtensionCtx: NO_TOKEN_EXTENSION_CONTEXT, // TokenExtension is not related to this test }); assert.ok(quote.tokenEstA.isZero()); diff --git a/sdk/tests/sdk/whirlpools/quote/increase-liquidity-quote-by-input-token.test.ts b/sdk/tests/sdk/whirlpools/quote/increase-liquidity-quote-by-input-token.test.ts index 098b3c44b..f4aea420d 100644 --- a/sdk/tests/sdk/whirlpools/quote/increase-liquidity-quote-by-input-token.test.ts +++ b/sdk/tests/sdk/whirlpools/quote/increase-liquidity-quote-by-input-token.test.ts @@ -12,6 +12,7 @@ import { getLiquidityFromTokenA, getLiquidityFromTokenB, } from "../../../../src/utils/position-util"; +import { NO_TOKEN_EXTENSION_CONTEXT } from "../../../../src/utils/public/token-extension-util"; function getTestSlippageRange(currIndex: number, slippage: Percentage) { const sqrtPrice = PriceMath.tickIndexToSqrtPriceX64(currIndex); @@ -230,6 +231,7 @@ variations.forEach(([currentTickIndex, isTokenA, slippage]) => { tickLowerIndex: pTickLowerIndex, tickUpperIndex: pTickUpperIndex, tickCurrentIndex, + tokenExtensionCtx: NO_TOKEN_EXTENSION_CONTEXT, // TokenExtension is not related to this test slippageTolerance: slippage, }); @@ -250,6 +252,7 @@ variations.forEach(([currentTickIndex, isTokenA, slippage]) => { liquidity, sqrtPrice: sqrtPrice, slippageTolerance: slippage, + tokenExtensionCtx: NO_TOKEN_EXTENSION_CONTEXT, // TokenExtension is not related to this test }); const { @@ -294,6 +297,7 @@ describe("edge cases for old slippage", () => { tickUpperIndex: 64, tickCurrentIndex: 0, slippageTolerance: Percentage.fromFraction(0, 100), + tokenExtensionCtx: NO_TOKEN_EXTENSION_CONTEXT, // TokenExtension is not related to this test }); assert.ok(quote.liquidityAmount.isZero()); @@ -314,6 +318,7 @@ describe("edge cases for old slippage", () => { tickUpperIndex: 64, tickCurrentIndex: 0, slippageTolerance: Percentage.fromFraction(0, 100), + tokenExtensionCtx: NO_TOKEN_EXTENSION_CONTEXT, // TokenExtension is not related to this test }); assert.ok(quote.liquidityAmount.gtn(0)); @@ -338,6 +343,7 @@ describe("edge cases for old slippage", () => { tickUpperIndex: 64, tickCurrentIndex: 0, slippageTolerance: Percentage.fromFraction(0, 100), + tokenExtensionCtx: NO_TOKEN_EXTENSION_CONTEXT, // TokenExtension is not related to this test }); assert.ok(quote.liquidityAmount.gtn(0)); @@ -362,6 +368,7 @@ describe("edge cases for old slippage", () => { tickUpperIndex: 64, tickCurrentIndex: 0, slippageTolerance: Percentage.fromFraction(0, 100), + tokenExtensionCtx: NO_TOKEN_EXTENSION_CONTEXT, // TokenExtension is not related to this test }); assert.ok(quote.liquidityAmount.gtn(0)); @@ -382,6 +389,7 @@ describe("edge cases for old slippage", () => { tickUpperIndex: 64, tickCurrentIndex: 64, slippageTolerance: Percentage.fromFraction(0, 100), + tokenExtensionCtx: NO_TOKEN_EXTENSION_CONTEXT, // TokenExtension is not related to this test }); assert.ok(quote.liquidityAmount.isZero()); @@ -402,6 +410,7 @@ describe("edge cases for old slippage", () => { tickUpperIndex: 64, tickCurrentIndex: 64, slippageTolerance: Percentage.fromFraction(0, 100), + tokenExtensionCtx: NO_TOKEN_EXTENSION_CONTEXT, // TokenExtension is not related to this test }); assert.ok(quote.liquidityAmount.gtn(0)); diff --git a/sdk/tests/sdk/whirlpools/quote/increase-liquidity-quote-by-liq.test.ts b/sdk/tests/sdk/whirlpools/quote/increase-liquidity-quote-by-liq.test.ts index 5514cc110..63022c589 100644 --- a/sdk/tests/sdk/whirlpools/quote/increase-liquidity-quote-by-liq.test.ts +++ b/sdk/tests/sdk/whirlpools/quote/increase-liquidity-quote-by-liq.test.ts @@ -6,6 +6,7 @@ import { getTokenAFromLiquidity, getTokenBFromLiquidity, } from "../../../../src/utils/position-util"; +import { NO_TOKEN_EXTENSION_CONTEXT } from "../../../../src/utils/public/token-extension-util"; const variations = [ [0, Percentage.fromFraction(1, 1000), new BN(17733543)] as const, @@ -198,6 +199,7 @@ variations.forEach(([currentTickIndex, slippage, liquidity]) => { tickUpperIndex: pTickUpperIndex, tickCurrentIndex, slippageTolerance: params.slippageTolerance, + tokenExtensionCtx: NO_TOKEN_EXTENSION_CONTEXT, // TokenExtension is not related to this test }); const { diff --git a/sdk/tests/sdk/whirlpools/swap/swap-array.test.ts b/sdk/tests/sdk/whirlpools/swap/swap-array.test.ts index f26d0c3a0..7175841e3 100644 --- a/sdk/tests/sdk/whirlpools/swap/swap-array.test.ts +++ b/sdk/tests/sdk/whirlpools/swap/swap-array.test.ts @@ -22,6 +22,7 @@ import { setupSwapTest } from "../../../utils/swap-test-utils"; import { getTickArrays } from "../../../utils/testDataTypes"; +import { TokenExtensionUtil } from "../../../../src/utils/public/token-extension-util"; describe("swap arrays test", () => { const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); @@ -344,6 +345,8 @@ describe("swap arrays test", () => { fetcher, IGNORE_CACHE ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + assert.throws( () => swapQuoteWithParams( @@ -355,6 +358,7 @@ describe("swap arrays test", () => { tickArrays, sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), otherAmountThreshold: ZERO, + tokenExtensionCtx, }, slippageTolerance ), @@ -395,6 +399,8 @@ describe("swap arrays test", () => { fetcher, IGNORE_CACHE ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + assert.throws( () => swapQuoteWithParams( @@ -406,6 +412,7 @@ describe("swap arrays test", () => { tickArrays, sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), otherAmountThreshold: ZERO, + tokenExtensionCtx, }, slippageTolerance ), @@ -445,6 +452,8 @@ describe("swap arrays test", () => { fetcher, IGNORE_CACHE ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + assert.throws( () => swapQuoteWithParams( @@ -456,6 +465,7 @@ describe("swap arrays test", () => { tickArrays, sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), otherAmountThreshold: ZERO, + tokenExtensionCtx, }, slippageTolerance ), @@ -495,6 +505,7 @@ describe("swap arrays test", () => { fetcher, IGNORE_CACHE ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); assert.throws( () => @@ -507,6 +518,7 @@ describe("swap arrays test", () => { tickArrays, sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), otherAmountThreshold: ZERO, + tokenExtensionCtx, }, slippageTolerance ), @@ -543,6 +555,8 @@ describe("swap arrays test", () => { AddressUtil.toPubKey(whirlpool.getAddress()), fetcher ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + assert.throws( () => swapQuoteWithParams( @@ -554,6 +568,7 @@ describe("swap arrays test", () => { tickArrays, sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), otherAmountThreshold: ZERO, + tokenExtensionCtx, }, slippageTolerance ), @@ -595,6 +610,8 @@ describe("swap arrays test", () => { AddressUtil.toPubKey(whirlpool.getAddress()), fetcher ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + assert.throws( () => swapQuoteWithParams( @@ -606,6 +623,7 @@ describe("swap arrays test", () => { tickArrays, sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), otherAmountThreshold: ZERO, + tokenExtensionCtx, }, slippageTolerance ), @@ -650,6 +668,8 @@ describe("swap arrays test", () => { AddressUtil.toPubKey(whirlpool.getAddress()), fetcher ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + const tradeAmount = new BN("33588"); const quote = swapQuoteWithParams( { @@ -660,6 +680,7 @@ describe("swap arrays test", () => { tickArrays, sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), otherAmountThreshold: ZERO, + tokenExtensionCtx, }, slippageTolerance ); @@ -708,6 +729,8 @@ describe("swap arrays test", () => { AddressUtil.toPubKey(whirlpool.getAddress()), fetcher ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + const tradeAmount = new BN("33588"); const quote = swapQuoteWithParams( { @@ -718,6 +741,7 @@ describe("swap arrays test", () => { tickArrays, sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), otherAmountThreshold: ZERO, + tokenExtensionCtx, }, slippageTolerance ); diff --git a/sdk/tests/sdk/whirlpools/swap/swap-traverse.test.ts b/sdk/tests/sdk/whirlpools/swap/swap-traverse.test.ts index e46e2adcf..29bc90c69 100644 --- a/sdk/tests/sdk/whirlpools/swap/swap-traverse.test.ts +++ b/sdk/tests/sdk/whirlpools/swap/swap-traverse.test.ts @@ -19,6 +19,7 @@ import { setupSwapTest } from "../../../utils/swap-test-utils"; import { getVaultAmounts } from "../../../utils/whirlpools-test-utils"; +import { TokenExtensionUtil } from "../../../../src/utils/public/token-extension-util"; describe("swap traversal tests", () => { const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); @@ -1364,6 +1365,8 @@ describe("swap traversal tests", () => { fetcher, IGNORE_CACHE ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + assert.throws( () => swapQuoteWithParams( @@ -1375,6 +1378,7 @@ describe("swap traversal tests", () => { tickArrays, sqrtPriceLimit: new BN(MIN_SQRT_PRICE).subn(1), otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + tokenExtensionCtx, }, slippageTolerance ), @@ -1416,6 +1420,8 @@ describe("swap traversal tests", () => { fetcher, IGNORE_CACHE ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + assert.throws( () => swapQuoteWithParams( @@ -1427,6 +1433,7 @@ describe("swap traversal tests", () => { tickArrays, sqrtPriceLimit: new BN(MAX_SQRT_PRICE).addn(1), otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + tokenExtensionCtx, }, slippageTolerance ), diff --git a/sdk/tests/sdk/whirlpools/swap/v2/swap-array.test.ts b/sdk/tests/sdk/whirlpools/swap/v2/swap-array.test.ts new file mode 100644 index 000000000..d71fcb6fd --- /dev/null +++ b/sdk/tests/sdk/whirlpools/swap/v2/swap-array.test.ts @@ -0,0 +1,914 @@ +import * as anchor from "@coral-xyz/anchor"; +import { AddressUtil, Percentage, ZERO } from "@orca-so/common-sdk"; +import * as assert from "assert"; +import BN from "bn.js"; +import { + PriceMath, + SwapUtils, + TICK_ARRAY_SIZE, + WhirlpoolContext, + buildWhirlpoolClient, + swapQuoteByInputToken, + swapQuoteWithParams +} from "../../../../../src"; +import { SwapErrorCode, WhirlpoolsError } from "../../../../../src/errors/errors"; +import { IGNORE_CACHE } from "../../../../../src/network/public/fetcher"; +import { adjustForSlippage } from "../../../../../src/utils/position-util"; +import { TickSpacing } from "../../../../utils"; +import { defaultConfirmOptions } from "../../../../utils/const"; +import { + arrayTickIndexToTickIndex, + buildPosition, +} from "../../../../utils/swap-test-utils"; +import { + setupSwapTestV2 +} from "../../../../utils/v2/swap-test-utils-v2"; +import { getTickArrays } from "../../../../utils/testDataTypes"; +import { TokenExtensionUtil } from "../../../../../src/utils/public/token-extension-util"; +import { TokenTrait } from "../../../../utils/v2/init-utils-v2"; + +describe("swap arrays test (v2)", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + const client = buildWhirlpoolClient(ctx); + const tickSpacing = TickSpacing.SixtyFour; + const slippageTolerance = Percentage.fromFraction(0, 100); + + const tokenTraitVariations: { + tokenTraitA: TokenTrait; + tokenTraitB: TokenTrait; + }[] = [ + { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + }, + { + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: false }, + }, + { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: true }, + }, + { + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + }, + ]; + tokenTraitVariations.forEach((tokenTraits) => { + describe(`tokenTraitA: ${ + tokenTraits.tokenTraitA.isToken2022 ? "Token2022" : "Token" + }, tokenTraitB: ${ + tokenTraits.tokenTraitB.isToken2022 ? "Token2022" : "Token" + }`, () => { + + /** + * |--------------------|xxxxxxxxxxxxxxxxx|-c2---c1-----------| + */ + it("3 sequential arrays, 2nd array not initialized, use tickArray0 only, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const tradeAmount = new BN(10000); + const quote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + tradeAmount, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + + // Verify with an actual swap. + // estimatedEndTickIndex is 8446 (arrayIndex: 1) + assert.equal(quote.aToB, true); + assert.equal(quote.amountSpecifiedIsInput, true); + assert.equal( + quote.sqrtPriceLimit.toString(), + SwapUtils.getDefaultSqrtPriceLimit(true).toString() + ); + assert.equal( + quote.otherAmountThreshold.toString(), + adjustForSlippage(quote.estimatedAmountOut, slippageTolerance, false).toString() + ); + assert.equal(quote.estimatedAmountIn.toString(), tradeAmount); + assert.doesNotThrow(async () => await (await whirlpool.swap(quote)).buildAndExecute()); + }); + + /** + * |--------------------|xxxxxxxxxxxxxc2xx|------c1-----------| + */ + it("3 sequential arrays, 2nd array not initialized, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + // estimatedEndTickIndex is 4091 (arrayIndex: 0 (not initialized)) + const whirlpoolData = await whirlpool.refreshData(); + const expectedError = "Swap input value traversed too many arrays."; + await assert.rejects( + swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + new BN(40_000_000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ), + (err: Error) => err.message.indexOf(expectedError) != -1 + ); + }); + + /** + * |-------------c1--c2-|xxxxxxxxxxxxxxxxx|-------------------| + */ + it("3 sequential arrays, 2nd array not initialized, use tickArray0 only, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const tradeAmount = new BN(10000); + const quote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + tradeAmount, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + + // Verify with an actual swap. + // estimatedEndTickIndex is -2816 (arrayIndex: -1) + assert.equal(quote.aToB, false); + assert.equal(quote.amountSpecifiedIsInput, true); + assert.equal( + quote.sqrtPriceLimit.toString(), + SwapUtils.getDefaultSqrtPriceLimit(false).toString() + ); + assert.equal( + quote.otherAmountThreshold.toString(), + adjustForSlippage(quote.estimatedAmountOut, slippageTolerance, false).toString() + ); + assert.equal(quote.estimatedAmountIn.toString(), tradeAmount); + assert.doesNotThrow(async () => await (await whirlpool.swap(quote)).buildAndExecute()); + }); + + /** + * |-------------c1-----|xxc2xxxxxxxxxxxxx|-------------------| + */ + it("3 sequential arrays, 2nd array not initialized, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + // estimatedEndTickIndex is 556 (arrayIndex: 0 (not initialized)) + const whirlpoolData = await whirlpool.refreshData(); + const expectedError = "Swap input value traversed too many arrays."; + await assert.rejects( + swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + new BN(40_000_000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ), + (err: Error) => err.message.indexOf(expectedError) != -1 + ); + }); + + /** + * |xxxxxxxxxxxxxxxxxxxx|xxxxxxxxxxxxxxxxx|-c2---c1-----------| + */ + it("3 sequential arrays, 2nd array and 3rd array not initialized, use tickArray0 only, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const tradeAmount = new BN(10000); + const quote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + tradeAmount, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + + // Verify with an actual swap. + // estimatedEndTickIndex is 8446 (arrayIndex: 1) + assert.equal(quote.aToB, true); + assert.equal(quote.amountSpecifiedIsInput, true); + assert.equal( + quote.sqrtPriceLimit.toString(), + SwapUtils.getDefaultSqrtPriceLimit(true).toString() + ); + assert.equal( + quote.otherAmountThreshold.toString(), + adjustForSlippage(quote.estimatedAmountOut, slippageTolerance, false).toString() + ); + assert.equal(quote.estimatedAmountIn.toString(), tradeAmount); + assert.doesNotThrow(async () => await (await whirlpool.swap(quote)).buildAndExecute()); + }); + + /** + * |-------------c1--c2-|xxxxxxxxxxxxxxxxx|xxxxxxxxxxxxxxxxxxx| + */ + it("3 sequential arrays, 2nd array and 3rd array not initialized, use tickArray0 only, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const tradeAmount = new BN(10000); + const quote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + tradeAmount, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + + // Verify with an actual swap. + // estimatedEndTickIndex is -2816 (arrayIndex: -1) + assert.equal(quote.aToB, false); + assert.equal(quote.amountSpecifiedIsInput, true); + assert.equal( + quote.sqrtPriceLimit.toString(), + SwapUtils.getDefaultSqrtPriceLimit(false).toString() + ); + assert.equal( + quote.otherAmountThreshold.toString(), + adjustForSlippage(quote.estimatedAmountOut, slippageTolerance, false).toString() + ); + assert.equal(quote.estimatedAmountIn.toString(), tradeAmount); + assert.doesNotThrow(async () => await (await whirlpool.swap(quote)).buildAndExecute()); + }); + + /** + * c1|------------------|-----------------|-------------------| + */ + it("3 sequential arrays does not contain curr_tick_index, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -2, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const aToB = true; + const tickArrays = await SwapUtils.getTickArrays( + arrayTickIndexToTickIndex({ arrayIndex: 0, offsetIndex: 10 }, tickSpacing), + tickSpacing, + aToB, + ctx.program.programId, + whirlpool.getAddress(), + fetcher, + IGNORE_CACHE + ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + + assert.throws( + () => + swapQuoteWithParams( + { + aToB, + amountSpecifiedIsInput: true, + tokenAmount: new BN("10000"), + whirlpoolData, + tickArrays, + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + otherAmountThreshold: ZERO, + tokenExtensionCtx, + }, + slippageTolerance + ), + (err) => (err as WhirlpoolsError).errorCode === SwapErrorCode.TickArraySequenceInvalid + ); + }); + + /** + * |--------------------|-----------------|-------------------|c1 + */ + it("3 sequential arrays does not contain curr_tick_index, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 2, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.getData(); + const aToB = false; + const tickArrays = await SwapUtils.getTickArrays( + arrayTickIndexToTickIndex({ arrayIndex: 0, offsetIndex: 10 }, tickSpacing), + tickSpacing, + aToB, + ctx.program.programId, + whirlpool.getAddress(), + fetcher, + IGNORE_CACHE + ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + + assert.throws( + () => + swapQuoteWithParams( + { + aToB, + amountSpecifiedIsInput: true, + tokenAmount: new BN("10000"), + whirlpoolData, + tickArrays, + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + otherAmountThreshold: ZERO, + tokenExtensionCtx, + }, + slippageTolerance + ), + (err) => (err as WhirlpoolsError).errorCode === SwapErrorCode.TickArraySequenceInvalid + ); + }); + + /** + * |--------------------|------c1---------|-------------------| + */ + it("3 sequential arrays, 2nd array contains curr_tick_index, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 0, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const aToB = true; + const tickArrays = await SwapUtils.getTickArrays( + arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 10 }, tickSpacing), + tickSpacing, + aToB, + ctx.program.programId, + whirlpool.getAddress(), + fetcher, + IGNORE_CACHE + ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + + assert.throws( + () => + swapQuoteWithParams( + { + aToB, + amountSpecifiedIsInput: true, + tokenAmount: new BN("10000"), + whirlpoolData, + tickArrays, + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + otherAmountThreshold: ZERO, + tokenExtensionCtx, + }, + slippageTolerance + ), + (err) => (err as WhirlpoolsError).errorCode === SwapErrorCode.TickArraySequenceInvalid + ); + }); + + /** + * |--------------------|------c1---------|-------------------| + */ + it("3 sequential arrays, 2nd array contains curr_tick_index, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 0, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264, 16896], + fundedPositions: [ + buildPosition( + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const aToB = false; + const tickArrays = await SwapUtils.getTickArrays( + arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 10 }, tickSpacing), + tickSpacing, + aToB, + ctx.program.programId, + whirlpool.getAddress(), + fetcher, + IGNORE_CACHE + ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + + assert.throws( + () => + swapQuoteWithParams( + { + aToB, + amountSpecifiedIsInput: true, + tokenAmount: new BN("10000"), + whirlpoolData, + tickArrays, + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + otherAmountThreshold: ZERO, + tokenExtensionCtx, + }, + slippageTolerance + ), + (err) => (err as WhirlpoolsError).errorCode === SwapErrorCode.TickArraySequenceInvalid + ); + }); + + /** + * |---a-c2--(5632)-----|------(0)--------|---c1--(11264)--a-| + */ + it("on first array, 2nd array is not sequential, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 2, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + { arrayIndex: 1, offsetIndex: 10 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const aToB = true; + const tickArrays = await getTickArrays( + [11264, 0, 5632], + ctx, + AddressUtil.toPubKey(whirlpool.getAddress()), + fetcher + ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + + assert.throws( + () => + swapQuoteWithParams( + { + aToB, + amountSpecifiedIsInput: true, + tokenAmount: new BN("10000"), + whirlpoolData, + tickArrays, + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + otherAmountThreshold: ZERO, + tokenExtensionCtx, + }, + slippageTolerance + ), + (err) => { + const whirlErr = err as WhirlpoolsError; + const errorCodeMatch = whirlErr.errorCode === SwapErrorCode.TickArraySequenceInvalid; + const messageMatch = whirlErr.message.indexOf("TickArray at index 1 is unexpected") >= 0; + return errorCodeMatch && messageMatch; + } + ); + }); + + /** + * |-a--(-11264)---c1---|--------(0)------|----(-5632)---c2--a-| + */ + it("on first array, 2nd array is not sequential, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -2, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + { arrayIndex: -2, offsetIndex: 10 }, + { arrayIndex: -1, offsetIndex: TICK_ARRAY_SIZE - 2 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const aToB = false; + const tickArrays = await getTickArrays( + [-11264, 0, -5632], + ctx, + AddressUtil.toPubKey(whirlpool.getAddress()), + fetcher + ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + + assert.throws( + () => + swapQuoteWithParams( + { + aToB, + amountSpecifiedIsInput: true, + tokenAmount: new BN("10000"), + whirlpoolData, + tickArrays, + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + otherAmountThreshold: ZERO, + tokenExtensionCtx, + }, + slippageTolerance + ), + (err) => { + const whirlErr = err as WhirlpoolsError; + const errorCodeMatch = whirlErr.errorCode === SwapErrorCode.TickArraySequenceInvalid; + const messageMatch = whirlErr.message.indexOf("TickArray at index 1 is unexpected") >= 0; + return errorCodeMatch && messageMatch; + } + ); + }); + + /** + * |-------(5632)------|-------(5632)------|---c2--(5632)-c1---| + */ + it("3 identical arrays, 1st contains curr_tick_index, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex( + { arrayIndex: 1, offsetIndex: TICK_ARRAY_SIZE - 4 }, + tickSpacing + ); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [5632], + fundedPositions: [ + buildPosition( + { arrayIndex: 1, offsetIndex: 0 }, + { arrayIndex: 1, offsetIndex: TICK_ARRAY_SIZE - 1 }, + tickSpacing, + new BN(250_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const aToB = true; + const tickArrays = await getTickArrays( + [5632, 5632, 5632], + ctx, + AddressUtil.toPubKey(whirlpool.getAddress()), + fetcher + ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + + const tradeAmount = new BN("33588"); + const quote = swapQuoteWithParams( + { + aToB, + amountSpecifiedIsInput: true, + tokenAmount: tradeAmount, + whirlpoolData, + tickArrays, + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + otherAmountThreshold: ZERO, + tokenExtensionCtx, + }, + slippageTolerance + ); + + // Verify with an actual swap. + assert.equal(quote.aToB, aToB); + assert.equal(quote.amountSpecifiedIsInput, true); + assert.equal( + quote.sqrtPriceLimit.toString(), + SwapUtils.getDefaultSqrtPriceLimit(aToB).toString() + ); + assert.equal( + quote.otherAmountThreshold.toString(), + adjustForSlippage(quote.estimatedAmountOut, slippageTolerance, false).toString() + ); + assert.equal(quote.estimatedAmountIn.toString(), tradeAmount); + assert.doesNotThrow(async () => await (await whirlpool.swap(quote)).buildAndExecute()); + }); + + /** + * |---c1--(5632)-c2---|-------(5632)------|-------(5632)------| + */ + it("3 identical arrays, 1st contains curr_tick_index, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 4 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [5632], + fundedPositions: [ + buildPosition( + { arrayIndex: 1, offsetIndex: 0 }, + { arrayIndex: 1, offsetIndex: TICK_ARRAY_SIZE - 1 }, + tickSpacing, + new BN(250_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const aToB = false; + const tickArrays = await getTickArrays( + [5632, 5632, 5632], + ctx, + AddressUtil.toPubKey(whirlpool.getAddress()), + fetcher + ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + + const tradeAmount = new BN("33588"); + const quote = swapQuoteWithParams( + { + aToB, + amountSpecifiedIsInput: true, + tokenAmount: tradeAmount, + whirlpoolData, + tickArrays, + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + otherAmountThreshold: ZERO, + tokenExtensionCtx, + }, + slippageTolerance + ); + + // Verify with an actual swap. + assert.equal(quote.aToB, aToB); + assert.equal(quote.amountSpecifiedIsInput, true); + assert.equal( + quote.sqrtPriceLimit.toString(), + SwapUtils.getDefaultSqrtPriceLimit(aToB).toString() + ); + assert.equal( + quote.otherAmountThreshold.toString(), + adjustForSlippage(quote.estimatedAmountOut, slippageTolerance, false).toString() + ); + assert.equal(quote.estimatedAmountIn.toString(), tradeAmount); + assert.doesNotThrow(async () => await (await whirlpool.swap(quote)).buildAndExecute()); + }); + + /** + * |xxxxxxxxxxxxxxxxxxxx|xxxxxxxxxxxxxxxxx|-c2---c1-----------| + */ + it("Whirlpool.swap with uninitialized TickArrays, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const tradeAmount = new BN(10000); + const aToB = true; + const tickArrays = SwapUtils.getTickArrayPublicKeys( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpool.getAddress() + ); + + await assert.rejects( + whirlpool.swap({ + amount: tradeAmount, + amountSpecifiedIsInput: true, + aToB, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + tickArray0: tickArrays[0], + tickArray1: tickArrays[1], + tickArray2: tickArrays[2], + }), + (err: Error) => { + const uninitializedArrays = [tickArrays[1].toBase58(), tickArrays[2].toBase58()].join(", "); + return err.message.indexOf(`TickArray addresses - [${uninitializedArrays}] need to be initialized.`) >= 0; + } + ); + }); + + /** + * |-------------c1--c2-|xxxxxxxxxxxxxxxxx|xxxxxxxxxxxxxxxxxxx| + */ + it("Whirlpool.swap with uninitialized TickArrays, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 44 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const tradeAmount = new BN(10000); + const aToB = false; + const tickArrays = SwapUtils.getTickArrayPublicKeys( + whirlpoolData.tickCurrentIndex, + whirlpoolData.tickSpacing, + aToB, + ctx.program.programId, + whirlpool.getAddress() + ); + + await assert.rejects( + whirlpool.swap({ + amount: tradeAmount, + amountSpecifiedIsInput: true, + aToB, + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + tickArray0: tickArrays[0], + tickArray1: tickArrays[1], + tickArray2: tickArrays[2], + }), + (err: Error) => { + const uninitializedArrays = [tickArrays[1].toBase58(), tickArrays[2].toBase58()].join(", "); + return err.message.indexOf(`TickArray addresses - [${uninitializedArrays}] need to be initialized.`) >= 0; + } + ); + }); + }); + }); +}); diff --git a/sdk/tests/sdk/whirlpools/swap/v2/swap-traverse.test.ts b/sdk/tests/sdk/whirlpools/swap/v2/swap-traverse.test.ts new file mode 100644 index 000000000..b1e86bc06 --- /dev/null +++ b/sdk/tests/sdk/whirlpools/swap/v2/swap-traverse.test.ts @@ -0,0 +1,1499 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Percentage } from "@orca-so/common-sdk"; +import * as assert from "assert"; +import { BN } from "bn.js"; +import { + buildWhirlpoolClient, MAX_SQRT_PRICE, MAX_TICK_INDEX, MIN_SQRT_PRICE, MIN_TICK_INDEX, PriceMath, + swapQuoteByInputToken, + swapQuoteByOutputToken, + swapQuoteWithParams, SwapUtils, TICK_ARRAY_SIZE, + WhirlpoolContext +} from "../../../../../src"; +import { SwapErrorCode, WhirlpoolsError } from "../../../../../src/errors/errors"; +import { IGNORE_CACHE } from "../../../../../src/network/public/fetcher"; +import { assertInputOutputQuoteEqual, assertQuoteAndResults, TickSpacing } from "../../../../utils"; +import { defaultConfirmOptions } from "../../../../utils/const"; +import { + arrayTickIndexToTickIndex, + buildPosition, +} from "../../../../utils/swap-test-utils"; +import { setupSwapTestV2 } from "../../../../utils/v2/swap-test-utils-v2"; +import { getVaultAmounts } from "../../../../utils/whirlpools-test-utils"; +import { TokenExtensionUtil } from "../../../../../src/utils/public/token-extension-util"; +import { TokenTrait } from "../../../../utils/v2/init-utils-v2"; + +describe("swap traversal tests", () => { + const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); + + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + const client = buildWhirlpoolClient(ctx); + const tickSpacing = TickSpacing.SixtyFour; + const slippageTolerance = Percentage.fromFraction(0, 100); + + const tokenTraitVariations: { + tokenTraitA: TokenTrait; + tokenTraitB: TokenTrait; + }[] = [ + { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + }, + { + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: false }, + }, + { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: true }, + }, + { + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: true }, + }, + ]; + tokenTraitVariations.forEach((tokenTraits) => { + describe(`tokenTraitA: ${ + tokenTraits.tokenTraitA.isToken2022 ? "Token2022" : "Token" + }, tokenTraitB: ${ + tokenTraits.tokenTraitB.isToken2022 ? "Token2022" : "Token" + }`, () => { + + /** + * |--------------------|b-----x2----a-------b-|x1-a------------------| + */ + it("curr_index on the last initializable tick, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 0, offsetIndex: 15 }, tickSpacing); + const aToB = true; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -1, offsetIndex: 44 }, + { arrayIndex: 0, offsetIndex: 30 }, + tickSpacing, + new BN(250_000) + ), + buildPosition( + //b + { arrayIndex: -1, offsetIndex: 0 }, + { arrayIndex: -1, offsetIndex: TICK_ARRAY_SIZE - 1 }, + tickSpacing, + new BN(350_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + new BN(150000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintB, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * |--------------------x1,a|-b--------a----x2---b-|-------------------| + */ + it("curr_index on the last initializable tick, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex( + { arrayIndex: 0, offsetIndex: TICK_ARRAY_SIZE - 1 }, + tickSpacing + ); + const aToB = false; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: 0, offsetIndex: TICK_ARRAY_SIZE - 1 }, + { arrayIndex: 1, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + buildPosition( + //b + { arrayIndex: 1, offsetIndex: 0 }, + { arrayIndex: 1, offsetIndex: TICK_ARRAY_SIZE - 1 }, + tickSpacing, + new BN(350_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + new BN(190000000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintA, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * b|-----x2---------|a---------------|a,x1-------------b| + */ + it("curr_index on the first initializable tick, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 0 }, tickSpacing); + const aToB = true; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: 0, offsetIndex: 0 }, + { arrayIndex: 1, offsetIndex: 0 }, + tickSpacing, + new BN(250_000_000) + ), + buildPosition( + //b + { arrayIndex: -2, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: TICK_ARRAY_SIZE - 1 }, + tickSpacing, + new BN(350_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + new BN(200000000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintB, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * b|a,x1--------------|a---------------|---------x2--------b| + */ + it("curr_index on the first initializable tick, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 0, offsetIndex: 0 }, tickSpacing); + const aToB = false; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: 0, offsetIndex: 0 }, + { arrayIndex: 1, offsetIndex: 0 }, + tickSpacing, + new BN(250_000_000) + ), + buildPosition( + //b + { arrayIndex: -1, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: TICK_ARRAY_SIZE - 1 }, + tickSpacing, + new BN(350_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + new BN(450000000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintA, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * |--------b-----x1-a|------a---x2---b--|-------------------| + */ + it("curr_index on the 2nd last initialized tick, with the next tick initialized, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex( + { arrayIndex: 0, offsetIndex: TICK_ARRAY_SIZE - 2 }, // 5504 + tickSpacing + ); + const aToB = false; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: 0, offsetIndex: TICK_ARRAY_SIZE - 1 }, + { arrayIndex: 1, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + buildPosition( + //b + { arrayIndex: 0, offsetIndex: 44 }, + { arrayIndex: 1, offsetIndex: TICK_ARRAY_SIZE - 4 }, + tickSpacing, + new BN(350_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + new BN(150000000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintA, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * |-----------b--x2--|-------a-----b-----|a-x1-------------| + */ + it("curr_index on the 2nd initialized tick, with the first tick initialized, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 2, offsetIndex: 1 }, tickSpacing); + const aToB = true; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: 1, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 0 }, + tickSpacing, + new BN(250_000_000) + ), + buildPosition( + //b + { arrayIndex: 0, offsetIndex: 44 }, + { arrayIndex: 1, offsetIndex: 64 }, + tickSpacing, + new BN(350_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + new BN(75000000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintB, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * |--------b-----a-x1|a---------x2---b--|-------------------| + */ + it("curr_index btw end of last offset and next array, with the next tick initialized, b->a", async () => { + const currIndex = 5629; + const aToB = false; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: 0, offsetIndex: TICK_ARRAY_SIZE - 1 }, + { arrayIndex: 1, offsetIndex: 1 }, + tickSpacing, + new BN(250_000_000) + ), + buildPosition( + //b + { arrayIndex: 0, offsetIndex: 44 }, + { arrayIndex: 1, offsetIndex: TICK_ARRAY_SIZE - 4 }, + tickSpacing, + new BN(350_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + new BN(15000000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintA, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * |-----------b--x2--|-------a-----b-----|x1,a-------------| + */ + it("curr_index btw end of last offset and next array, with the next tick initialized, a->b", async () => { + const currIndex = 11264 + 30; + const aToB = true; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: 1, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 0 }, + tickSpacing, + new BN(250_000_000) + ), + buildPosition( + //b + { arrayIndex: 0, offsetIndex: 44 }, + { arrayIndex: 1, offsetIndex: 64 }, + tickSpacing, + new BN(350_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + new BN(7500000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintB, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * |----------------|-----a----x2-----b|--------x1----a---b----| + */ + it("on some tick, traverse to the 1st initialized tick in the next tick-array, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 2, offsetIndex: 22 }, tickSpacing); + const aToB = true; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: 1, offsetIndex: 44 }, + { arrayIndex: 2, offsetIndex: 44 }, + tickSpacing, + new BN(250_000_000) + ), + buildPosition( + //b + { arrayIndex: 1, offsetIndex: TICK_ARRAY_SIZE - 1 }, + { arrayIndex: 2, offsetIndex: 64 }, + tickSpacing, + new BN(350_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + new BN(45000000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintB, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * |----a--b---x1------|a---x2-----b-------|------------------| + */ + it("on some tick, traverse to the 1st initialized tick in the next tick-array, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 64 }, tickSpacing); + const aToB = false; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-5632, 0, 5632], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -1, offsetIndex: 10 }, + { arrayIndex: 0, offsetIndex: 0 }, + tickSpacing, + new BN(250_000_000) + ), + buildPosition( + //b + { arrayIndex: -1, offsetIndex: 22 }, + { arrayIndex: 0, offsetIndex: 64 }, + tickSpacing, + new BN(350_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + new BN(49500000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintA, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * |-------a----x2------|-----------------|----x1-----a-------| + */ + it("on some tick, traverse to the next tick in the n+2 tick-array, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 22 }, tickSpacing); + const aToB = true; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-5632, 0, 5632], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -1, offsetIndex: 10 }, + { arrayIndex: 1, offsetIndex: 23 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + new BN(119500000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintB, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * |-------a------x1----|-----------------|-----x2--------a---| + */ + it("on some tick, traverse to the next tick in the n+2 tick-array, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 22 }, tickSpacing); + const aToB = false; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-5632, 0, 5632], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -1, offsetIndex: 10 }, + { arrayIndex: 1, offsetIndex: 23 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + new BN(119500000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintA, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * a|----------------|-----------------|-------x1--------|a + */ + it("3 arrays, on some initialized tick, no other initialized tick in the sequence, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 22 }, tickSpacing); + const aToB = true; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 10 }, + { arrayIndex: 2, offsetIndex: 23 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + new BN(119500000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintB, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * |-----x1-------------|-----------------|-------------------| + */ + it("3 arrays, on some initialized tick, no other initialized tick in the sequence, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 22 }, tickSpacing); + const aToB = false; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 10 }, + { arrayIndex: 2, offsetIndex: 23 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + new BN(159500000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintA, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * [1, 0, -1] + * e|---c--x2----a---d----b|f-----a--b----d----|f-----c---x1-------|e + */ + it("3 arrays, on some uninitialized tick, traverse lots of ticks, a->b", async () => { + const currIndex = + arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 25 }, tickSpacing) - 30; + const aToB = true; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // e + { arrayIndex: -2, offsetIndex: 10 }, + { arrayIndex: 2, offsetIndex: 23 }, + tickSpacing, + new BN(250_000) + ), + buildPosition( + // c + { arrayIndex: -1, offsetIndex: 10 }, + { arrayIndex: 1, offsetIndex: 15 }, + tickSpacing, + new BN(100_000_000) + ), + buildPosition( + // a + { arrayIndex: -1, offsetIndex: 30 }, + { arrayIndex: 0, offsetIndex: 20 }, + tickSpacing, + new BN(100_000_000) + ), + buildPosition( + // d + { arrayIndex: -1, offsetIndex: 60 }, + { arrayIndex: 0, offsetIndex: 60 }, + tickSpacing, + new BN(50_000_000) + ), + buildPosition( + // f + { arrayIndex: 0, offsetIndex: 0 }, + { arrayIndex: 1, offsetIndex: 0 }, + tickSpacing, + new BN(25_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + new BN(102195000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintB, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * e|---c--x1----a---d--b---|f-----a--b----d----|f------c---x2--------|e + */ + it("3 arrays, on some uninitialized tick, traverse lots of ticks, b->a", async () => { + const currIndex = + arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 15 }, tickSpacing) - 30; + const aToB = false; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // e + { arrayIndex: -2, offsetIndex: 10 }, + { arrayIndex: 2, offsetIndex: 23 }, + tickSpacing, + new BN(250_000) + ), + buildPosition( + // c + { arrayIndex: -1, offsetIndex: 10 }, + { arrayIndex: 1, offsetIndex: 15 }, + tickSpacing, + new BN(100_000_000) + ), + buildPosition( + // a + { arrayIndex: -1, offsetIndex: 30 }, + { arrayIndex: 0, offsetIndex: 20 }, + tickSpacing, + new BN(100_000_000) + ), + buildPosition( + // d + { arrayIndex: -1, offsetIndex: 60 }, + { arrayIndex: 0, offsetIndex: 60 }, + tickSpacing, + new BN(50_000_000) + ), + buildPosition( + // f + { arrayIndex: 0, offsetIndex: 0 }, + { arrayIndex: 1, offsetIndex: 0 }, + tickSpacing, + new BN(25_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + new BN(99900000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintA, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * trade amount > liquidity + * |----------x1----------|-----------------|-------------------| + */ + it("3 arrays, trade amount exceeds liquidity available in array sequence, b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 22 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 10 }, + { arrayIndex: 2, offsetIndex: 23 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + await assert.rejects( + async () => + await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + new BN(9159500000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ), + (err) => { + const whirlErr = err as WhirlpoolsError; + const errorMatch = whirlErr.errorCode === SwapErrorCode.TickArraySequenceInvalid; + // Message contains failure on finding beyond tickIndex + const messageMatch = whirlErr.message.indexOf("11264") >= 0; + assert.ok(messageMatch, "Error Message must match condition."); + assert.ok(errorMatch, "Error Code must match condition."); + return true; + } + ); + }); + + /** + * trade amount > liquidity + * |--------------------|-----------------|---------x1----------| + */ + it("3 arrays, trade amount exceeds liquidity available in array sequence, a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 22 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 10 }, + { arrayIndex: 2, offsetIndex: 23 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + await assert.rejects( + async () => + await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + new BN(9159500000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ), + (err) => { + const whirlErr = err as WhirlpoolsError; + const errorMatch = whirlErr.errorCode === SwapErrorCode.TickArraySequenceInvalid; + // Message contains failure on finding beyond tickIndex + const messageMatch = whirlErr.message.indexOf("-5696") >= 0; + assert.ok(messageMatch, "Error Message must match condition."); + assert.ok(errorMatch, "Error Code must match condition."); + return true; + } + ); + }); + + /** + * |a--------x1----------a| Max + */ + it("on the last tick-array, traverse to the MAX_TICK_INDEX tick", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 78, offsetIndex: 22 }, tickSpacing); + const aToB = false; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [MAX_TICK_INDEX], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: 78, offsetIndex: 0 }, // 439,296 + { arrayIndex: 78, offsetIndex: 67 }, // 443,584 + tickSpacing, + new BN(250) + ), + ], + tokenMintAmount: new BN("95000000000000000"), + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const quote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + new BN("12595000000000"), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + await (await whirlpool.swap(quote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, quote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * Min |a--------x2--------a----|-----------------|-------------------| + */ + it("on the first tick-array, traverse to the MIN_TICK_INDEX tick", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -79, offsetIndex: 22 }, tickSpacing); + const aToB = true; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [MIN_TICK_INDEX], + fundedPositions: [ + buildPosition( + // a -444,928 + { arrayIndex: -79, offsetIndex: 21 }, + { arrayIndex: -79, offsetIndex: TICK_ARRAY_SIZE - 1 }, + tickSpacing, + new BN(250) + ), + ], + tokenMintAmount: new BN("95000000000000000"), + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const quote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintA, + new BN("12595000000000"), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + await (await whirlpool.swap(quote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, quote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * -5632 0 5632 11264 + * |-a--------|-------x1-|----------|----------|-x2-----a-| + * ta0 ta1 ta2 + */ + it("b->a, tickCurrentIndex = -tickSpacing, shifted", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 87 }, tickSpacing); + const aToB = false; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 10 }, + { arrayIndex: 2, offsetIndex: 80 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + new BN(200000000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const ta2StartTickIndex = 11264 + assert.ok(inputTokenQuote.estimatedEndTickIndex > ta2StartTickIndex); // traverse ta0, ta1, and ta2 + + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintA, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * -5632 0 5632 11264 + * |-a--------|--------x1|----------|----------|-x2-----a-| + * ta0 ta1 ta2 + */ + it("b->a, tickCurrentIndex = -1, shifted", async () => { + const currIndex = -1; + const aToB = false; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 10 }, + { arrayIndex: 2, offsetIndex: 80 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + new BN(200000000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const ta2StartTickIndex = 11264 + assert.ok(inputTokenQuote.estimatedEndTickIndex > ta2StartTickIndex); // traverse ta0, ta1, and ta2 + + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintA, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * -5632 0 5632 11264 + * |-a--------|XXXXXXXXx1|----------|----------|-x2-----a-| + * ta0 ta1 ta2 + */ + it("b->a, tickCurrentIndex = -1, tickCurrentIndex on uninitialized TickArray, shifted", async () => { + const currIndex = -1; + const aToB = false; + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 10 }, + { arrayIndex: 2, offsetIndex: 80 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + new BN(200000000), + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + const ta2StartTickIndex = 11264 + assert.ok(inputTokenQuote.estimatedEndTickIndex > ta2StartTickIndex); // traverse ta0, ta1, and ta2 + + const outputTokenQuote = await swapQuoteByOutputToken( + whirlpool, + whirlpoolData.tokenMintA, + inputTokenQuote.estimatedAmountOut, + slippageTolerance, + ctx.program.programId, + fetcher, + IGNORE_CACHE + ); + assertInputOutputQuoteEqual(inputTokenQuote, outputTokenQuote); + await (await whirlpool.swap(inputTokenQuote)).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + /** + * sqrtPriceLimit < MIN_SQRT_PRICE + * |--------------------|-----------------|---------x1----------| + */ + it("3 arrays, sqrtPriceLimit is out of bounds (< MIN_SQRT_PRICE), a->b", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 22 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 10 }, + { arrayIndex: 2, offsetIndex: 23 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const aToB = true; + const tickArrays = await SwapUtils.getTickArrays( + currIndex, + tickSpacing, + aToB, + ctx.program.programId, + whirlpool.getAddress(), + fetcher, + IGNORE_CACHE + ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + + assert.throws( + () => + swapQuoteWithParams( + { + aToB, + amountSpecifiedIsInput: true, + tokenAmount: new BN("10000"), + whirlpoolData, + tickArrays, + sqrtPriceLimit: new BN(MIN_SQRT_PRICE).subn(1), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + tokenExtensionCtx, + }, + slippageTolerance + ), + (err) => (err as WhirlpoolsError).errorCode === SwapErrorCode.SqrtPriceOutOfBounds + ); + }); + + /** + * sqrtPriceLimit > MAX_SQRT_PRICE + * |-----x1-------------|-----------------|---------------------| + */ + it("3 arrays, sqrtPriceLimit is out of bounds (> MAX_SQRT_PRICE), b->a", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 22 }, tickSpacing); + const whirlpool = await setupSwapTestV2({ + ctx, + ...tokenTraits, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-11264, -5632, 0, 5632, 11264], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -2, offsetIndex: 10 }, + { arrayIndex: 2, offsetIndex: 23 }, + tickSpacing, + new BN(250_000_000) + ), + ], + }); + + const whirlpoolData = await whirlpool.refreshData(); + const aToB = false; + const tickArrays = await SwapUtils.getTickArrays( + currIndex, + tickSpacing, + aToB, + ctx.program.programId, + whirlpool.getAddress(), + fetcher, + IGNORE_CACHE + ); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE); + + assert.throws( + () => + swapQuoteWithParams( + { + aToB, + amountSpecifiedIsInput: true, + tokenAmount: new BN("10000"), + whirlpoolData, + tickArrays, + sqrtPriceLimit: new BN(MAX_SQRT_PRICE).addn(1), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(true), + tokenExtensionCtx, + }, + slippageTolerance + ), + (err) => (err as WhirlpoolsError).errorCode === SwapErrorCode.SqrtPriceOutOfBounds + ); + }); + }); + }); +}); diff --git a/sdk/tests/sdk/whirlpools/whirlpool-client-impl.test.ts b/sdk/tests/sdk/whirlpools/whirlpool-client-impl.test.ts index 7505ad6f1..306d11bf6 100644 --- a/sdk/tests/sdk/whirlpools/whirlpool-client-impl.test.ts +++ b/sdk/tests/sdk/whirlpools/whirlpool-client-impl.test.ts @@ -4,15 +4,19 @@ import Decimal from "decimal.js"; import { buildWhirlpoolClient, InitPoolParams, + InitPoolV2Params, PDAUtil, PriceMath, TickUtil, + toTx, WhirlpoolContext } from "../../../src"; import { IGNORE_CACHE } from "../../../src/network/public/fetcher"; -import { ONE_SOL, systemTransferTx, TickSpacing } from "../../utils"; +import { ONE_SOL, systemTransferTx, TEST_TOKEN_2022_PROGRAM_ID, TickSpacing } from "../../utils"; import { defaultConfirmOptions } from "../../utils/const"; import { buildTestPoolParams } from "../../utils/init-utils"; +import { buildTestPoolV2Params } from "../../utils/v2/init-utils-v2"; +import { getMint, getTransferFeeConfig } from "@solana/spl-token"; describe("whirlpool-client-impl", () => { const provider = anchor.AnchorProvider.local(undefined, defaultConfirmOptions); @@ -21,115 +25,328 @@ describe("whirlpool-client-impl", () => { const ctx = WhirlpoolContext.fromWorkspace(provider, program); const client = buildWhirlpoolClient(ctx); - let funderKeypair: anchor.web3.Keypair; - let poolInitInfo: InitPoolParams; - beforeEach(async () => { - funderKeypair = anchor.web3.Keypair.generate(); - await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute(); - poolInitInfo = ( - await buildTestPoolParams( - ctx, - TickSpacing.Standard, - 3000, - PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6), + describe("TokenProgram", () => { + let funderKeypair: anchor.web3.Keypair; + let poolInitInfo: InitPoolParams; + beforeEach(async () => { + funderKeypair = anchor.web3.Keypair.generate(); + await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute(); + poolInitInfo = ( + await buildTestPoolParams( + ctx, + TickSpacing.Standard, + 3000, + PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6), + funderKeypair.publicKey + ) + ).poolInitInfo; + }); + + it("successfully creates a new whirpool account and initial tick array account", async () => { + const initalTick = TickUtil.getInitializableTickIndex( + PriceMath.sqrtPriceX64ToTickIndex(poolInitInfo.initSqrtPrice), + poolInitInfo.tickSpacing + ); + + const { poolKey: actualPubkey, tx } = await client.createPool( + poolInitInfo.whirlpoolsConfig, + poolInitInfo.tokenMintA, + poolInitInfo.tokenMintB, + poolInitInfo.tickSpacing, + initalTick, funderKeypair.publicKey - ) - ).poolInitInfo; - }); + ); + + const expectedPda = PDAUtil.getWhirlpool( + ctx.program.programId, + poolInitInfo.whirlpoolsConfig, + poolInitInfo.tokenMintA, + poolInitInfo.tokenMintB, + poolInitInfo.tickSpacing + ); + + const startTickArrayPda = PDAUtil.getTickArrayFromTickIndex( + initalTick, + poolInitInfo.tickSpacing, + expectedPda.publicKey, + ctx.program.programId + ); + + assert.ok(expectedPda.publicKey.equals(actualPubkey)); + + const [whirlpoolAccountBefore, tickArrayAccountBefore] = await Promise.all([ + ctx.fetcher.getPool(expectedPda.publicKey, IGNORE_CACHE), + ctx.fetcher.getTickArray(startTickArrayPda.publicKey, IGNORE_CACHE), + ]); + + assert.ok(whirlpoolAccountBefore === null); + assert.ok(tickArrayAccountBefore === null); - it("successfully creates a new whirpool account and initial tick array account", async () => { - const initalTick = TickUtil.getInitializableTickIndex( - PriceMath.sqrtPriceX64ToTickIndex(poolInitInfo.initSqrtPrice), - poolInitInfo.tickSpacing - ); - - const { poolKey: actualPubkey, tx } = await client.createPool( - poolInitInfo.whirlpoolsConfig, - poolInitInfo.tokenMintA, - poolInitInfo.tokenMintB, - poolInitInfo.tickSpacing, - initalTick, - funderKeypair.publicKey - ); - - const expectedPda = PDAUtil.getWhirlpool( - ctx.program.programId, - poolInitInfo.whirlpoolsConfig, - poolInitInfo.tokenMintA, - poolInitInfo.tokenMintB, - poolInitInfo.tickSpacing - ); - - const startTickArrayPda = PDAUtil.getTickArrayFromTickIndex( - initalTick, - poolInitInfo.tickSpacing, - expectedPda.publicKey, - ctx.program.programId - ); - - assert.ok(expectedPda.publicKey.equals(actualPubkey)); - - const [whirlpoolAccountBefore, tickArrayAccountBefore] = await Promise.all([ - ctx.fetcher.getPool(expectedPda.publicKey, IGNORE_CACHE), - ctx.fetcher.getTickArray(startTickArrayPda.publicKey, IGNORE_CACHE), - ]); - - assert.ok(whirlpoolAccountBefore === null); - assert.ok(tickArrayAccountBefore === null); - - await tx.addSigner(funderKeypair).buildAndExecute(); - - const [whirlpoolAccountAfter, tickArrayAccountAfter] = await Promise.all([ - ctx.fetcher.getPool(expectedPda.publicKey, IGNORE_CACHE), - ctx.fetcher.getTickArray(startTickArrayPda.publicKey, IGNORE_CACHE), - ]); - - assert.ok(whirlpoolAccountAfter !== null); - assert.ok(tickArrayAccountAfter !== null); - - assert.ok(whirlpoolAccountAfter.feeGrowthGlobalA.eqn(0)); - assert.ok(whirlpoolAccountAfter.feeGrowthGlobalB.eqn(0)); - assert.ok(whirlpoolAccountAfter.feeRate === 3000); - assert.ok(whirlpoolAccountAfter.liquidity.eqn(0)); - assert.ok(whirlpoolAccountAfter.protocolFeeOwedA.eqn(0)); - assert.ok(whirlpoolAccountAfter.protocolFeeOwedB.eqn(0)); - assert.ok(whirlpoolAccountAfter.protocolFeeRate === 300); - assert.ok(whirlpoolAccountAfter.rewardInfos.length === 3); - assert.ok(whirlpoolAccountAfter.rewardLastUpdatedTimestamp.eqn(0)); - assert.ok(whirlpoolAccountAfter.sqrtPrice.eq(PriceMath.tickIndexToSqrtPriceX64(initalTick))); - assert.ok(whirlpoolAccountAfter.tickCurrentIndex === initalTick); - assert.ok(whirlpoolAccountAfter.tickSpacing === poolInitInfo.tickSpacing); - assert.ok(whirlpoolAccountAfter.tokenMintA.equals(poolInitInfo.tokenMintA)); - assert.ok(whirlpoolAccountAfter.tokenMintB.equals(poolInitInfo.tokenMintB)); - assert.ok(whirlpoolAccountAfter.whirlpoolBump[0] === expectedPda.bump); - assert.ok(whirlpoolAccountAfter.whirlpoolsConfig.equals(poolInitInfo.whirlpoolsConfig)); - - assert.ok( - tickArrayAccountAfter.startTickIndex === - TickUtil.getStartTickIndex(initalTick, poolInitInfo.tickSpacing) - ); - assert.ok(tickArrayAccountAfter.ticks.length > 0); - assert.ok(tickArrayAccountAfter.whirlpool.equals(expectedPda.publicKey)); + await tx.addSigner(funderKeypair).buildAndExecute(); + + const [whirlpoolAccountAfter, tickArrayAccountAfter] = await Promise.all([ + ctx.fetcher.getPool(expectedPda.publicKey, IGNORE_CACHE), + ctx.fetcher.getTickArray(startTickArrayPda.publicKey, IGNORE_CACHE), + ]); + + assert.ok(whirlpoolAccountAfter !== null); + assert.ok(tickArrayAccountAfter !== null); + + assert.ok(whirlpoolAccountAfter.feeGrowthGlobalA.eqn(0)); + assert.ok(whirlpoolAccountAfter.feeGrowthGlobalB.eqn(0)); + assert.ok(whirlpoolAccountAfter.feeRate === 3000); + assert.ok(whirlpoolAccountAfter.liquidity.eqn(0)); + assert.ok(whirlpoolAccountAfter.protocolFeeOwedA.eqn(0)); + assert.ok(whirlpoolAccountAfter.protocolFeeOwedB.eqn(0)); + assert.ok(whirlpoolAccountAfter.protocolFeeRate === 300); + assert.ok(whirlpoolAccountAfter.rewardInfos.length === 3); + assert.ok(whirlpoolAccountAfter.rewardLastUpdatedTimestamp.eqn(0)); + assert.ok(whirlpoolAccountAfter.sqrtPrice.eq(PriceMath.tickIndexToSqrtPriceX64(initalTick))); + assert.ok(whirlpoolAccountAfter.tickCurrentIndex === initalTick); + assert.ok(whirlpoolAccountAfter.tickSpacing === poolInitInfo.tickSpacing); + assert.ok(whirlpoolAccountAfter.tokenMintA.equals(poolInitInfo.tokenMintA)); + assert.ok(whirlpoolAccountAfter.tokenMintB.equals(poolInitInfo.tokenMintB)); + assert.ok(whirlpoolAccountAfter.whirlpoolBump[0] === expectedPda.bump); + assert.ok(whirlpoolAccountAfter.whirlpoolsConfig.equals(poolInitInfo.whirlpoolsConfig)); + + assert.ok( + tickArrayAccountAfter.startTickIndex === + TickUtil.getStartTickIndex(initalTick, poolInitInfo.tickSpacing) + ); + assert.ok(tickArrayAccountAfter.ticks.length > 0); + assert.ok(tickArrayAccountAfter.whirlpool.equals(expectedPda.publicKey)); + }); + + it("throws an error when token order is incorrect", async () => { + const initalTick = TickUtil.getInitializableTickIndex( + PriceMath.sqrtPriceX64ToTickIndex(poolInitInfo.initSqrtPrice), + poolInitInfo.tickSpacing + ); + + const invInitialTick = TickUtil.invertTick(initalTick); + + await assert.rejects( + client.createPool( + poolInitInfo.whirlpoolsConfig, + poolInitInfo.tokenMintB, + poolInitInfo.tokenMintA, + poolInitInfo.tickSpacing, + invInitialTick, + funderKeypair.publicKey + ), + /Token order needs to be flipped to match the canonical ordering \(i.e. sorted on the byte repr. of the mint pubkeys\)/ + ); + }); }); - it("throws an error when token order is incorrect", async () => { - const initalTick = TickUtil.getInitializableTickIndex( - PriceMath.sqrtPriceX64ToTickIndex(poolInitInfo.initSqrtPrice), - poolInitInfo.tickSpacing - ); + describe("TokenExtension", () => { + let funderKeypair: anchor.web3.Keypair; + beforeEach(async () => { + funderKeypair = anchor.web3.Keypair.generate(); + await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute(); + }); + + it("successfully creates a new whirpool account and initial tick array account (without TokenBadge)", async () => { + const poolInitInfo = ( + await buildTestPoolV2Params( + ctx, + {isToken2022: true, hasTransferFeeExtension: true}, + {isToken2022: true, hasTransferFeeExtension: true}, + TickSpacing.Standard, + 3000, + PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6), + funderKeypair.publicKey + ) + ).poolInitInfo; - const invInitialTick = TickUtil.invertTick(initalTick); + // initialized with TransferFee extension + const mintDataA = await getMint(provider.connection, poolInitInfo.tokenMintA, "confirmed", TEST_TOKEN_2022_PROGRAM_ID); + const mintDataB = await getMint(provider.connection, poolInitInfo.tokenMintB, "confirmed", TEST_TOKEN_2022_PROGRAM_ID); + const transferFeeConfigA = getTransferFeeConfig(mintDataA); + const transferFeeConfigB = getTransferFeeConfig(mintDataB); + assert.ok(transferFeeConfigA !== null); + assert.ok(transferFeeConfigB !== null); + + const initalTick = TickUtil.getInitializableTickIndex( + PriceMath.sqrtPriceX64ToTickIndex(poolInitInfo.initSqrtPrice), + poolInitInfo.tickSpacing + ); - await assert.rejects( - client.createPool( + const { poolKey: actualPubkey, tx } = await client.createPool( poolInitInfo.whirlpoolsConfig, - poolInitInfo.tokenMintB, poolInitInfo.tokenMintA, + poolInitInfo.tokenMintB, poolInitInfo.tickSpacing, - invInitialTick, + initalTick, funderKeypair.publicKey - ), - /Token order needs to be flipped to match the canonical ordering \(i.e. sorted on the byte repr. of the mint pubkeys\)/ - ); + ); + + const expectedPda = PDAUtil.getWhirlpool( + ctx.program.programId, + poolInitInfo.whirlpoolsConfig, + poolInitInfo.tokenMintA, + poolInitInfo.tokenMintB, + poolInitInfo.tickSpacing + ); + + const startTickArrayPda = PDAUtil.getTickArrayFromTickIndex( + initalTick, + poolInitInfo.tickSpacing, + expectedPda.publicKey, + ctx.program.programId + ); + + assert.ok(expectedPda.publicKey.equals(actualPubkey)); + + const [whirlpoolAccountBefore, tickArrayAccountBefore] = await Promise.all([ + ctx.fetcher.getPool(expectedPda.publicKey, IGNORE_CACHE), + ctx.fetcher.getTickArray(startTickArrayPda.publicKey, IGNORE_CACHE), + ]); + + assert.ok(whirlpoolAccountBefore === null); + assert.ok(tickArrayAccountBefore === null); + + await tx.addSigner(funderKeypair).buildAndExecute(); + + const [whirlpoolAccountAfter, tickArrayAccountAfter] = await Promise.all([ + ctx.fetcher.getPool(expectedPda.publicKey, IGNORE_CACHE), + ctx.fetcher.getTickArray(startTickArrayPda.publicKey, IGNORE_CACHE), + ]); + + assert.ok(whirlpoolAccountAfter !== null); + assert.ok(tickArrayAccountAfter !== null); + + assert.ok(whirlpoolAccountAfter.feeGrowthGlobalA.eqn(0)); + assert.ok(whirlpoolAccountAfter.feeGrowthGlobalB.eqn(0)); + assert.ok(whirlpoolAccountAfter.feeRate === 3000); + assert.ok(whirlpoolAccountAfter.liquidity.eqn(0)); + assert.ok(whirlpoolAccountAfter.protocolFeeOwedA.eqn(0)); + assert.ok(whirlpoolAccountAfter.protocolFeeOwedB.eqn(0)); + assert.ok(whirlpoolAccountAfter.protocolFeeRate === 300); + assert.ok(whirlpoolAccountAfter.rewardInfos.length === 3); + assert.ok(whirlpoolAccountAfter.rewardLastUpdatedTimestamp.eqn(0)); + assert.ok(whirlpoolAccountAfter.sqrtPrice.eq(PriceMath.tickIndexToSqrtPriceX64(initalTick))); + assert.ok(whirlpoolAccountAfter.tickCurrentIndex === initalTick); + assert.ok(whirlpoolAccountAfter.tickSpacing === poolInitInfo.tickSpacing); + assert.ok(whirlpoolAccountAfter.tokenMintA.equals(poolInitInfo.tokenMintA)); + assert.ok(whirlpoolAccountAfter.tokenMintB.equals(poolInitInfo.tokenMintB)); + assert.ok(whirlpoolAccountAfter.whirlpoolBump[0] === expectedPda.bump); + assert.ok(whirlpoolAccountAfter.whirlpoolsConfig.equals(poolInitInfo.whirlpoolsConfig)); + + assert.ok( + tickArrayAccountAfter.startTickIndex === + TickUtil.getStartTickIndex(initalTick, poolInitInfo.tickSpacing) + ); + assert.ok(tickArrayAccountAfter.ticks.length > 0); + assert.ok(tickArrayAccountAfter.whirlpool.equals(expectedPda.publicKey)); + }); + + it("successfully creates a new whirpool account (with TokenBadge)", async () => { + const poolInitInfo = ( + await buildTestPoolV2Params( + ctx, + {isToken2022: true, hasTransferHookExtension: true, hasPermanentDelegate: true}, // TokenBadge required + {isToken2022: true, hasTransferHookExtension: true, hasPermanentDelegate: true}, // TokenBadge required + TickSpacing.Standard, + 3000, + PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6), + ctx.wallet.publicKey, + true, // initialize TokenBadge + true, // initialize TokenBadge + ) + ).poolInitInfo; + + const initialTick = TickUtil.getInitializableTickIndex( + PriceMath.sqrtPriceX64ToTickIndex(poolInitInfo.initSqrtPrice), + poolInitInfo.tickSpacing + ); + + const tx = (await client.createPool( + poolInitInfo.whirlpoolsConfig, + poolInitInfo.tokenMintA, + poolInitInfo.tokenMintB, + poolInitInfo.tickSpacing, + initialTick, + ctx.wallet.publicKey, + )).tx; + + await tx.buildAndExecute(); + + const whirlpool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey, IGNORE_CACHE); + + assert.ok(whirlpool !== null); + assert.ok(whirlpool.getData().tokenMintA.equals(poolInitInfo.tokenMintA)); + assert.ok(whirlpool.getData().tokenMintB.equals(poolInitInfo.tokenMintB)); + }); + + it("throws an error when token order is incorrect", async () => { + const poolInitInfo = ( + await buildTestPoolV2Params( + ctx, + {isToken2022: true, hasTransferFeeExtension: true}, + {isToken2022: true, hasTransferFeeExtension: true}, + TickSpacing.Standard, + 3000, + PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6), + funderKeypair.publicKey + ) + ).poolInitInfo; + + const initialTick = TickUtil.getInitializableTickIndex( + PriceMath.sqrtPriceX64ToTickIndex(poolInitInfo.initSqrtPrice), + poolInitInfo.tickSpacing + ); + + const invInitialTick = TickUtil.invertTick(initialTick); + + await assert.rejects( + client.createPool( + poolInitInfo.whirlpoolsConfig, + poolInitInfo.tokenMintB, + poolInitInfo.tokenMintA, + poolInitInfo.tickSpacing, + invInitialTick, + funderKeypair.publicKey + ), + /Token order needs to be flipped to match the canonical ordering \(i.e. sorted on the byte repr. of the mint pubkeys\)/ + ); + }); + + it("throws an error when TokenBadge is not initialized", async () => { + const poolInitInfo = ( + await buildTestPoolV2Params( + ctx, + {isToken2022: true, hasTransferHookExtension: true, hasPermanentDelegate: true}, + {isToken2022: true, hasTransferHookExtension: true, hasPermanentDelegate: true}, + TickSpacing.Standard, + 3000, + PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6), + ctx.wallet.publicKey, + false, // not initialize TokenBadge + false, // not initialize TokenBadge + ) + ).poolInitInfo; + + const initialTick = TickUtil.getInitializableTickIndex( + PriceMath.sqrtPriceX64ToTickIndex(poolInitInfo.initSqrtPrice), + poolInitInfo.tickSpacing + ); + + const tx = (await client.createPool( + poolInitInfo.whirlpoolsConfig, + poolInitInfo.tokenMintA, + poolInitInfo.tokenMintB, + poolInitInfo.tickSpacing, + initialTick, + ctx.wallet.publicKey, + )).tx; + + await assert.rejects( + tx.buildAndExecute(), + /0x179f/ // UnsupportedTokenMint + ); + }); + }); + }); diff --git a/sdk/tests/sdk/whirlpools/whirlpool-impl#closePosition.test.ts b/sdk/tests/sdk/whirlpools/whirlpool-impl#closePosition.test.ts index 3493c0865..4771051ce 100644 --- a/sdk/tests/sdk/whirlpools/whirlpool-impl#closePosition.test.ts +++ b/sdk/tests/sdk/whirlpools/whirlpool-impl#closePosition.test.ts @@ -18,6 +18,9 @@ import { IGNORE_CACHE } from "../../../src/network/public/fetcher"; import { TickSpacing, ZERO_BN, createAssociatedTokenAccount, sleep, transferToken } from "../../utils"; import { defaultConfirmOptions } from "../../utils/const"; import { WhirlpoolTestFixture } from "../../utils/fixture"; +import { TokenExtensionUtil } from "../../../src/utils/public/token-extension-util"; +import { TokenTrait } from "../../utils/v2/init-utils-v2"; +import { WhirlpoolTestFixtureV2 } from "../../utils/v2/fixture-v2"; interface SharedTestContext { provider: anchor.AnchorProvider; @@ -50,7 +53,7 @@ describe("WhirlpoolImpl#closePosition()", () => { }; }); - async function accrueFeesAndRewards(fixture: WhirlpoolTestFixture) { + async function accrueFeesAndRewards(fixture: WhirlpoolTestFixture | WhirlpoolTestFixtureV2) { const ctx = testCtx.whirlpoolCtx; const { poolInitInfo } = fixture.getInfos(); const { whirlpoolClient } = testCtx; @@ -67,7 +70,7 @@ describe("WhirlpoolImpl#closePosition()", () => { aToB: true, tickArray0: tickArrayPda.publicKey, tickArray1: tickArrayPda.publicKey, - tickArray2: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, })).buildAndExecute() // Accrue fees in token B @@ -86,7 +89,7 @@ describe("WhirlpoolImpl#closePosition()", () => { await sleep(1200); } - async function removeLiquidity(fixture: WhirlpoolTestFixture) { + async function removeLiquidity(fixture: WhirlpoolTestFixture | WhirlpoolTestFixtureV2) { const { poolInitInfo, positions: [positionInfo], @@ -99,7 +102,8 @@ describe("WhirlpoolImpl#closePosition()", () => { position.getData().liquidity, Percentage.fromDecimal(new Decimal(0)), position, - pool + pool, + await TokenExtensionUtil.buildTokenExtensionContext(testCtx.whirlpoolCtx.fetcher, pool.getData(), IGNORE_CACHE), ); const tx = await position.decreaseLiquidity(liquidityCollectedQuote); @@ -107,7 +111,7 @@ describe("WhirlpoolImpl#closePosition()", () => { await tx.buildAndExecute(); } - async function collectFees(fixture: WhirlpoolTestFixture) { + async function collectFees(fixture: WhirlpoolTestFixture | WhirlpoolTestFixtureV2) { const { positions } = fixture.getInfos(); const { whirlpoolClient } = testCtx; const position = await whirlpoolClient.getPosition(positions[0].publicKey, IGNORE_CACHE); @@ -115,14 +119,17 @@ describe("WhirlpoolImpl#closePosition()", () => { await (await position.collectFees(hasL)).buildAndExecute(); } - async function collectRewards(fixture: WhirlpoolTestFixture) { + async function collectRewards(fixture: WhirlpoolTestFixture | WhirlpoolTestFixtureV2) { const { positions } = fixture.getInfos(); const { whirlpoolClient } = testCtx; const position = await whirlpoolClient.getPosition(positions[0].publicKey, IGNORE_CACHE); - await (await position.collectRewards(undefined, true)).buildAndExecute(); + const txs = await position.collectRewards(undefined, true); + for (const tx of txs) { + await tx.buildAndExecute(); + } } - async function testClosePosition(fixture: WhirlpoolTestFixture, isWSOLTest = false) { + async function testClosePosition(fixture: WhirlpoolTestFixture | WhirlpoolTestFixtureV2, isWSOLTest = false) { const { positions, poolInitInfo, rewards } = fixture.getInfos(); const { whirlpoolClient } = testCtx; const ctx = whirlpoolClient.getContext(); @@ -144,19 +151,22 @@ describe("WhirlpoolImpl#closePosition()", () => { // TODO: Our createWSOLAccountInstructions ignores payer and requires destinationWallet to sign // We can remove this once we move to syncNative and wSOL becomes another ATA to handle. if (isWSOLTest) { - txs[txs.length - 1].addSigner(otherWallet) + txs[0].addSigner(otherWallet) } for (const tx of txs) { await tx.buildAndExecute(); } + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(ctx.fetcher, pool.getData(), IGNORE_CACHE); + // Verify liquidity and fees collected const liquidityCollectedQuote = decreaseLiquidityQuoteByLiquidity( position.getData().liquidity, Percentage.fromDecimal(new Decimal(0)), position, - pool + pool, + tokenExtensionCtx, ); const feeQuote = collectFeesQuote({ @@ -164,8 +174,9 @@ describe("WhirlpoolImpl#closePosition()", () => { whirlpool: pool.getData(), tickLower: position.getLowerTickData(), tickUpper: position.getUpperTickData(), + tokenExtensionCtx, }); - const accountAPubkey = getAssociatedTokenAddressSync(poolInitInfo.tokenMintA, otherWallet.publicKey); + const accountAPubkey = getAssociatedTokenAddressSync(poolInitInfo.tokenMintA, otherWallet.publicKey, undefined, tokenExtensionCtx.tokenMintWithProgramA.tokenProgram); const accountA = (await ctx.fetcher.getTokenInfo(accountAPubkey, IGNORE_CACHE)) as Account; const expectAmountA = liquidityCollectedQuote.tokenMinA.add(feeQuote.feeOwedA); if (isWSOLTest) { @@ -188,7 +199,7 @@ describe("WhirlpoolImpl#closePosition()", () => { ); } - const accountBPubkey = getAssociatedTokenAddressSync(poolInitInfo.tokenMintB, otherWallet.publicKey); + const accountBPubkey = getAssociatedTokenAddressSync(poolInitInfo.tokenMintB, otherWallet.publicKey, undefined, tokenExtensionCtx.tokenMintWithProgramB.tokenProgram); const accountB = await ctx.fetcher.getTokenInfo(accountBPubkey, IGNORE_CACHE); const expectAmountB = liquidityCollectedQuote.tokenMinB.add(feeQuote.feeOwedB); if (expectAmountB.isZero()) { @@ -207,253 +218,313 @@ describe("WhirlpoolImpl#closePosition()", () => { whirlpool: preClosePoolData, tickLower: position.getLowerTickData(), tickUpper: position.getUpperTickData(), + tokenExtensionCtx, timeStampInSeconds: postClosePoolData.rewardLastUpdatedTimestamp, }); for (let i = 0; i < NUM_REWARDS; i++) { if (!!rewards[i]) { - const rewardATA = getAssociatedTokenAddressSync(rewards[i].rewardMint, otherWallet.publicKey); + const rewardATA = getAssociatedTokenAddressSync(rewards[i].rewardMint, otherWallet.publicKey, undefined, tokenExtensionCtx.rewardTokenMintsWithProgram[i]!.tokenProgram); const rewardTokenAccount = await ctx.fetcher.getTokenInfo(rewardATA, IGNORE_CACHE); - assert.equal(rewardTokenAccount?.amount.toString(), rewardQuote[i]?.toString()); + assert.equal(rewardTokenAccount?.amount.toString(), rewardQuote.rewardOwed[i]?.toString()); } } } context("when the whirlpool is SPL-only", () => { - it("should close a position with no liquidity, fees, or rewards", async () => { - const fixture = await new WhirlpoolTestFixture(testCtx.whirlpoolCtx).init({ - tickSpacing, - positions: [ - { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position - ], - }); - - await removeLiquidity(fixture); - await testClosePosition(fixture); - }); - - it("should close a position with only liquidity", async () => { - const fixture = await new WhirlpoolTestFixture(testCtx.whirlpoolCtx).init({ - tickSpacing, - positions: [ - { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position - ], - }); - - await testClosePosition(fixture); - }); - - it("should close a position with only fees", async () => { - const fixture = await new WhirlpoolTestFixture(testCtx.whirlpoolCtx).init({ - tickSpacing, - positions: [ - { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position - ], - }); - - await accrueFeesAndRewards(fixture); - await removeLiquidity(fixture); - await testClosePosition(fixture); - }); - - it("should close a position with only rewards", async () => { - const fixture = await new WhirlpoolTestFixture(testCtx.whirlpoolCtx).init({ - tickSpacing, - positions: [ - { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position - ], - rewards: [ - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - ], - }); - - // accrue rewards - // closePosition does not attempt to create an ATA unless reward has accumulated. - await sleep(1200); - - await removeLiquidity(fixture); - await collectFees(fixture); - await testClosePosition(fixture); - }); - - it("should close a position with only liquidity and fees", async () => { - const fixture = await new WhirlpoolTestFixture(testCtx.whirlpoolCtx).init({ - tickSpacing, - positions: [ - { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position - ], - }); - - await accrueFeesAndRewards(fixture); - await testClosePosition(fixture); - }); - - it("should close a position with only liquidity and rewards", async () => { - const fixture = await new WhirlpoolTestFixture(testCtx.whirlpoolCtx).init({ - tickSpacing, - positions: [ - { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position - ], - rewards: [ - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - ], - }); - - // accrue rewards - // closePosition does not attempt to create an ATA unless reward has accumulated. - await sleep(1200); - - await testClosePosition(fixture); - }); - - it("should close a position with only fees and rewards", async () => { - const fixture = await new WhirlpoolTestFixture(testCtx.whirlpoolCtx).init({ - tickSpacing, - positions: [ - { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position - ], - rewards: [ - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - ], - }); - - await accrueFeesAndRewards(fixture); - await removeLiquidity(fixture); - await testClosePosition(fixture); - }); + const tokenTraitVariations: { + tokenTraitA: TokenTrait; + tokenTraitB: TokenTrait; + tokenTraitR: TokenTrait; + }[] = [ + { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + tokenTraitR: { isToken2022: false }, + }, + { + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: false }, + tokenTraitR: { isToken2022: false }, + }, + { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: true }, + tokenTraitR: { isToken2022: false }, + }, + { + // TransferHook is most difficult extension in transaction size + tokenTraitA: { isToken2022: true, hasTransferHookExtension: true }, + tokenTraitB: { isToken2022: true, hasTransferHookExtension: true }, + tokenTraitR: { isToken2022: true, hasTransferHookExtension: true }, + }, + ]; + tokenTraitVariations.forEach((tokenTraits) => { + describe(`tokenTraitA: ${ + tokenTraits.tokenTraitA.isToken2022 ? "Token2022" : "Token" + }, tokenTraitB: ${ + tokenTraits.tokenTraitB.isToken2022 ? "Token2022" : "Token" + }`, () => { + + it("should close a position with no liquidity, fees, or rewards", async () => { + const fixture = await new WhirlpoolTestFixtureV2(testCtx.whirlpoolCtx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position + ], + }); + + await removeLiquidity(fixture); + await testClosePosition(fixture); + }); + + it("should close a position with only liquidity", async () => { + const fixture = await new WhirlpoolTestFixtureV2(testCtx.whirlpoolCtx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position + ], + }); + + await testClosePosition(fixture); + }); + + it("should close a position with only fees", async () => { + const fixture = await new WhirlpoolTestFixtureV2(testCtx.whirlpoolCtx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position + ], + }); + + await accrueFeesAndRewards(fixture); + await removeLiquidity(fixture); + await testClosePosition(fixture); + }); + + it("should close a position with only rewards", async () => { + const fixture = await new WhirlpoolTestFixtureV2(testCtx.whirlpoolCtx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + + // accrue rewards + // closePosition does not attempt to create an ATA unless reward has accumulated. + await sleep(1200); + + await removeLiquidity(fixture); + await collectFees(fixture); + await testClosePosition(fixture); + }); + + it("should close a position with only liquidity and fees", async () => { + const fixture = await new WhirlpoolTestFixtureV2(testCtx.whirlpoolCtx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position + ], + }); + + await accrueFeesAndRewards(fixture); + await testClosePosition(fixture); + }); + + it("should close a position with only liquidity and rewards", async () => { + const fixture = await new WhirlpoolTestFixtureV2(testCtx.whirlpoolCtx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + + // accrue rewards + // closePosition does not attempt to create an ATA unless reward has accumulated. + await sleep(1200); + + await testClosePosition(fixture); + }); + + it("should close a position with only fees and rewards", async () => { + const fixture = await new WhirlpoolTestFixtureV2(testCtx.whirlpoolCtx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + + await accrueFeesAndRewards(fixture); + await removeLiquidity(fixture); + await testClosePosition(fixture); + }); + + it("should close a position with liquidity, fees, and rewards", async () => { + const fixture = await new WhirlpoolTestFixtureV2(testCtx.whirlpoolCtx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + + await accrueFeesAndRewards(fixture); + await testClosePosition(fixture); + }); + + it("should close a position with liquidity, fees, and rewards (no ATAs)", async () => { + const ctx = testCtx.whirlpoolCtx; + const fixture = await new WhirlpoolTestFixtureV2(testCtx.whirlpoolCtx).init({ + ...tokenTraits, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position + ], + rewards: [ + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: tokenTraits.tokenTraitR, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + + const otherWallet = anchor.web3.Keypair.generate(); + const positionData = fixture.getInfos().positions[0]; + + const position = await testCtx.whirlpoolClient.getPosition(positionData.publicKey, IGNORE_CACHE); + + const walletPositionTokenAccount = getAssociatedTokenAddressSync( + positionData.mintKeypair.publicKey, + testCtx.whirlpoolCtx.wallet.publicKey, + ); + + const newOwnerPositionTokenAccount = await createAssociatedTokenAccount( + ctx.provider, + positionData.mintKeypair.publicKey, + otherWallet.publicKey, + ctx.wallet.publicKey + ); + + await accrueFeesAndRewards(fixture); + await transferToken(testCtx.provider, walletPositionTokenAccount, newOwnerPositionTokenAccount, 1); + + const { poolInitInfo } = fixture.getInfos(); + + const pool = await testCtx.whirlpoolClient.getPool(poolInitInfo.whirlpoolPda.publicKey); + + const positionDataBefore = await testCtx.whirlpoolCtx.fetcher.getPosition( + position.getAddress(), + IGNORE_CACHE + ); + + assert.notEqual(positionDataBefore, null); + + const txs = await pool.closePosition( + position.getAddress(), + Percentage.fromFraction(10, 100), + otherWallet.publicKey, + otherWallet.publicKey, + ctx.wallet.publicKey + ); + + for (const tx of txs) { + await tx.addSigner(otherWallet).buildAndExecute(); + } + + const positionDataAfter = await testCtx.whirlpoolCtx.fetcher.getPosition( + position.getAddress(), + IGNORE_CACHE + ); + + assert.equal(positionDataAfter, null); + }); - it("should close a position with liquidity, fees, and rewards", async () => { - const fixture = await new WhirlpoolTestFixture(testCtx.whirlpoolCtx).init({ - tickSpacing, - positions: [ - { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position - ], - rewards: [ - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - ], }); - - await accrueFeesAndRewards(fixture); - await testClosePosition(fixture); - }); - - it("should close a position with liquidity, fees, and rewards (no ATAs)", async () => { - const ctx = testCtx.whirlpoolCtx; - const fixture = await new WhirlpoolTestFixture(testCtx.whirlpoolCtx).init({ - tickSpacing, - positions: [ - { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position - ], - rewards: [ - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - ], - }); - - const otherWallet = anchor.web3.Keypair.generate(); - const positionData = fixture.getInfos().positions[0]; - - const position = await testCtx.whirlpoolClient.getPosition(positionData.publicKey, IGNORE_CACHE); - - const walletPositionTokenAccount = getAssociatedTokenAddressSync( - positionData.mintKeypair.publicKey, - testCtx.whirlpoolCtx.wallet.publicKey, - ); - - const newOwnerPositionTokenAccount = await createAssociatedTokenAccount( - ctx.provider, - positionData.mintKeypair.publicKey, - otherWallet.publicKey, - ctx.wallet.publicKey - ); - - await accrueFeesAndRewards(fixture); - await transferToken(testCtx.provider, walletPositionTokenAccount, newOwnerPositionTokenAccount, 1); - - const { poolInitInfo } = fixture.getInfos(); - - const pool = await testCtx.whirlpoolClient.getPool(poolInitInfo.whirlpoolPda.publicKey); - - const positionDataBefore = await testCtx.whirlpoolCtx.fetcher.getPosition( - position.getAddress(), - IGNORE_CACHE - ); - - assert.notEqual(positionDataBefore, null); - - const txs = await pool.closePosition( - position.getAddress(), - Percentage.fromFraction(10, 100), - otherWallet.publicKey, - otherWallet.publicKey, - ctx.wallet.publicKey - ); - - txs[txs.length - 1].addSigner(otherWallet) - - for (const tx of txs) { - await tx.buildAndExecute(); - } - - const positionDataAfter = await testCtx.whirlpoolCtx.fetcher.getPosition( - position.getAddress(), - IGNORE_CACHE - ); - - assert.equal(positionDataAfter, null); }); }); @@ -548,10 +619,8 @@ describe("WhirlpoolImpl#closePosition()", () => { ctx.wallet.publicKey ); - txs[txs.length - 1].addSigner(otherWallet) - for (const tx of txs) { - await tx.buildAndExecute(); + await tx.addSigner(otherWallet).buildAndExecute(); } const positionDataAfter = await testCtx.whirlpoolCtx.fetcher.getPosition( diff --git a/sdk/tests/sdk/whirlpools/whirlpool-impl#collectFeesAndRewardsForPositions.test.ts b/sdk/tests/sdk/whirlpools/whirlpool-impl#collectFeesAndRewardsForPositions.test.ts index 9b36b1bef..7b5c75e8a 100644 --- a/sdk/tests/sdk/whirlpools/whirlpool-impl#collectFeesAndRewardsForPositions.test.ts +++ b/sdk/tests/sdk/whirlpools/whirlpool-impl#collectFeesAndRewardsForPositions.test.ts @@ -19,10 +19,12 @@ import { toTx } from "../../../src"; import { IGNORE_CACHE } from "../../../src/network/public/fetcher"; -import { TickSpacing, ZERO_BN } from "../../utils"; +import { TEST_TOKEN_2022_PROGRAM_ID, TEST_TOKEN_PROGRAM_ID, TickSpacing, ZERO_BN } from "../../utils"; import { defaultConfirmOptions } from "../../utils/const"; import { WhirlpoolTestFixture } from "../../utils/fixture"; import { FundedPositionInfo } from "../../utils/init-utils"; +import { TokenExtensionUtil } from "../../../src/utils/public/token-extension-util"; +import { WhirlpoolTestFixtureV2 } from "../../utils/v2/fixture-v2"; interface SharedTestContext { provider: anchor.AnchorProvider; @@ -134,6 +136,7 @@ describe("WhirlpoolImpl#collectFeesAndRewardsForPositions()", () => { position: positionData, tickLower: tickLowerData, tickUpper: tickUpperData, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(testCtx.whirlpoolCtx.fetcher, poolData, IGNORE_CACHE), }); assert.ok(quote.feeOwedA.gtn(0) || quote.feeOwedB.gtn(0)); @@ -200,30 +203,31 @@ describe("WhirlpoolImpl#collectFeesAndRewardsForPositions()", () => { await tx.buildAndExecute(); } - async function createATAs(fixture: WhirlpoolTestFixture) { + async function createATAs(fixture: WhirlpoolTestFixture | WhirlpoolTestFixtureV2) { const ctx = testCtx.whirlpoolCtx; const { poolInitInfo, configKeypairs } = fixture.getInfos(); const { whirlpoolPda } = poolInitInfo; const pool = await testCtx.whirlpoolClient.getPool(whirlpoolPda.publicKey); - const mintA = pool.getTokenAInfo().mint; - const mintB = pool.getTokenBInfo().mint; - const ataA = getAssociatedTokenAddressSync(mintA, ctx.wallet.publicKey); - const ataB = getAssociatedTokenAddressSync(mintB, ctx.wallet.publicKey); - await createATA(ctx, ataA, mintA); - await createATA(ctx, ataB, mintB); + const mintA = await testCtx.whirlpoolCtx.fetcher.getMintInfo(pool.getTokenAInfo().mint); + const mintB = await testCtx.whirlpoolCtx.fetcher.getMintInfo(pool.getTokenBInfo().mint); + + const ataA = getAssociatedTokenAddressSync(mintA!.address, ctx.wallet.publicKey, undefined, mintA!.tokenProgram); + const ataB = getAssociatedTokenAddressSync(mintB!.address, ctx.wallet.publicKey, undefined, mintB!.tokenProgram); + await createATA(ctx, ataA, mintA!.address, mintA!.tokenProgram); + await createATA(ctx, ataB, mintB!.address, mintB!.tokenProgram); for (let i = 0; i < NUM_REWARDS; i++) { if (PoolUtil.isRewardInitialized(pool.getRewardInfos()[i])) { - const mintReward = pool.getRewardInfos()[i].mint; - const ataReward = getAssociatedTokenAddressSync(mintReward, ctx.wallet.publicKey); - await createATA(ctx, ataReward, mintReward); + const mintReward = await testCtx.whirlpoolCtx.fetcher.getMintInfo(pool.getRewardInfos()[i].mint); + const ataReward = getAssociatedTokenAddressSync(mintReward!.address, ctx.wallet.publicKey, undefined, mintReward!.tokenProgram); + await createATA(ctx, ataReward, mintReward!.address, mintReward!.tokenProgram); } } } - async function createATA(ctx: WhirlpoolContext, ata: PublicKey, mint: PublicKey) { + async function createATA(ctx: WhirlpoolContext, ata: PublicKey, mint: PublicKey, tokenProgram: PublicKey) { if (mint.equals(NATIVE_MINT)) return; const account = await ctx.fetcher.getTokenInfo(ata, IGNORE_CACHE); @@ -233,6 +237,7 @@ describe("WhirlpoolImpl#collectFeesAndRewardsForPositions()", () => { ata, ctx.wallet.publicKey, mint, + tokenProgram, ); const tx = new TransactionBuilder(ctx.connection, ctx.wallet, ctx.txBuilderOpts); @@ -300,6 +305,7 @@ describe("WhirlpoolImpl#collectFeesAndRewardsForPositions()", () => { position: positionData, tickLower: tickLowerData, tickUpper: tickUpperData, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(testCtx.whirlpoolCtx.fetcher, poolData!, IGNORE_CACHE), }); const rewardQuote = collectRewardsQuote({ @@ -307,14 +313,15 @@ describe("WhirlpoolImpl#collectFeesAndRewardsForPositions()", () => { position: positionData, tickLower: tickLowerData, tickUpper: tickUpperData, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(testCtx.whirlpoolCtx.fetcher, poolData!, IGNORE_CACHE), timeStampInSeconds: poolData!.rewardLastUpdatedTimestamp, }); assert.ok(feeQuote.feeOwedA.gt(ZERO)); assert.ok(feeQuote.feeOwedB.gt(ZERO)); - assert.ok(rewardQuote[0]?.gt(ZERO)); - assert.ok(rewardQuote[1]?.gt(ZERO)); - assert.ok(rewardQuote[2]?.gt(ZERO)); + assert.ok(rewardQuote.rewardOwed[0]?.gt(ZERO)); + assert.ok(rewardQuote.rewardOwed[1]?.gt(ZERO)); + assert.ok(rewardQuote.rewardOwed[2]?.gt(ZERO)); } const txs = await testCtx.whirlpoolClient.collectFeesAndRewardsForPositions( @@ -356,6 +363,7 @@ describe("WhirlpoolImpl#collectFeesAndRewardsForPositions()", () => { position: positionData, tickLower: tickLowerData, tickUpper: tickUpperData, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(testCtx.whirlpoolCtx.fetcher, poolData!, IGNORE_CACHE), }); const rewardQuote = collectRewardsQuote({ @@ -363,14 +371,15 @@ describe("WhirlpoolImpl#collectFeesAndRewardsForPositions()", () => { position: positionData, tickLower: tickLowerData, tickUpper: tickUpperData, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(testCtx.whirlpoolCtx.fetcher, poolData!, IGNORE_CACHE), timeStampInSeconds: poolData!.rewardLastUpdatedTimestamp, }); assert.ok(feeQuote.feeOwedA.eq(ZERO)); assert.ok(feeQuote.feeOwedB.eq(ZERO)); - assert.ok(rewardQuote[0]?.eq(ZERO)); - assert.ok(rewardQuote[1]?.eq(ZERO)); - assert.ok(rewardQuote[2]?.eq(ZERO)); + assert.ok(rewardQuote.rewardOwed[0]?.eq(ZERO)); + assert.ok(rewardQuote.rewardOwed[1]?.eq(ZERO)); + assert.ok(rewardQuote.rewardOwed[2]?.eq(ZERO)); } } @@ -401,4 +410,272 @@ describe("WhirlpoolImpl#collectFeesAndRewardsForPositions()", () => { await baseTestSenario(tokenAIsNative, ataExists); }); }); + + context("when the whirlpool is TokenExtension-TokenExtension", () => { + async function accrueFeesV2(fixture: WhirlpoolTestFixtureV2) { + const ctx = testCtx.whirlpoolCtx; + const { + poolInitInfo, + positions: [positionInfo], + tokenAccountA, + tokenAccountB, + } = fixture.getInfos(); + + const { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair } = poolInitInfo; + + const tickArrayPda = PDAUtil.getTickArray(ctx.program.programId, whirlpoolPda.publicKey, 22528); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + + const pool = await testCtx.whirlpoolClient.getPool(whirlpoolPda.publicKey); + const position = await testCtx.whirlpoolClient.getPosition(positionInfo.publicKey); + + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(ctx.fetcher, pool.getData(), IGNORE_CACHE); + + // Accrue fees in token A + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + tokenMintA: tokenExtensionCtx.tokenMintWithProgramA.address, + tokenMintB: tokenExtensionCtx.tokenMintWithProgramB.address, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + ...await TokenExtensionUtil.getExtraAccountMetasForTransferHookForPool( + ctx.connection, + tokenExtensionCtx, + tokenAccountA, + tokenVaultAKeypair.publicKey, + ctx.wallet.publicKey, + tokenVaultBKeypair.publicKey, + tokenAccountB, + whirlpoolPda.publicKey, + ), + }) + ).buildAndExecute(); + + // Accrue fees in token B + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(5)), + amountSpecifiedIsInput: true, + aToB: false, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + tokenMintA: tokenExtensionCtx.tokenMintWithProgramA.address, + tokenMintB: tokenExtensionCtx.tokenMintWithProgramB.address, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + ...await TokenExtensionUtil.getExtraAccountMetasForTransferHookForPool( + ctx.connection, + tokenExtensionCtx, + tokenVaultAKeypair.publicKey, + tokenAccountA, + whirlpoolPda.publicKey, + tokenAccountB, + tokenVaultBKeypair.publicKey, + ctx.wallet.publicKey, + ), + }) + ).buildAndExecute(); + + const poolData = await pool.refreshData(); + const positionData = await position.refreshData(); + const tickLowerData = position.getLowerTickData(); + const tickUpperData = position.getLowerTickData(); + + const quote = collectFeesQuote({ + whirlpool: poolData, + position: positionData, + tickLower: tickLowerData, + tickUpper: tickUpperData, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(ctx.fetcher, poolData, IGNORE_CACHE), + }); + + assert.ok(quote.feeOwedA.gtn(0) || quote.feeOwedB.gtn(0)); + } + + async function stopRewardsEmissionV2(fixture: WhirlpoolTestFixtureV2) { + const ctx = testCtx.whirlpoolCtx; + const { poolInitInfo, configKeypairs } = fixture.getInfos(); + const { whirlpoolPda } = poolInitInfo; + + const pool = await testCtx.whirlpoolClient.getPool(whirlpoolPda.publicKey); + + for (let i = 0; i < NUM_REWARDS; i++) { + await toTx( + ctx, + WhirlpoolIx.setRewardEmissionsV2Ix(ctx.program, { + whirlpool: pool.getAddress(), + rewardVaultKey: pool.getData().rewardInfos[i].vault, + rewardAuthority: configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey, + rewardIndex: i, + emissionsPerSecondX64: ZERO, + }) + ).addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair).buildAndExecute(); + } + } + + it("should collect fees and rewards, create all ATAs", async () => { + const fixtures: WhirlpoolTestFixtureV2[] = []; + const positions: FundedPositionInfo[] = []; + const numOfPool = 3; + + for (let i = 0; i < numOfPool; i++) { + const fixture = await new WhirlpoolTestFixtureV2(testCtx.whirlpoolCtx).init({ + tokenTraitA: {isToken2022: true, hasTransferHookExtension: true}, + tokenTraitB: {isToken2022: true, hasTransferHookExtension: true}, + tickSpacing, + positions: [ + // 3 Positions / pool + { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position + { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position + { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position + ], + rewards: [ + { + rewardTokenTrait: {isToken2022: true, hasTransferHookExtension: true}, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: {isToken2022: true, hasTransferHookExtension: true}, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: {isToken2022: true, hasTransferHookExtension: true}, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + + fixtures.push(fixture); + positions.push(...fixture.getInfos().positions); + } + + await sleep(2); // accrueRewards + for (const fixture of fixtures) { + await accrueFeesV2(fixture); + await createATAs(fixture); + await stopRewardsEmissionV2(fixture); + } + + // check all positions have fees and rewards + for (const positionInfo of positions) { + const position = await testCtx.whirlpoolClient.getPosition(positionInfo.publicKey); + + const poolData = await testCtx.whirlpoolCtx.fetcher.getPool(position.getData().whirlpool, IGNORE_CACHE); + const positionData = await position.refreshData(); + const tickLowerData = position.getLowerTickData(); + const tickUpperData = position.getLowerTickData(); + + const feeQuote = collectFeesQuote({ + whirlpool: poolData!, + position: positionData, + tickLower: tickLowerData, + tickUpper: tickUpperData, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(testCtx.whirlpoolCtx.fetcher, poolData!, IGNORE_CACHE), + }); + + const rewardQuote = collectRewardsQuote({ + whirlpool: poolData!, + position: positionData, + tickLower: tickLowerData, + tickUpper: tickUpperData, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(testCtx.whirlpoolCtx.fetcher, poolData!, IGNORE_CACHE), + timeStampInSeconds: poolData!.rewardLastUpdatedTimestamp, + }); + + assert.ok(feeQuote.feeOwedA.gt(ZERO)); + assert.ok(feeQuote.feeOwedB.gt(ZERO)); + assert.ok(rewardQuote.rewardOwed[0]?.gt(ZERO)); + assert.ok(rewardQuote.rewardOwed[1]?.gt(ZERO)); + assert.ok(rewardQuote.rewardOwed[2]?.gt(ZERO)); + } + + const txs = await testCtx.whirlpoolClient.collectFeesAndRewardsForPositions( + positions.map((p) => p.publicKey), + IGNORE_CACHE, + ); + assert.ok(txs.length >= 2); + + // TODO: We should not depend on Transaction Processor for mass txn sending. SendTxRequest is also a hack. + // Remove when we have an official multi-transaction sending solution. + const requests: SendTxRequest[] = []; + for (const tx of txs) { + requests.push(await tx.build() as SendTxRequest); + } + + const parallel = true; + const processor = new TransactionProcessor(testCtx.whirlpoolCtx.connection, testCtx.whirlpoolCtx.wallet); + const { execute } = await processor.signAndConstructTransactions(requests, parallel); + + const txResults = await execute(); + for (const result of txResults) { + if (result.status === "rejected") { + console.log(result.reason); + } + assert.equal(result.status, "fulfilled"); + } + + // check all positions have no fees and rewards + for (const positionInfo of positions) { + const position = await testCtx.whirlpoolClient.getPosition(positionInfo.publicKey); + + const poolData = await testCtx.whirlpoolCtx.fetcher.getPool(position.getData().whirlpool, IGNORE_CACHE); + const positionData = await position.refreshData(); + const tickLowerData = position.getLowerTickData(); + const tickUpperData = position.getLowerTickData(); + + const feeQuote = collectFeesQuote({ + whirlpool: poolData!, + position: positionData, + tickLower: tickLowerData, + tickUpper: tickUpperData, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(testCtx.whirlpoolCtx.fetcher, poolData!, IGNORE_CACHE), + }); + + const rewardQuote = collectRewardsQuote({ + whirlpool: poolData!, + position: positionData, + tickLower: tickLowerData, + tickUpper: tickUpperData, + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(testCtx.whirlpoolCtx.fetcher, poolData!, IGNORE_CACHE), + timeStampInSeconds: poolData!.rewardLastUpdatedTimestamp, + }); + + assert.ok(feeQuote.feeOwedA.eq(ZERO)); + assert.ok(feeQuote.feeOwedB.eq(ZERO)); + assert.ok(rewardQuote.rewardOwed[0]?.eq(ZERO)); + assert.ok(rewardQuote.rewardOwed[1]?.eq(ZERO)); + assert.ok(rewardQuote.rewardOwed[2]?.eq(ZERO)); + } + }) + }) }); diff --git a/sdk/tests/sdk/whirlpools/whirlpool-impl.test.ts b/sdk/tests/sdk/whirlpools/whirlpool-impl.test.ts index 0fc718aa1..e600a4ca3 100644 --- a/sdk/tests/sdk/whirlpools/whirlpool-impl.test.ts +++ b/sdk/tests/sdk/whirlpools/whirlpool-impl.test.ts @@ -12,6 +12,7 @@ import { collectFeesQuote, collectRewardsQuote, decreaseLiquidityQuoteByLiquidity, + increaseLiquidityQuoteByInputToken, increaseLiquidityQuoteByInputTokenUsingPriceSlippage, swapQuoteByInputToken, toTx @@ -20,6 +21,8 @@ import { WhirlpoolContext } from "../../../src/context"; import { IGNORE_CACHE } from "../../../src/network/public/fetcher"; import { ONE_SOL, + TEST_TOKEN_2022_PROGRAM_ID, + TEST_TOKEN_PROGRAM_ID, TickSpacing, ZERO_BN, createAssociatedTokenAccount, @@ -30,8 +33,10 @@ import { } from "../../utils"; import { defaultConfirmOptions } from "../../utils/const"; import { WhirlpoolTestFixture } from "../../utils/fixture"; -import { initTestPool } from "../../utils/init-utils"; -import { mintTokensToTestAccount } from "../../utils/test-builders"; +import { TokenExtensionUtil } from "../../../src/utils/public/token-extension-util"; +import { TokenTrait, initTestPoolV2 } from "../../utils/v2/init-utils-v2"; +import { mintTokensToTestAccountV2 } from "../../utils/v2/token-2022"; +import { WhirlpoolTestFixtureV2 } from "../../utils/v2/fixture-v2"; import { ASSOCIATED_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/utils/token"; describe("whirlpool-impl", () => { @@ -42,444 +47,775 @@ describe("whirlpool-impl", () => { const fetcher = ctx.fetcher; const client = buildWhirlpoolClient(ctx); - it("open and add liquidity to a position, then close", async () => { - const funderKeypair = anchor.web3.Keypair.generate(); - await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute(); + const tokenTraitVariations: { + tokenTraitA: TokenTrait; + tokenTraitB: TokenTrait; + }[] = [ + { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: false }, + }, + { + tokenTraitA: { isToken2022: true }, + tokenTraitB: { isToken2022: false }, + }, + { + tokenTraitA: { isToken2022: false }, + tokenTraitB: { isToken2022: true }, + }, + { + // TransferHook is most difficult extension in transaction size + tokenTraitA: { isToken2022: true, hasTransferHookExtension: true }, + tokenTraitB: { isToken2022: true, hasTransferHookExtension: true }, + }, + ]; + tokenTraitVariations.forEach((tokenTraits) => { + describe(`tokenTraitA: ${ + tokenTraits.tokenTraitA.isToken2022 ? "Token2022" : "Token" + }, tokenTraitB: ${ + tokenTraits.tokenTraitB.isToken2022 ? "Token2022" : "Token" + }`, () => { + + it("open and add liquidity to a position, then close [TokenAmount Slippage]", async () => { + const funderKeypair = anchor.web3.Keypair.generate(); + await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute(); + + const { poolInitInfo } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard, + PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6) + ); + const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey); + + // Verify token mint info is correct + const tokenAInfo = pool.getTokenAInfo(); + const tokenBInfo = pool.getTokenBInfo(); + assert.ok(tokenAInfo.mint.equals(poolInitInfo.tokenMintA)); + assert.ok(tokenBInfo.mint.equals(poolInitInfo.tokenMintB)); + + // Create and mint tokens in this wallet + const mintedTokenAmount = 150_000_000; + const [userTokenAAccount, userTokenBAccount] = await mintTokensToTestAccountV2( + ctx.provider, + tokenAInfo.mint, + tokenTraits.tokenTraitA, + mintedTokenAmount, + tokenBInfo.mint, + tokenTraits.tokenTraitB, + mintedTokenAmount + ); - const { poolInitInfo } = await initTestPool( - ctx, - TickSpacing.Standard, - PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6) - ); - const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey); + // Open a position with no tick arrays initialized. + const lowerPrice = new Decimal(96); + const upperPrice = new Decimal(101); + const poolData = pool.getData(); + const tokenADecimal = tokenAInfo.decimals; + const tokenBDecimal = tokenBInfo.decimals; - // Verify token mint info is correct - const tokenAInfo = pool.getTokenAInfo(); - const tokenBInfo = pool.getTokenBInfo(); - assert.ok(tokenAInfo.mint.equals(poolInitInfo.tokenMintA)); - assert.ok(tokenBInfo.mint.equals(poolInitInfo.tokenMintB)); + const tickLower = TickUtil.getInitializableTickIndex( + PriceMath.priceToTickIndex(lowerPrice, tokenADecimal, tokenBDecimal), + poolData.tickSpacing + ); + const tickUpper = TickUtil.getInitializableTickIndex( + PriceMath.priceToTickIndex(upperPrice, tokenADecimal, tokenBDecimal), + poolData.tickSpacing + ); - // Create and mint tokens in this wallet - const mintedTokenAmount = 150_000_000; - const [userTokenAAccount, userTokenBAccount] = await mintTokensToTestAccount( - ctx.provider, - tokenAInfo.mint, - mintedTokenAmount, - tokenBInfo.mint, - mintedTokenAmount - ); + const inputTokenMint = poolData.tokenMintA; + const quote = increaseLiquidityQuoteByInputToken( + inputTokenMint, + new Decimal(50), + tickLower, + tickUpper, + Percentage.fromFraction(1, 100), + pool, + await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolData, IGNORE_CACHE), + ); - // Open a position with no tick arrays initialized. - const lowerPrice = new Decimal(96); - const upperPrice = new Decimal(101); - const poolData = pool.getData(); - const tokenADecimal = tokenAInfo.decimals; - const tokenBDecimal = tokenBInfo.decimals; + // [Action] Initialize Tick Arrays + const initTickArrayTx = ( + await pool.initTickArrayForTicks([tickLower, tickUpper], funderKeypair.publicKey) + )?.addSigner(funderKeypair); - const tickLower = TickUtil.getInitializableTickIndex( - PriceMath.priceToTickIndex(lowerPrice, tokenADecimal, tokenBDecimal), - poolData.tickSpacing - ); - const tickUpper = TickUtil.getInitializableTickIndex( - PriceMath.priceToTickIndex(upperPrice, tokenADecimal, tokenBDecimal), - poolData.tickSpacing - ); + assert.ok(!!initTickArrayTx); - const inputTokenMint = poolData.tokenMintA; - const quote = increaseLiquidityQuoteByInputTokenUsingPriceSlippage( - inputTokenMint, - new Decimal(50), - tickLower, - tickUpper, - Percentage.fromFraction(1, 100), - pool - ); + // [Action] Open Position (and increase L) + const { positionMint, tx: openIx } = await pool.openPosition( + tickLower, + tickUpper, + quote, + ctx.wallet.publicKey, + funderKeypair.publicKey + ); + openIx.addSigner(funderKeypair); - // [Action] Initialize Tick Arrays - const initTickArrayTx = ( - await pool.initTickArrayForTicks([tickLower, tickUpper], funderKeypair.publicKey) - )?.addSigner(funderKeypair); + await initTickArrayTx.buildAndExecute(); + await openIx.buildAndExecute(); - assert.ok(!!initTickArrayTx); + // Verify position exists and numbers fit input parameters + const positionAddress = PDAUtil.getPosition(ctx.program.programId, positionMint).publicKey; + const position = await client.getPosition(positionAddress, IGNORE_CACHE); + const positionData = position.getData(); - // [Action] Open Position (and increase L) - const { positionMint, tx: openIx } = await pool.openPosition( - tickLower, - tickUpper, - quote, - ctx.wallet.publicKey, - funderKeypair.publicKey - ); - openIx.addSigner(funderKeypair); + const tickLowerIndex = TickUtil.getInitializableTickIndex( + PriceMath.priceToTickIndex(lowerPrice, tokenAInfo.decimals, tokenBInfo.decimals), + poolData.tickSpacing + ); + const tickUpperIndex = TickUtil.getInitializableTickIndex( + PriceMath.priceToTickIndex(upperPrice, tokenAInfo.decimals, tokenBInfo.decimals), + poolData.tickSpacing + ); + assert.ok(positionData.liquidity.eq(quote.liquidityAmount)); + assert.ok(positionData.tickLowerIndex === tickLowerIndex); + assert.ok(positionData.tickUpperIndex === tickUpperIndex); + assert.ok(positionData.positionMint.equals(positionMint)); + assert.ok(positionData.whirlpool.equals(poolInitInfo.whirlpoolPda.publicKey)); + + // [Action] Close Position + const txs = await pool.closePosition(positionAddress, Percentage.fromFraction(1, 100)); + + for (const tx of txs) { + await tx.buildAndExecute(); + } + + // Verify position is closed and owner wallet has the tokens back + const postClosePosition = await fetcher.getPosition(positionAddress, IGNORE_CACHE); + assert.ok(postClosePosition === null); + + // TODO: we are leaking 1 decimal place of token? + assert.equal(await getTokenBalance(ctx.provider, userTokenAAccount), mintedTokenAmount - 1); + assert.equal(await getTokenBalance(ctx.provider, userTokenBAccount), mintedTokenAmount - 1); + }); + + it("open and add liquidity to a position, transfer position to another wallet, then close the tokens to another wallet [TokenAmount Slippage]", async () => { + const funderKeypair = anchor.web3.Keypair.generate(); + await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute(); + + const { poolInitInfo } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard, + PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6) + ); + const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey); + + // Verify token mint info is correct + const tokenAInfo = pool.getTokenAInfo(); + const tokenBInfo = pool.getTokenBInfo(); + assert.ok(tokenAInfo.mint.equals(poolInitInfo.tokenMintA)); + assert.ok(tokenBInfo.mint.equals(poolInitInfo.tokenMintB)); + + // Create and mint tokens in this wallet + const mintedTokenAmount = 150_000_000; + await mintTokensToTestAccountV2( + ctx.provider, + tokenAInfo.mint, + tokenTraits.tokenTraitA, + mintedTokenAmount, + tokenBInfo.mint, + tokenTraits.tokenTraitB, + mintedTokenAmount + ); - await initTickArrayTx.buildAndExecute(); - await openIx.buildAndExecute(); + // Open a position with no tick arrays initialized. + const lowerPrice = new Decimal(96); + const upperPrice = new Decimal(101); + const poolData = pool.getData(); + const tokenADecimal = tokenAInfo.decimals; + const tokenBDecimal = tokenBInfo.decimals; - // Verify position exists and numbers fit input parameters - const positionAddress = PDAUtil.getPosition(ctx.program.programId, positionMint).publicKey; - const position = await client.getPosition(positionAddress, IGNORE_CACHE); - const positionData = position.getData(); + const tickLower = TickUtil.getInitializableTickIndex( + PriceMath.priceToTickIndex(lowerPrice, tokenADecimal, tokenBDecimal), + poolData.tickSpacing + ); + const tickUpper = TickUtil.getInitializableTickIndex( + PriceMath.priceToTickIndex(upperPrice, tokenADecimal, tokenBDecimal), + poolData.tickSpacing + ); - const tickLowerIndex = TickUtil.getInitializableTickIndex( - PriceMath.priceToTickIndex(lowerPrice, tokenAInfo.decimals, tokenBInfo.decimals), - poolData.tickSpacing - ); - const tickUpperIndex = TickUtil.getInitializableTickIndex( - PriceMath.priceToTickIndex(upperPrice, tokenAInfo.decimals, tokenBInfo.decimals), - poolData.tickSpacing - ); - assert.ok(positionData.liquidity.eq(quote.liquidityAmount)); - assert.ok(positionData.tickLowerIndex === tickLowerIndex); - assert.ok(positionData.tickUpperIndex === tickUpperIndex); - assert.ok(positionData.positionMint.equals(positionMint)); - assert.ok(positionData.whirlpool.equals(poolInitInfo.whirlpoolPda.publicKey)); + const inputTokenMint = poolData.tokenMintA; + const depositAmount = new Decimal(50); + const quote = increaseLiquidityQuoteByInputToken( + inputTokenMint, + depositAmount, + tickLower, + tickUpper, + Percentage.fromFraction(1, 100), + pool, + await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolData, IGNORE_CACHE), + ); - // [Action] Close Position - const txs = await pool.closePosition(positionAddress, Percentage.fromFraction(1, 100)); + // [Action] Initialize Tick Arrays + const initTickArrayTx = ( + await pool.initTickArrayForTicks([tickLower, tickUpper], funderKeypair.publicKey) + )?.addSigner(funderKeypair); - for (const tx of txs) { - await tx.buildAndExecute(); - } + assert.ok(!!initTickArrayTx); - // Verify position is closed and owner wallet has the tokens back - const postClosePosition = await fetcher.getPosition(positionAddress, IGNORE_CACHE); - assert.ok(postClosePosition === null); + // [Action] Open Position (and increase L) + const { positionMint, tx: openIx } = await pool.openPosition( + tickLower, + tickUpper, + quote, + ctx.wallet.publicKey, + funderKeypair.publicKey + ); + openIx.addSigner(funderKeypair); - // TODO: we are leaking 1 decimal place of token? - assert.equal(await getTokenBalance(ctx.provider, userTokenAAccount), mintedTokenAmount - 1); - assert.equal(await getTokenBalance(ctx.provider, userTokenBAccount), mintedTokenAmount - 1); - }); + await initTickArrayTx.buildAndExecute(); + await openIx.buildAndExecute(); - it("open and add liquidity to a position, transfer position to another wallet, then close the tokens to another wallet", async () => { - const funderKeypair = anchor.web3.Keypair.generate(); - await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute(); + // Verify position exists and numbers fit input parameters + const positionAddress = PDAUtil.getPosition(ctx.program.programId, positionMint).publicKey; + const position = await client.getPosition(positionAddress, IGNORE_CACHE); + const positionData = position.getData(); - const { poolInitInfo } = await initTestPool( - ctx, - TickSpacing.Standard, - PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6) - ); - const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey); + const tickLowerIndex = TickUtil.getInitializableTickIndex( + PriceMath.priceToTickIndex(lowerPrice, tokenAInfo.decimals, tokenBInfo.decimals), + poolData.tickSpacing + ); + const tickUpperIndex = TickUtil.getInitializableTickIndex( + PriceMath.priceToTickIndex(upperPrice, tokenAInfo.decimals, tokenBInfo.decimals), + poolData.tickSpacing + ); + assert.ok(positionData.liquidity.eq(quote.liquidityAmount)); + assert.ok(positionData.tickLowerIndex === tickLowerIndex); + assert.ok(positionData.tickUpperIndex === tickUpperIndex); + assert.ok(positionData.positionMint.equals(positionMint)); + assert.ok(positionData.whirlpool.equals(poolInitInfo.whirlpoolPda.publicKey)); + + // Transfer the position token to another wallet + const otherWallet = anchor.web3.Keypair.generate(); + const walletPositionTokenAccount = getAssociatedTokenAddressSync(positionMint, ctx.wallet.publicKey); + const newOwnerPositionTokenAccount = await createAssociatedTokenAccount( + ctx.provider, + positionMint, + otherWallet.publicKey, + ctx.wallet.publicKey + ); + await transferToken(provider, walletPositionTokenAccount, newOwnerPositionTokenAccount, 1); - // Verify token mint info is correct - const tokenAInfo = pool.getTokenAInfo(); - const tokenBInfo = pool.getTokenBInfo(); - assert.ok(tokenAInfo.mint.equals(poolInitInfo.tokenMintA)); - assert.ok(tokenBInfo.mint.equals(poolInitInfo.tokenMintB)); + // [Action] Close Position + const expectationQuote = await decreaseLiquidityQuoteByLiquidity( + positionData.liquidity, + Percentage.fromDecimal(new Decimal(0)), + position, + pool, + await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolData, IGNORE_CACHE), + ); - // Create and mint tokens in this wallet - const mintedTokenAmount = 150_000_000; - await mintTokensToTestAccount( - ctx.provider, - tokenAInfo.mint, - mintedTokenAmount, - tokenBInfo.mint, - mintedTokenAmount - ); + const destinationWallet = anchor.web3.Keypair.generate(); - // Open a position with no tick arrays initialized. - const lowerPrice = new Decimal(96); - const upperPrice = new Decimal(101); - const poolData = pool.getData(); - const tokenADecimal = tokenAInfo.decimals; - const tokenBDecimal = tokenBInfo.decimals; + const txs = await pool.closePosition( + positionAddress, + Percentage.fromFraction(1, 100), + destinationWallet.publicKey, + otherWallet.publicKey, + ctx.wallet.publicKey + ); - const tickLower = TickUtil.getInitializableTickIndex( - PriceMath.priceToTickIndex(lowerPrice, tokenADecimal, tokenBDecimal), - poolData.tickSpacing - ); - const tickUpper = TickUtil.getInitializableTickIndex( - PriceMath.priceToTickIndex(upperPrice, tokenADecimal, tokenBDecimal), - poolData.tickSpacing - ); + for (const tx of txs) { + await tx.addSigner(otherWallet).buildAndExecute(); + } - const inputTokenMint = poolData.tokenMintA; - const depositAmount = new Decimal(50); - const quote = increaseLiquidityQuoteByInputTokenUsingPriceSlippage( - inputTokenMint, - depositAmount, - tickLower, - tickUpper, - Percentage.fromFraction(1, 100), - pool - ); + // Verify position is closed and owner wallet has the tokens back + const postClosePosition = await fetcher.getPosition(positionAddress, IGNORE_CACHE); + assert.ok(postClosePosition === null); - // [Action] Initialize Tick Arrays - const initTickArrayTx = ( - await pool.initTickArrayForTicks([tickLower, tickUpper], funderKeypair.publicKey) - )?.addSigner(funderKeypair); + const tokenProgramA = tokenTraits.tokenTraitA.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID; + const tokenProgramB = tokenTraits.tokenTraitB.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID; + const dWalletTokenAAccount = getAssociatedTokenAddressSync(poolData.tokenMintA, destinationWallet.publicKey, undefined, tokenProgramA); + const dWalletTokenBAccount = getAssociatedTokenAddressSync(poolData.tokenMintB, destinationWallet.publicKey, undefined, tokenProgramB); - assert.ok(!!initTickArrayTx); + assert.equal( + await getTokenBalance(ctx.provider, dWalletTokenAAccount), + expectationQuote.tokenMinA.toString() + ); + assert.equal( + await getTokenBalance(ctx.provider, dWalletTokenBAccount), + expectationQuote.tokenMinB.toString() + ); + }); + + it("open and add liquidity to a position, then close [Price Slippage]", async () => { + const funderKeypair = anchor.web3.Keypair.generate(); + await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute(); + + const { poolInitInfo } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard, + PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6) + ); + const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey); + + // Verify token mint info is correct + const tokenAInfo = pool.getTokenAInfo(); + const tokenBInfo = pool.getTokenBInfo(); + assert.ok(tokenAInfo.mint.equals(poolInitInfo.tokenMintA)); + assert.ok(tokenBInfo.mint.equals(poolInitInfo.tokenMintB)); + + // Create and mint tokens in this wallet + const mintedTokenAmount = 150_000_000; + const [userTokenAAccount, userTokenBAccount] = await mintTokensToTestAccountV2( + ctx.provider, + tokenAInfo.mint, + tokenTraits.tokenTraitA, + mintedTokenAmount, + tokenBInfo.mint, + tokenTraits.tokenTraitB, + mintedTokenAmount + ); - // [Action] Open Position (and increase L) - const { positionMint, tx: openIx } = await pool.openPosition( - tickLower, - tickUpper, - quote, - ctx.wallet.publicKey, - funderKeypair.publicKey - ); - openIx.addSigner(funderKeypair); + // Open a position with no tick arrays initialized. + const lowerPrice = new Decimal(96); + const upperPrice = new Decimal(101); + const poolData = pool.getData(); + const tokenADecimal = tokenAInfo.decimals; + const tokenBDecimal = tokenBInfo.decimals; - await initTickArrayTx.buildAndExecute(); - await openIx.buildAndExecute(); + const tickLower = TickUtil.getInitializableTickIndex( + PriceMath.priceToTickIndex(lowerPrice, tokenADecimal, tokenBDecimal), + poolData.tickSpacing + ); + const tickUpper = TickUtil.getInitializableTickIndex( + PriceMath.priceToTickIndex(upperPrice, tokenADecimal, tokenBDecimal), + poolData.tickSpacing + ); - // Verify position exists and numbers fit input parameters - const positionAddress = PDAUtil.getPosition(ctx.program.programId, positionMint).publicKey; - const position = await client.getPosition(positionAddress, IGNORE_CACHE); - const positionData = position.getData(); + const inputTokenMint = poolData.tokenMintA; + const quote = increaseLiquidityQuoteByInputTokenUsingPriceSlippage( + inputTokenMint, + new Decimal(50), + tickLower, + tickUpper, + Percentage.fromFraction(1, 100), + pool, + await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolData, IGNORE_CACHE), + ); - const tickLowerIndex = TickUtil.getInitializableTickIndex( - PriceMath.priceToTickIndex(lowerPrice, tokenAInfo.decimals, tokenBInfo.decimals), - poolData.tickSpacing - ); - const tickUpperIndex = TickUtil.getInitializableTickIndex( - PriceMath.priceToTickIndex(upperPrice, tokenAInfo.decimals, tokenBInfo.decimals), - poolData.tickSpacing - ); - assert.ok(positionData.liquidity.eq(quote.liquidityAmount)); - assert.ok(positionData.tickLowerIndex === tickLowerIndex); - assert.ok(positionData.tickUpperIndex === tickUpperIndex); - assert.ok(positionData.positionMint.equals(positionMint)); - assert.ok(positionData.whirlpool.equals(poolInitInfo.whirlpoolPda.publicKey)); + // [Action] Initialize Tick Arrays + const initTickArrayTx = ( + await pool.initTickArrayForTicks([tickLower, tickUpper], funderKeypair.publicKey) + )?.addSigner(funderKeypair); - // Transfer the position token to another wallet - const otherWallet = anchor.web3.Keypair.generate(); - const walletPositionTokenAccount = getAssociatedTokenAddressSync(positionMint, ctx.wallet.publicKey); - const newOwnerPositionTokenAccount = await createAssociatedTokenAccount( - ctx.provider, - positionMint, - otherWallet.publicKey, - ctx.wallet.publicKey - ); - await transferToken(provider, walletPositionTokenAccount, newOwnerPositionTokenAccount, 1); + assert.ok(!!initTickArrayTx); - // [Action] Close Position - const expectationQuote = await decreaseLiquidityQuoteByLiquidity( - positionData.liquidity, - Percentage.fromDecimal(new Decimal(0)), - position, - pool - ); + // [Action] Open Position (and increase L) + const { positionMint, tx: openIx } = await pool.openPosition( + tickLower, + tickUpper, + quote, + ctx.wallet.publicKey, + funderKeypair.publicKey + ); + openIx.addSigner(funderKeypair); - const destinationWallet = anchor.web3.Keypair.generate(); + await initTickArrayTx.buildAndExecute(); + await openIx.buildAndExecute(); - const txs = await pool.closePosition( - positionAddress, - Percentage.fromFraction(1, 100), - destinationWallet.publicKey, - otherWallet.publicKey, - ctx.wallet.publicKey - ); + // Verify position exists and numbers fit input parameters + const positionAddress = PDAUtil.getPosition(ctx.program.programId, positionMint).publicKey; + const position = await client.getPosition(positionAddress, IGNORE_CACHE); + const positionData = position.getData(); - let ataTx: TransactionBuilder | undefined; - let closeTx: TransactionBuilder; - if (txs.length === 1) { - closeTx = txs[0]; - } else if (txs.length === 2) { - ataTx = txs[0]; - closeTx = txs[1]; - } else { - throw new Error(`Invalid length for txs ${txs}`); - } + const tickLowerIndex = TickUtil.getInitializableTickIndex( + PriceMath.priceToTickIndex(lowerPrice, tokenAInfo.decimals, tokenBInfo.decimals), + poolData.tickSpacing + ); + const tickUpperIndex = TickUtil.getInitializableTickIndex( + PriceMath.priceToTickIndex(upperPrice, tokenAInfo.decimals, tokenBInfo.decimals), + poolData.tickSpacing + ); + assert.ok(positionData.liquidity.eq(quote.liquidityAmount)); + assert.ok(positionData.tickLowerIndex === tickLowerIndex); + assert.ok(positionData.tickUpperIndex === tickUpperIndex); + assert.ok(positionData.positionMint.equals(positionMint)); + assert.ok(positionData.whirlpool.equals(poolInitInfo.whirlpoolPda.publicKey)); + + // [Action] Close Position + const txs = await pool.closePosition(positionAddress, Percentage.fromFraction(1, 100)); + + for (const tx of txs) { + await tx.buildAndExecute(); + } + + // Verify position is closed and owner wallet has the tokens back + const postClosePosition = await fetcher.getPosition(positionAddress, IGNORE_CACHE); + assert.ok(postClosePosition === null); + + // TODO: we are leaking 1 decimal place of token? + assert.equal(await getTokenBalance(ctx.provider, userTokenAAccount), mintedTokenAmount - 1); + assert.equal(await getTokenBalance(ctx.provider, userTokenBAccount), mintedTokenAmount - 1); + }); + + it("open and add liquidity to a position, transfer position to another wallet, then close the tokens to another wallet [Price Slippage]", async () => { + const funderKeypair = anchor.web3.Keypair.generate(); + await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute(); + + const { poolInitInfo } = await initTestPoolV2( + ctx, + tokenTraits.tokenTraitA, + tokenTraits.tokenTraitB, + TickSpacing.Standard, + PriceMath.priceToSqrtPriceX64(new Decimal(100), 6, 6) + ); + const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey); + + // Verify token mint info is correct + const tokenAInfo = pool.getTokenAInfo(); + const tokenBInfo = pool.getTokenBInfo(); + assert.ok(tokenAInfo.mint.equals(poolInitInfo.tokenMintA)); + assert.ok(tokenBInfo.mint.equals(poolInitInfo.tokenMintB)); + + // Create and mint tokens in this wallet + const mintedTokenAmount = 150_000_000; + await mintTokensToTestAccountV2( + ctx.provider, + tokenAInfo.mint, + tokenTraits.tokenTraitA, + mintedTokenAmount, + tokenBInfo.mint, + tokenTraits.tokenTraitB, + mintedTokenAmount + ); - await ataTx?.buildAndExecute(); - await closeTx.addSigner(otherWallet).buildAndExecute(); + // Open a position with no tick arrays initialized. + const lowerPrice = new Decimal(96); + const upperPrice = new Decimal(101); + const poolData = pool.getData(); + const tokenADecimal = tokenAInfo.decimals; + const tokenBDecimal = tokenBInfo.decimals; - // Verify position is closed and owner wallet has the tokens back - const postClosePosition = await fetcher.getPosition(positionAddress, IGNORE_CACHE); - assert.ok(postClosePosition === null); + const tickLower = TickUtil.getInitializableTickIndex( + PriceMath.priceToTickIndex(lowerPrice, tokenADecimal, tokenBDecimal), + poolData.tickSpacing + ); + const tickUpper = TickUtil.getInitializableTickIndex( + PriceMath.priceToTickIndex(upperPrice, tokenADecimal, tokenBDecimal), + poolData.tickSpacing + ); - const dWalletTokenAAccount = getAssociatedTokenAddressSync(poolData.tokenMintA, destinationWallet.publicKey,); - const dWalletTokenBAccount = getAssociatedTokenAddressSync(poolData.tokenMintB, destinationWallet.publicKey); + const inputTokenMint = poolData.tokenMintA; + const depositAmount = new Decimal(50); + const quote = increaseLiquidityQuoteByInputTokenUsingPriceSlippage( + inputTokenMint, + depositAmount, + tickLower, + tickUpper, + Percentage.fromFraction(1, 100), + pool, + await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolData, IGNORE_CACHE), + ); - assert.equal( - await getTokenBalance(ctx.provider, dWalletTokenAAccount), - expectationQuote.tokenMinA.toString() - ); - assert.equal( - await getTokenBalance(ctx.provider, dWalletTokenBAccount), - expectationQuote.tokenMinB.toString() - ); - }); + // [Action] Initialize Tick Arrays + const initTickArrayTx = ( + await pool.initTickArrayForTicks([tickLower, tickUpper], funderKeypair.publicKey) + )?.addSigner(funderKeypair); - it("open and add liquidity to a position, trade against it, transfer position to another wallet, then close the tokens to another wallet", async () => { - // In same tick array - start index 22528 - const tickLowerIndex = 29440; - const tickUpperIndex = 33536; - const vaultStartBalance = 1_000_000_000; - const tickSpacing = TickSpacing.Standard; - const fixture = await new WhirlpoolTestFixture(ctx).init({ - tickSpacing, - positions: [ - { tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }, // In range position - { tickLowerIndex: 0, tickUpperIndex: 128, liquidityAmount: new anchor.BN(1_000_000) }, // Out of range position - ], - rewards: [ - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(5)), - vaultAmount: new BN(vaultStartBalance), - }, - { - emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), - vaultAmount: new BN(vaultStartBalance), - }, - ], - }); - const { - poolInitInfo: { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair }, - tokenAccountA, - tokenAccountB, - positions, - } = fixture.getInfos(); + assert.ok(!!initTickArrayTx); - const tickArrayPda = PDAUtil.getTickArray(ctx.program.programId, whirlpoolPda.publicKey, 22528); - const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); + // [Action] Open Position (and increase L) + const { positionMint, tx: openIx } = await pool.openPosition( + tickLower, + tickUpper, + quote, + ctx.wallet.publicKey, + funderKeypair.publicKey + ); + openIx.addSigner(funderKeypair); - // Accrue fees in token A - await toTx( - ctx, - WhirlpoolIx.swapIx(ctx.program, { - amount: new BN(200_000), - otherAmountThreshold: ZERO_BN, - sqrtPriceLimit: MathUtil.toX64(new Decimal(4)), - amountSpecifiedIsInput: true, - aToB: true, - whirlpool: whirlpoolPda.publicKey, - tokenAuthority: ctx.wallet.publicKey, - tokenOwnerAccountA: tokenAccountA, - tokenVaultA: tokenVaultAKeypair.publicKey, - tokenOwnerAccountB: tokenAccountB, - tokenVaultB: tokenVaultBKeypair.publicKey, - tickArray0: tickArrayPda.publicKey, - tickArray1: tickArrayPda.publicKey, - tickArray2: tickArrayPda.publicKey, - oracle: oraclePda.publicKey, - }) - ).buildAndExecute(); + await initTickArrayTx.buildAndExecute(); + await openIx.buildAndExecute(); - // Accrue fees in token B - await toTx( - ctx, - WhirlpoolIx.swapIx(ctx.program, { - amount: new BN(200_000), - otherAmountThreshold: ZERO_BN, - sqrtPriceLimit: MathUtil.toX64(new Decimal(5)), - amountSpecifiedIsInput: true, - aToB: false, - whirlpool: whirlpoolPda.publicKey, - tokenAuthority: ctx.wallet.publicKey, - tokenOwnerAccountA: tokenAccountA, - tokenVaultA: tokenVaultAKeypair.publicKey, - tokenOwnerAccountB: tokenAccountB, - tokenVaultB: tokenVaultBKeypair.publicKey, - tickArray0: tickArrayPda.publicKey, - tickArray1: tickArrayPda.publicKey, - tickArray2: tickArrayPda.publicKey, - oracle: oraclePda.publicKey, - }) - ).buildAndExecute(); + // Verify position exists and numbers fit input parameters + const positionAddress = PDAUtil.getPosition(ctx.program.programId, positionMint).publicKey; + const position = await client.getPosition(positionAddress, IGNORE_CACHE); + const positionData = position.getData(); - // accrue rewards - // closePosition does not attempt to create an ATA unless reward has accumulated. - await sleep(1200); + const tickLowerIndex = TickUtil.getInitializableTickIndex( + PriceMath.priceToTickIndex(lowerPrice, tokenAInfo.decimals, tokenBInfo.decimals), + poolData.tickSpacing + ); + const tickUpperIndex = TickUtil.getInitializableTickIndex( + PriceMath.priceToTickIndex(upperPrice, tokenAInfo.decimals, tokenBInfo.decimals), + poolData.tickSpacing + ); + assert.ok(positionData.liquidity.eq(quote.liquidityAmount)); + assert.ok(positionData.tickLowerIndex === tickLowerIndex); + assert.ok(positionData.tickUpperIndex === tickUpperIndex); + assert.ok(positionData.positionMint.equals(positionMint)); + assert.ok(positionData.whirlpool.equals(poolInitInfo.whirlpoolPda.publicKey)); + + // Transfer the position token to another wallet + const otherWallet = anchor.web3.Keypair.generate(); + const walletPositionTokenAccount = getAssociatedTokenAddressSync(positionMint, ctx.wallet.publicKey); + const newOwnerPositionTokenAccount = await createAssociatedTokenAccount( + ctx.provider, + positionMint, + otherWallet.publicKey, + ctx.wallet.publicKey + ); + await transferToken(provider, walletPositionTokenAccount, newOwnerPositionTokenAccount, 1); - const [positionWithFees] = positions; + // [Action] Close Position + const expectationQuote = await decreaseLiquidityQuoteByLiquidity( + positionData.liquidity, + Percentage.fromDecimal(new Decimal(0)), + position, + pool, + await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolData, IGNORE_CACHE), + ); - // Transfer the position token to another wallet - const otherWallet = anchor.web3.Keypair.generate(); - const walletPositionTokenAccount = getAssociatedTokenAddressSync( - positionWithFees.mintKeypair.publicKey, - ctx.wallet.publicKey, - ); + const destinationWallet = anchor.web3.Keypair.generate(); - const newOwnerPositionTokenAccount = await createAssociatedTokenAccount( - ctx.provider, - positionWithFees.mintKeypair.publicKey, - otherWallet.publicKey, - ctx.wallet.publicKey - ); + const txs = await pool.closePosition( + positionAddress, + Percentage.fromFraction(1, 100), + destinationWallet.publicKey, + otherWallet.publicKey, + ctx.wallet.publicKey + ); - await transferToken(provider, walletPositionTokenAccount, newOwnerPositionTokenAccount, 1); + for (const tx of txs) { + await tx.addSigner(otherWallet).buildAndExecute(); + } - const pool = await client.getPool(whirlpoolPda.publicKey, IGNORE_CACHE); - const position = await client.getPosition(positionWithFees.publicKey, IGNORE_CACHE); - const positionData = position.getData(); - const poolData = pool.getData(); - const txs = await pool.closePosition( - positionWithFees.publicKey, - new Percentage(new BN(10), new BN(100)), - otherWallet.publicKey, - otherWallet.publicKey, - ctx.wallet.publicKey - ); + // Verify position is closed and owner wallet has the tokens back + const postClosePosition = await fetcher.getPosition(positionAddress, IGNORE_CACHE); + assert.ok(postClosePosition === null); - const expectationQuote = decreaseLiquidityQuoteByLiquidity( - position.getData().liquidity, - Percentage.fromDecimal(new Decimal(0)), - position, - pool - ); + const tokenProgramA = tokenTraits.tokenTraitA.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID; + const tokenProgramB = tokenTraits.tokenTraitB.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID; + const dWalletTokenAAccount = getAssociatedTokenAddressSync(poolData.tokenMintA, destinationWallet.publicKey, undefined, tokenProgramA); + const dWalletTokenBAccount = getAssociatedTokenAddressSync(poolData.tokenMintB, destinationWallet.publicKey, undefined, tokenProgramB); - const dWalletTokenAAccount = getAssociatedTokenAddressSync(poolData.tokenMintA, otherWallet.publicKey,); - const dWalletTokenBAccount = getAssociatedTokenAddressSync(poolData.tokenMintB, otherWallet.publicKey,); - const rewardAccount0 = getAssociatedTokenAddressSync(poolData.rewardInfos[0].mint, otherWallet.publicKey,); - const rewardAccount1 = getAssociatedTokenAddressSync(poolData.rewardInfos[1].mint, otherWallet.publicKey,); - const rewardAccount2 = getAssociatedTokenAddressSync(poolData.rewardInfos[2].mint, otherWallet.publicKey,); + assert.equal( + await getTokenBalance(ctx.provider, dWalletTokenAAccount), + expectationQuote.tokenMinA.toString() + ); + assert.equal( + await getTokenBalance(ctx.provider, dWalletTokenBAccount), + expectationQuote.tokenMinB.toString() + ); + }); - const feesQuote = collectFeesQuote({ - whirlpool: poolData, - position: positionData, - tickLower: position.getLowerTickData(), - tickUpper: position.getUpperTickData(), - }); + it("open and add liquidity to a position, trade against it, transfer position to another wallet, then close the tokens to another wallet", async () => { + // In same tick array - start index 22528 + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const vaultStartBalance = 1_000_000_000; + const tickSpacing = TickSpacing.Standard; + const fixture = await new WhirlpoolTestFixtureV2(ctx).init({ + tokenTraitA: tokenTraits.tokenTraitA, + tokenTraitB: tokenTraits.tokenTraitB, + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(10_000_000) }, // In range position + { tickLowerIndex: 0, tickUpperIndex: 128, liquidityAmount: new anchor.BN(1_000_000) }, // Out of range position + ], + rewards: [ + { + rewardTokenTrait: {isToken2022: false}, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: {isToken2022: false}, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(5)), + vaultAmount: new BN(vaultStartBalance), + }, + { + rewardTokenTrait: {isToken2022: false}, + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + }); + const { + poolInitInfo: { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair }, + tokenAccountA, + tokenAccountB, + positions, + } = fixture.getInfos(); - let ataTx: TransactionBuilder | undefined; - let closeTx: TransactionBuilder; - if (txs.length === 1) { - closeTx = txs[0]; - } else if (txs.length === 2) { - ataTx = txs[0]; - closeTx = txs[1]; - } else { - throw new Error(`Invalid length for txs ${txs}`); - } + const tickArrayPda = PDAUtil.getTickArray(ctx.program.programId, whirlpoolPda.publicKey, 22528); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey); - await ataTx?.buildAndExecute(); - const signature = await closeTx.addSigner(otherWallet).buildAndExecute(); + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext( + ctx.fetcher, + (await client.getPool(whirlpoolPda.publicKey, IGNORE_CACHE)).getData(), + IGNORE_CACHE + ); - // To calculate the rewards that have accumulated up to the timing of the close, - // the block time at transaction execution is used. - // TODO: maxSupportedTransactionVersion needs to come from ctx - const tx = await ctx.provider.connection.getTransaction(signature, { - maxSupportedTransactionVersion: 0 - }); - const closeTimestampInSeconds = new anchor.BN(tx!.blockTime!.toString()); - const rewardsQuote = collectRewardsQuote({ - whirlpool: poolData, - position: positionData, - tickLower: position.getLowerTickData(), - tickUpper: position.getUpperTickData(), - timeStampInSeconds: closeTimestampInSeconds, - }); + // Accrue fees in token A + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + tokenMintA: tokenExtensionCtx.tokenMintWithProgramA.address, + tokenMintB: tokenExtensionCtx.tokenMintWithProgramB.address, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + ...await TokenExtensionUtil.getExtraAccountMetasForTransferHookForPool( + ctx.connection, + tokenExtensionCtx, + tokenAccountA, + tokenVaultAKeypair.publicKey, + ctx.wallet.publicKey, + tokenVaultBKeypair.publicKey, + tokenAccountB, + whirlpoolPda.publicKey, + ), + }) + ).buildAndExecute(); + + // Accrue fees in token B + await toTx( + ctx, + WhirlpoolIx.swapV2Ix(ctx.program, { + amount: new BN(200_000), + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(5)), + amountSpecifiedIsInput: true, + aToB: false, + whirlpool: whirlpoolPda.publicKey, + tokenAuthority: ctx.wallet.publicKey, + tokenOwnerAccountA: tokenAccountA, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenOwnerAccountB: tokenAccountB, + tokenVaultB: tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + tokenMintA: tokenExtensionCtx.tokenMintWithProgramA.address, + tokenMintB: tokenExtensionCtx.tokenMintWithProgramB.address, + tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram, + tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram, + ...await TokenExtensionUtil.getExtraAccountMetasForTransferHookForPool( + ctx.connection, + tokenExtensionCtx, + tokenVaultAKeypair.publicKey, + tokenAccountA, + whirlpoolPda.publicKey, + tokenAccountB, + tokenVaultBKeypair.publicKey, + ctx.wallet.publicKey, + ), + }) + ).buildAndExecute(); + + // accrue rewards + // closePosition does not attempt to create an ATA unless reward has accumulated. + await sleep(1200); + + const [positionWithFees] = positions; + + // Transfer the position token to another wallet + const otherWallet = anchor.web3.Keypair.generate(); + const walletPositionTokenAccount = getAssociatedTokenAddressSync( + positionWithFees.mintKeypair.publicKey, + ctx.wallet.publicKey, + ); - assert.equal( - await getTokenBalance(ctx.provider, dWalletTokenAAccount), - expectationQuote.tokenMinA.add(feesQuote.feeOwedA).toString() - ); + const newOwnerPositionTokenAccount = await createAssociatedTokenAccount( + ctx.provider, + positionWithFees.mintKeypair.publicKey, + otherWallet.publicKey, + ctx.wallet.publicKey + ); - assert.equal( - await getTokenBalance(ctx.provider, dWalletTokenBAccount), - expectationQuote.tokenMinB.add(feesQuote.feeOwedB).toString() - ); + await transferToken(provider, walletPositionTokenAccount, newOwnerPositionTokenAccount, 1); + + const pool = await client.getPool(whirlpoolPda.publicKey, IGNORE_CACHE); + const position = await client.getPosition(positionWithFees.publicKey, IGNORE_CACHE); + const positionData = position.getData(); + const poolData = pool.getData(); + const txs = await pool.closePosition( + positionWithFees.publicKey, + new Percentage(new BN(10), new BN(100)), + otherWallet.publicKey, + otherWallet.publicKey, + ctx.wallet.publicKey + ); + + const expectationQuote = decreaseLiquidityQuoteByLiquidity( + position.getData().liquidity, + Percentage.fromDecimal(new Decimal(0)), + position, + pool, + await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolData, IGNORE_CACHE), + ); + + const dWalletTokenAAccount = getAssociatedTokenAddressSync(poolData.tokenMintA, otherWallet.publicKey, undefined, tokenExtensionCtx.tokenMintWithProgramA.tokenProgram); + const dWalletTokenBAccount = getAssociatedTokenAddressSync(poolData.tokenMintB, otherWallet.publicKey, undefined, tokenExtensionCtx.tokenMintWithProgramB.tokenProgram); + const rewardAccount0 = getAssociatedTokenAddressSync(poolData.rewardInfos[0].mint, otherWallet.publicKey, undefined, tokenExtensionCtx.rewardTokenMintsWithProgram[0]!.tokenProgram); + const rewardAccount1 = getAssociatedTokenAddressSync(poolData.rewardInfos[1].mint, otherWallet.publicKey, undefined, tokenExtensionCtx.rewardTokenMintsWithProgram[1]!.tokenProgram); + const rewardAccount2 = getAssociatedTokenAddressSync(poolData.rewardInfos[2].mint, otherWallet.publicKey, undefined, tokenExtensionCtx.rewardTokenMintsWithProgram[2]!.tokenProgram); + + const feesQuote = collectFeesQuote({ + whirlpool: poolData, + position: positionData, + tickLower: position.getLowerTickData(), + tickUpper: position.getUpperTickData(), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolData, IGNORE_CACHE), + }); + + const signatures: string[] = []; + for (const tx of txs) { + signatures.push(await tx.addSigner(otherWallet).buildAndExecute()); + } + + // To calculate the rewards that have accumulated up to the timing of the close (strictly, decreaseLiquidity), + // the block time at transaction execution is used. + // TODO: maxSupportedTransactionVersion needs to come from ctx + const tx = await ctx.provider.connection.getTransaction(signatures[0], { + maxSupportedTransactionVersion: 0 + }); + const closeTimestampInSeconds = new anchor.BN(tx!.blockTime!.toString()); + const rewardsQuote = collectRewardsQuote({ + whirlpool: poolData, + position: positionData, + tickLower: position.getLowerTickData(), + tickUpper: position.getUpperTickData(), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolData, IGNORE_CACHE), + timeStampInSeconds: closeTimestampInSeconds, + }); - assert.equal(await getTokenBalance(ctx.provider, rewardAccount0), rewardsQuote[0]?.toString()); - assert.equal(await getTokenBalance(ctx.provider, rewardAccount1), rewardsQuote[1]?.toString()); - assert.equal(await getTokenBalance(ctx.provider, rewardAccount2), rewardsQuote[2]?.toString()); + assert.equal( + await getTokenBalance(ctx.provider, dWalletTokenAAccount), + expectationQuote.tokenMinA.add(feesQuote.feeOwedA).toString() + ); + + assert.equal( + await getTokenBalance(ctx.provider, dWalletTokenBAccount), + expectationQuote.tokenMinB.add(feesQuote.feeOwedB).toString() + ); + + assert.equal(await getTokenBalance(ctx.provider, rewardAccount0), rewardsQuote.rewardOwed[0]?.toString()); + assert.equal(await getTokenBalance(ctx.provider, rewardAccount1), rewardsQuote.rewardOwed[1]?.toString()); + assert.equal(await getTokenBalance(ctx.provider, rewardAccount2), rewardsQuote.rewardOwed[2]?.toString()); + }); + }); }); it("open and add liquidity to a position with SOL as token A, trade against it, transfer position to another wallet, then close the tokens to another wallet", async () => { @@ -595,7 +931,8 @@ describe("whirlpool-impl", () => { position.getData().liquidity, Percentage.fromDecimal(new Decimal(0)), position, - pool + pool, + await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolData, IGNORE_CACHE), ); const feesQuote = collectFeesQuote({ @@ -603,6 +940,7 @@ describe("whirlpool-impl", () => { position: positionData, tickLower: position.getLowerTickData(), tickUpper: position.getUpperTickData(), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolData, IGNORE_CACHE), }); const dWalletTokenBAccount = getAssociatedTokenAddressSync(poolData.tokenMintB, otherWallet.publicKey,); @@ -618,27 +956,23 @@ describe("whirlpool-impl", () => { ctx.wallet.publicKey ); - let ataTx: TransactionBuilder | undefined; - let closeTx: TransactionBuilder; - if (txs.length === 1) { - closeTx = txs[0]; - } else if (txs.length === 2) { - ataTx = txs[0]; - closeTx = txs[1]; - } else { + // This test case is TokenProgram/TokenProgram, so at most 2 is appropriate + if (txs.length > 2) { throw new Error(`Invalid length for txs ${txs}`); } const otherWalletBalanceBefore = await ctx.connection.getBalance(otherWallet.publicKey); const positionAccountBalance = await ctx.connection.getBalance(positionWithFees.publicKey); - await ataTx?.buildAndExecute(); - const signature = await closeTx.addSigner(otherWallet).buildAndExecute(); + const signatures: string[] = []; + for (const tx of txs) { + signatures.push(await tx.addSigner(otherWallet).buildAndExecute()); + } - // To calculate the rewards that have accumulated up to the timing of the close, + // To calculate the rewards that have accumulated up to the timing of the close (strictly, decreaseLiquidity), // the block time at transaction execution is used. // TODO: maxSupportedTransactionVersion needs to come from ctx - const tx = await ctx.provider.connection.getTransaction(signature, { + const tx = await ctx.provider.connection.getTransaction(signatures[0], { maxSupportedTransactionVersion: 0, }); const closeTimestampInSeconds = new anchor.BN(tx!.blockTime!.toString()); @@ -647,6 +981,7 @@ describe("whirlpool-impl", () => { position: positionData, tickLower: position.getLowerTickData(), tickUpper: position.getUpperTickData(), + tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(fetcher, poolData, IGNORE_CACHE), timeStampInSeconds: closeTimestampInSeconds, }); @@ -680,9 +1015,9 @@ describe("whirlpool-impl", () => { decreaseLiquidityQuote.tokenMinB.add(feesQuote.feeOwedB).toString() ); - assert.equal(await getTokenBalance(ctx.provider, rewardAccount0), rewardsQuote[0]?.toString()); - assert.equal(await getTokenBalance(ctx.provider, rewardAccount1), rewardsQuote[1]?.toString()); - assert.equal(await getTokenBalance(ctx.provider, rewardAccount2), rewardsQuote[2]?.toString()); + assert.equal(await getTokenBalance(ctx.provider, rewardAccount0), rewardsQuote.rewardOwed[0]?.toString()); + assert.equal(await getTokenBalance(ctx.provider, rewardAccount1), rewardsQuote.rewardOwed[1]?.toString()); + assert.equal(await getTokenBalance(ctx.provider, rewardAccount2), rewardsQuote.rewardOwed[2]?.toString()); }); it("swap with idempotent", async () => { diff --git a/sdk/tests/utils/metaplex.ts b/sdk/tests/utils/metaplex.ts new file mode 100644 index 000000000..c92984b8e --- /dev/null +++ b/sdk/tests/utils/metaplex.ts @@ -0,0 +1,220 @@ +// To eliminate deps on @metaplex-foundation/mpl-token-metadata +// Copied from https://github.com/orca-so/orca-sdks/blob/main/packages/token-sdk/src/metadata/client/metaplex-client.ts + +import { PublicKey } from "@solana/web3.js"; +import invariant from "tiny-invariant"; + +const METADATA_PROGRAM_ID = new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); + +// Metadata should be a just tiny JSON file, 2000ms should be sufficient for most cases +const DEFAULT_GET_OFF_CHAIN_METADATA_TIMEOUT_MS = 2000; + +interface Creator { + address: PublicKey; + verified: boolean; + share: number; +} + +interface Collection { + verified: boolean; + key: PublicKey; +} + +interface Uses { + useMethod: number; + remaining: BigInt; + total: BigInt; +} + +interface OnChainMetadataPrefix { + key: number; + updateAuthority: PublicKey; + mint: PublicKey; + name: string; + symbol: string; + uri: string; + sellerFeeBasisPoints: number; +} + +interface OnChainMetadataCreators { + creators: Creator[]; +} + +interface OnChainMetadataSuffix { + primarySaleHappened: boolean; + isMutable: boolean; + editionNonce: number | null; + tokenStandard: number | null; + collection: Collection | null; + uses: Uses | null; +} + +export type OnChainMetadata = OnChainMetadataPrefix & OnChainMetadataCreators & OnChainMetadataSuffix + +export interface OffChainMetadata { + name?: string; + symbol?: string; + description?: string; + image?: string; +} + +export interface MetaplexClient { + getMetadataAddress(mint: PublicKey): PublicKey; + parseOnChainMetadata(mint: PublicKey, buffer: Buffer | Uint8Array): OnChainMetadata | null; + getOffChainMetadata(metadata: OnChainMetadata, timeoutMs?: number): Promise; +} + +export class MetaplexHttpClient implements MetaplexClient { + + getMetadataAddress(mint: PublicKey): PublicKey { + const seeds = [Buffer.from("metadata"), METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer()]; + return PublicKey.findProgramAddressSync(seeds, METADATA_PROGRAM_ID)[0]; + } + + parseOnChainMetadata(mint: PublicKey, data: Uint8Array | Buffer): OnChainMetadata | null { + try { + const buffer = Buffer.from(data); + const [prefix, creatorsOffset] = parseOnChainMetadataPrefix(buffer, 0); + const [creators, suffixOffset] = parseOnChainMetadataCreators(buffer, creatorsOffset); + const [suffix] = parseOnChainMetadataSuffix(buffer, suffixOffset); + return { ...prefix, ...creators, ...suffix }; + } catch { + console.error(`Failed to parse onchain metadata for ${mint}`) + return null; + } + } + + async getOffChainMetadata(metadata: OnChainMetadata, timeoutMs: number = DEFAULT_GET_OFF_CHAIN_METADATA_TIMEOUT_MS): Promise { + try { + if (metadata.uri === "") { + return null; + } + const response = await fetch(metadata.uri, { signal: AbortSignal.timeout(timeoutMs) }); + if (response.status === 404) { + return null; + } + invariant(response.ok, `Unexpected status code fetching ${metadata.uri}: ${response.status}`); + const json = await response.json(); + invariant(isMetadataResponse(json), "Unexpected offchain metadata response type"); + return json; + } catch { + console.error(`Failed to fetch offchain metadata for ${metadata.mint}`) + return null; + } + } +} + +function readString(buffer: Buffer, offset: number): string { + const readLength = buffer.readUInt32LE(offset); + const bytes = buffer.subarray(offset + 4, offset + 4 + readLength); + const nullIndex = bytes.indexOf(0); + return new TextDecoder().decode(bytes.subarray(0, nullIndex === -1 ? undefined : nullIndex)); +} + +function parseOnChainMetadataPrefix(buffer: Buffer, offset: number): [OnChainMetadataPrefix, number] { + const key = buffer.readUInt8(offset); + offset += 1; + const updateAuthority = new PublicKey(buffer.subarray(offset, offset + 32)); + offset += 32; + const mint = new PublicKey(buffer.subarray(offset, offset + 32)); + offset += 32; + const name = readString(buffer, offset); + offset += 36; + const symbol = readString(buffer, offset); + offset += 14; + const uri = readString(buffer, offset); + offset += 204; + const sellerFeeBasisPoints = buffer.readUInt16LE(offset); + offset += 2; + return [ + { key, updateAuthority, mint, name, symbol, uri, sellerFeeBasisPoints }, + offset + ]; +} + +function parseOnChainMetadataCreators(buffer: Buffer, offset: number): [OnChainMetadataCreators, number] { + const creatorsPresent = !!buffer.readUInt8(offset); + offset += 1; + if (!creatorsPresent) { + return [{ creators: [] }, offset]; + } + const creatorCount = buffer.readUInt16LE(offset); + offset += 4; + let creators: Creator[] = []; + for (let i = 0; i < creatorCount; i++) { + const address = new PublicKey(buffer.subarray(offset, offset + 32)); + offset += 32; + const verified = !!buffer.readUInt8(offset); + offset += 1; + const share = buffer.readUInt8(offset); + offset += 1; + creators.push({ address, verified, share }); + } + return [{ creators }, offset] ; +} + +function parseOnChainMetadataSuffix(buffer: Buffer, offset: number): [OnChainMetadataSuffix, number] { + const primarySaleHappened = !!buffer.readUInt8(offset); + offset += 1; + const isMutable = !!buffer.readUInt8(offset); + offset += 1; + const editionNoncePresent = !!buffer.readUInt8(offset); + offset += 1; + let editionNonce: number | null = null; + if (editionNoncePresent) { + editionNonce = editionNoncePresent ? buffer.readUInt8(offset) : null; + offset += 1; + } + const tokenStandardPresent = !!buffer.readUInt8(offset); + offset += 1; + let tokenStandard: number | null = null; + if (tokenStandardPresent) { + tokenStandard = tokenStandardPresent ? buffer.readUInt8(offset) : null; + offset += 1; + } + const collectionPresent = !!buffer.readUInt8(offset); + offset += 1; + let collection: Collection | null = null; + if (collectionPresent) { + const collectionVerified = !!buffer.readUInt8(offset); + offset += 1; + const collectionKey = new PublicKey(buffer.subarray(offset, offset + 32)); + offset += 32; + collection = collectionPresent ? { verified: collectionVerified, key: collectionKey } : null; + } + const usesPresent = !!buffer.readUInt8(offset); + offset += 1; + let uses: Uses | null = null; + if (usesPresent) { + const useMethod = buffer.readUInt8(offset); + offset += 1; + const remaining = buffer.readBigUInt64LE(offset); + offset += 8; + const total = buffer.readBigUInt64LE(offset); + offset += 8; + uses = usesPresent ? { useMethod, remaining, total } : null; + } + return [ + { primarySaleHappened, isMutable, editionNonce, tokenStandard, collection, uses }, + offset + ]; +} + +function isMetadataResponse(value: any): value is OffChainMetadata { + if (!value || typeof value !== "object") { + return false; + } + if (value.name && typeof value.name !== "string") { + return false; + } + if (value.image && typeof value.image !== "string") { + return false; + } + if (value.description && typeof value.description !== "string") { + return false; + } + if (value.symbol && typeof value.symbol !== "string") { + return false; + } + return true; +} \ No newline at end of file diff --git a/sdk/tests/utils/test-builders.ts b/sdk/tests/utils/test-builders.ts index da8e91037..cc1e84c6b 100644 --- a/sdk/tests/utils/test-builders.ts +++ b/sdk/tests/utils/test-builders.ts @@ -7,6 +7,7 @@ import { Keypair, PublicKey } from "@solana/web3.js"; import Decimal from "decimal.js"; import { createAndMintToAssociatedTokenAccount, createMint } from "."; import { + IGNORE_CACHE, InitConfigParams, InitFeeTierParams, InitPoolParams, @@ -17,6 +18,7 @@ import { increaseLiquidityQuoteByInputTokenUsingPriceSlippage, } from "../../src"; import { WhirlpoolContext } from "../../src/context"; +import { TokenExtensionUtil } from "../../src/utils/public/token-extension-util"; export interface TestWhirlpoolsConfigKeypairs { feeAuthorityKeypair: Keypair; @@ -221,7 +223,8 @@ export async function initPosition( lowerTick, upperTick, Percentage.fromFraction(1, 100), - pool + pool, + await TokenExtensionUtil.buildTokenExtensionContext(ctx.fetcher, pool.getData(), IGNORE_CACHE), ); // [Action] Open Position (and increase L) diff --git a/sdk/tests/utils/test-consts.ts b/sdk/tests/utils/test-consts.ts index 82d76738a..083f10ca2 100644 --- a/sdk/tests/utils/test-consts.ts +++ b/sdk/tests/utils/test-consts.ts @@ -1,8 +1,12 @@ import * as anchor from "@coral-xyz/anchor"; -import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token"; import { BN } from "bn.js"; export const TEST_TOKEN_PROGRAM_ID = new anchor.web3.PublicKey(TOKEN_PROGRAM_ID.toString()); +export const TEST_TOKEN_2022_PROGRAM_ID = new anchor.web3.PublicKey(TOKEN_2022_PROGRAM_ID.toString()); + +// sdk/tests/external_program/transfer_hook_counter.so +export const TEST_TRANSFER_HOOK_PROGRAM_ID = new anchor.web3.PublicKey("EBZDYx7599krFc4m2govwBdZcicr4GgepqC78m71nsHS"); export const ZERO_BN = new anchor.BN(0); diff --git a/sdk/tests/utils/v2/aquarium-v2.ts b/sdk/tests/utils/v2/aquarium-v2.ts new file mode 100644 index 000000000..855973754 --- /dev/null +++ b/sdk/tests/utils/v2/aquarium-v2.ts @@ -0,0 +1,324 @@ +import * as anchor from "@coral-xyz/anchor"; +import { AddressUtil, MathUtil, PDA } from "@orca-so/common-sdk"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import Decimal from "decimal.js"; +import { + TickSpacing, +} from ".."; +import { + InitFeeTierParams, + InitPoolV2Params, + PDAUtil, + WhirlpoolContext, + WhirlpoolIx, + toTx +} from "../../../src"; +import { PoolUtil } from "../../../src/utils/public/pool-utils"; +import { + TestConfigParams, + generateDefaultConfigParams, +} from "../test-builders"; +import { FundedPositionV2Params, TokenTrait, fundPositionsV2, generateDefaultConfigExtensionParams, isTokenBadgeRequired } from "./init-utils-v2"; +import { initFeeTier, initTickArrayRange } from "../init-utils"; +import { createAndMintToAssociatedTokenAccountV2, createMintV2 } from "./token-2022"; +import invariant from "tiny-invariant"; + +interface InitTestFeeTierV2Params { + tickSpacing: number; + feeRate?: number; +} + +interface InitTestPoolV2Params { + mintIndices: [number, number]; + tickSpacing: number; + feeTierIndex?: number; + initSqrtPrice?: anchor.BN; +} + +interface InitTestMintV2Params { + tokenTrait: TokenTrait; +} + +interface InitTestTokenAccV2Params { + mintIndex: number; + mintAmount?: anchor.BN; +} + +interface InitTestTickArrayRangeV2Params { + poolIndex: number; + startTickIndex: number; + arrayCount: number; + aToB: boolean; +} + +interface InitTestPositionV2Params { + poolIndex: number; + fundParams: FundedPositionV2Params[]; +} + +export interface InitAquariumV2Params { + // Single-ton per aquarium + configParams?: TestConfigParams; + + initFeeTierParams: InitTestFeeTierV2Params[]; + + initMintParams: InitTestMintV2Params[]; + + initTokenAccParams: InitTestTokenAccV2Params[]; + + initPoolParams: InitTestPoolV2Params[]; + + initTickArrayRangeParams: InitTestTickArrayRangeV2Params[]; + + initPositionParams: InitTestPositionV2Params[]; +} + +export interface TestAquarium { + configParams: TestConfigParams; + feeTierParams: InitFeeTierParams[]; + mintKeys: PublicKey[]; + tokenAccounts: { mint: PublicKey; account: PublicKey, tokenTrait: TokenTrait }[]; + pools: InitPoolV2Params[]; + tickArrays: { params: InitTestTickArrayRangeV2Params; pdas: PDA[] }[]; +} + +const DEFAULT_FEE_RATE = 3000; +const DEFAULT_MINT_AMOUNT = new anchor.BN("15000000000"); +const DEFAULT_SQRT_PRICE = MathUtil.toX64(new Decimal(5)); + +const DEFAULT_INIT_FEE_TIER = [{ tickSpacing: TickSpacing.Standard }]; +const DEFAULT_INIT_MINT: InitTestMintV2Params[] = [{ tokenTrait: {isToken2022: true} }, { tokenTrait: {isToken2022: true} }]; +const DEFAULT_INIT_TOKEN = [{ mintIndex: 0 }, { mintIndex: 1 }]; +const DEFAULT_INIT_POOL: InitTestPoolV2Params[] = [ + { mintIndices: [0, 1], tickSpacing: TickSpacing.Standard }, +]; +const DEFAULT_INIT_TICK_ARR: InitTestTickArrayRangeV2Params[] = []; +const DEFAULT_INIT_POSITION: InitTestPositionV2Params[] = []; + +export function getDefaultAquariumV2(): InitAquariumV2Params { + return { + initFeeTierParams: [...DEFAULT_INIT_FEE_TIER], + initMintParams: [...DEFAULT_INIT_MINT], + initTokenAccParams: [...DEFAULT_INIT_TOKEN], + initPoolParams: [...DEFAULT_INIT_POOL], + initTickArrayRangeParams: [...DEFAULT_INIT_TICK_ARR], + initPositionParams: [...DEFAULT_INIT_POSITION], + }; +} + +export async function buildTestAquariumsV2( + ctx: WhirlpoolContext, + initParams: InitAquariumV2Params[] +): Promise { + const aquariums = []; + // Airdrop SOL into provider wallet; + await ctx.connection.requestAirdrop(ctx.provider.wallet.publicKey, 100_000_000_000_000); + for (const initParam of initParams) { + // Create configs + let configParams = initParam.configParams; + if (!configParams) { + configParams = generateDefaultConfigParams(ctx); + } + // Could batch + await toTx( + ctx, + WhirlpoolIx.initializeConfigIx(ctx.program, configParams.configInitInfo) + ).buildAndExecute(); + + // initialize ConfigExtension + const { configExtensionInitInfo, configExtensionSetTokenBadgeAuthorityInfo, configExtensionKeypairs } = generateDefaultConfigExtensionParams( + ctx, + configParams.configInitInfo.whirlpoolsConfigKeypair.publicKey, + configParams.configKeypairs.feeAuthorityKeypair.publicKey + ); + await toTx(ctx, WhirlpoolIx.initializeConfigExtensionIx(ctx.program, configExtensionInitInfo)) + .addSigner(configParams.configKeypairs.feeAuthorityKeypair).buildAndExecute(); + await toTx(ctx, WhirlpoolIx.setTokenBadgeAuthorityIx(ctx.program, configExtensionSetTokenBadgeAuthorityInfo)) + .addSigner(configParams.configKeypairs.feeAuthorityKeypair).buildAndExecute(); + + const { + initFeeTierParams, + initMintParams, + initTokenAccParams, + initPoolParams, + initTickArrayRangeParams, + initPositionParams, + } = initParam; + + const feeTierParams: InitFeeTierParams[] = []; + for (const initFeeTierParam of initFeeTierParams) { + const { tickSpacing } = initFeeTierParam; + const feeRate = + initFeeTierParam.feeRate !== undefined ? initFeeTierParam.feeRate : DEFAULT_FEE_RATE; + const { params } = await initFeeTier( + ctx, + configParams.configInitInfo, + configParams.configKeypairs.feeAuthorityKeypair, + tickSpacing, + feeRate + ); + feeTierParams.push(params); + } + + // TODO: handle nativeMint + initMintParams.forEach((initMintParam) => { + invariant(!initMintParam.tokenTrait.isNativeMint, "Native mint not supported"); + }); + + const mintKeypairs = initMintParams + .map(() => Keypair.generate()) + .sort((a, b) => PoolUtil.compareMints(a.publicKey, b.publicKey)); + const mintKeys = ( + await Promise.all( + initMintParams.map(({ tokenTrait }, i) => createMintV2(ctx.provider, tokenTrait, undefined, mintKeypairs[i])) + ) + ); + + // create TokenBadge if needed + await Promise.all( + initMintParams.map(({ tokenTrait }, i) => { + if (isTokenBadgeRequired(tokenTrait)) { + return toTx(ctx, WhirlpoolIx.initializeTokenBadgeIx(ctx.program, { + tokenMint: mintKeys[i], + tokenBadgeAuthority: configExtensionKeypairs.tokenBadgeAuthorityKeypair.publicKey, + tokenBadgePda: PDAUtil.getTokenBadge(ctx.program.programId, configParams!.configInitInfo.whirlpoolsConfigKeypair.publicKey, mintKeys[i]), + whirlpoolsConfig: configParams!.configInitInfo.whirlpoolsConfigKeypair.publicKey, + whirlpoolsConfigExtension: configExtensionInitInfo.whirlpoolsConfigExtensionPda.publicKey, + funder: ctx.wallet.publicKey, + })).addSigner(configExtensionKeypairs.tokenBadgeAuthorityKeypair).buildAndExecute(); + } + return Promise.resolve(); + }) + ); + + const tokenAccounts = await Promise.all( + initTokenAccParams.map(async (initTokenAccParam) => { + const { mintIndex, mintAmount = DEFAULT_MINT_AMOUNT } = initTokenAccParam; + const mintKey = mintKeys[mintIndex]; + const tokenTrait = initMintParams[mintIndex].tokenTrait; + const account = await createAndMintToAssociatedTokenAccountV2( + ctx.provider, + tokenTrait, + mintKey, + mintAmount + ); + return { mint: mintKey, account, tokenTrait }; + }) + ); + + const pools = await Promise.all( + initPoolParams.map(async (initPoolParam) => { + const { + tickSpacing, + mintIndices, + initSqrtPrice = DEFAULT_SQRT_PRICE, + feeTierIndex = 0, + } = initPoolParam; + const [mintOne, mintTwo] = mintIndices.map((idx) => mintKeys[idx]); + const [tokenMintA, tokenMintB] = PoolUtil.orderMints(mintOne, mintTwo).map( + AddressUtil.toPubKey + ); + + const isInverted = mintOne.equals(tokenMintB); + invariant(!isInverted, "should not be inverted"); + + const configKey = configParams!.configInitInfo.whirlpoolsConfigKeypair.publicKey; + const whirlpoolPda = PDAUtil.getWhirlpool( + ctx.program.programId, + configKey, + tokenMintA, + tokenMintB, + tickSpacing + ); + + const tokenBadgeAPda = PDAUtil.getTokenBadge( + ctx.program.programId, + configKey, + tokenMintA, + ); + const tokenBadgeBPda = PDAUtil.getTokenBadge( + ctx.program.programId, + configKey, + tokenMintB, + ); + + const tokenProgramA = (await ctx.connection.getAccountInfo(tokenMintA))!.owner; + const tokenProgramB = (await ctx.connection.getAccountInfo(tokenMintB))!.owner; + + const poolParam: InitPoolV2Params = { + initSqrtPrice, + whirlpoolsConfig: configKey, + tokenMintA, + tokenMintB, + tokenBadgeA: tokenBadgeAPda.publicKey, + tokenBadgeB: tokenBadgeBPda.publicKey, + tokenProgramA, + tokenProgramB, + whirlpoolPda, + tokenVaultAKeypair: Keypair.generate(), + tokenVaultBKeypair: Keypair.generate(), + feeTierKey: feeTierParams[feeTierIndex].feeTierPda.publicKey, + tickSpacing, + // TODO: funder + funder: ctx.wallet.publicKey, + }; + + const tx = toTx(ctx, WhirlpoolIx.initializePoolV2Ix(ctx.program, poolParam)); + await tx.buildAndExecute(); + return poolParam; + }) + ); + + const tickArrays = await Promise.all( + initTickArrayRangeParams.map(async (initTickArrayRangeParam) => { + const { poolIndex, startTickIndex, arrayCount, aToB } = initTickArrayRangeParam; + const pool = pools[poolIndex]; + const pdas = await initTickArrayRange( + ctx, + pool.whirlpoolPda.publicKey, + startTickIndex, + arrayCount, + pool.tickSpacing, + aToB + ); + return { + params: initTickArrayRangeParam, + pdas, + }; + }) + ); + + await Promise.all( + initPositionParams.map(async (initPositionParam) => { + const { poolIndex, fundParams } = initPositionParam; + const pool = pools[poolIndex]; + const tokenAccKeys = getTokenAccsForPoolsV2([pool], tokenAccounts); + await fundPositionsV2(ctx, pool, tokenAccKeys[0], tokenAccKeys[1], fundParams); + }) + ); + + aquariums.push({ + configParams, + feeTierParams, + mintKeys, + tokenAccounts, + pools, + tickArrays, + }); + } + return aquariums; +} + +export function getTokenAccsForPoolsV2( + pools: InitPoolV2Params[], + tokenAccounts: { mint: PublicKey; account: PublicKey, tokenTrait: TokenTrait }[] +) { + const mints = []; + for (const pool of pools) { + mints.push(pool.tokenMintA); + mints.push(pool.tokenMintB); + } + return mints.map((mint) => + tokenAccounts.find((acc) => acc.mint.equals(mint))!.account + ); +} diff --git a/sdk/tests/utils/v2/confidential-transfer.ts b/sdk/tests/utils/v2/confidential-transfer.ts new file mode 100644 index 000000000..1052a95d9 --- /dev/null +++ b/sdk/tests/utils/v2/confidential-transfer.ts @@ -0,0 +1,87 @@ +// [Mar 6, 2024] ConfidentialTransfer is not supported in @solana/spl-token, so we need to build instructions manually... + +import { ExtensionType, TOKEN_2022_PROGRAM_ID, TokenInstruction, TokenUnsupportedInstructionError, getExtensionTypes, getMint, programSupportsExtensions } from "@solana/spl-token"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { struct, u8 } from '@solana/buffer-layout'; +import { publicKey } from '@solana/buffer-layout-utils'; +import { AnchorProvider } from "@coral-xyz/anchor"; +import { TEST_TOKEN_2022_PROGRAM_ID } from "../test-consts"; + +enum ConfidentialTransferInstruction { + // We are interested in initilization only + InitializeMint = 0, + // ... + // https://github.com/solana-labs/solana-program-library/blob/master/token/program-2022/src/extension/confidential_transfer/instruction.rs +} + +interface InitializeConfidentialTransferMintInstructionData { + instruction: TokenInstruction.ConfidentialTransferExtension; + confidentialTransferInstruction: ConfidentialTransferInstruction.InitializeMint; + authority: PublicKey | null; + autoApproveNewAccounts: boolean; + auditorElgamalPubkey: PublicKey | null; +} + +/* + +Sample transaction instruction data + +1b 00 0c 8e 98 78 4f 83 30 4f 46 14 80 d7 86 b4  +7b da 04 59 14 d2 21 b4 ac 77 74 02 97 af b6 71  +53 35 01 00 00 00 00 00 00 00 00 00 00 00 00 00  +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  +00 00 00  + +data length: 67 bytes + + 1: confidential transfer prefix + 1: confidential transfer ix + 32: authority + 1: auto approve + 32: elgamal + +*/ +const initializeConfidentialTransferMintInstructionData = struct([ + u8('instruction'), + u8('confidentialTransferInstruction'), + publicKey('authority'), + u8('autoApproveNewAccounts'), + publicKey('auditorElgamalPubkey'), +]); + +export function createInitializeConfidentialTransferMintInstruction( + mint: PublicKey, + authority: PublicKey, + autoApproveNewAccounts: boolean = true, + auditorElgamalPubkey: PublicKey = PublicKey.default, + programId: PublicKey = TOKEN_2022_PROGRAM_ID, +) { + if (!programSupportsExtensions(programId)) { + throw new TokenUnsupportedInstructionError(); + } + + const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; + const data = Buffer.alloc(initializeConfidentialTransferMintInstructionData.span); + initializeConfidentialTransferMintInstructionData.encode( + { + instruction: TokenInstruction.ConfidentialTransferExtension, + confidentialTransferInstruction: ConfidentialTransferInstruction.InitializeMint, + authority, + auditorElgamalPubkey, + autoApproveNewAccounts, + }, + data + ); + + return new TransactionInstruction({ keys, programId, data }); +} + +export async function hasConfidentialTransferMintExtension( + provider: AnchorProvider, + mint: PublicKey, +): Promise { + const account = await getMint(provider.connection, mint, "confirmed", TEST_TOKEN_2022_PROGRAM_ID); + + const extensions = getExtensionTypes(account.tlvData); + return extensions.includes(ExtensionType.ConfidentialTransferMint); +} diff --git a/sdk/tests/utils/v2/fixture-v2.ts b/sdk/tests/utils/v2/fixture-v2.ts new file mode 100644 index 000000000..c37c25e58 --- /dev/null +++ b/sdk/tests/utils/v2/fixture-v2.ts @@ -0,0 +1,174 @@ +import { BN } from "@coral-xyz/anchor"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import { TickSpacing, ZERO_BN } from ".."; +import { InitConfigParams, InitPoolV2Params, TickUtil, WhirlpoolContext } from "../../../src"; +import { + initTickArray, +} from "../init-utils"; +import { + initRewardAndSetEmissionsV2, + initTestPoolWithTokensV2, + FundedPositionV2Info, + FundedPositionV2Params, + fundPositionsV2, + TokenTrait, +} from "./init-utils-v2"; + + +interface InitFixtureV2Params { + tokenTraitA: TokenTrait; + tokenTraitB: TokenTrait; + tickSpacing: number; + initialSqrtPrice?: BN; + mintAmount?: BN; + positions?: FundedPositionV2Params[]; + rewards?: RewardV2Param[]; +} + +interface RewardV2Param { + rewardTokenTrait: TokenTrait; + emissionsPerSecondX64: BN; + vaultAmount: BN; +} + +interface InitializedRewardV2Info { + rewardMint: PublicKey; + rewardVaultKeypair: Keypair; + tokenProgram: PublicKey; +} + +export class WhirlpoolTestFixtureV2 { + private ctx: WhirlpoolContext; + private poolInitInfo: InitPoolV2Params = defaultPoolInitInfoV2; + private configInitInfo: InitConfigParams = defaultConfigInitInfoV2; + private configKeypairs = defaultConfigKeypairsV2; + private positions: FundedPositionV2Info[] = []; + private rewards: InitializedRewardV2Info[] = []; + private tokenAccountA = PublicKey.default; + private tokenAccountB = PublicKey.default; + private initialized = false; + + constructor(ctx: WhirlpoolContext) { + this.ctx = ctx; + } + + async init(params: InitFixtureV2Params): Promise { + const { tickSpacing, initialSqrtPrice, positions, rewards, tokenTraitA, tokenTraitB, mintAmount } = params; + + const { poolInitInfo, configInitInfo, configKeypairs, configExtension, tokenAccountA, tokenAccountB } = + await initTestPoolWithTokensV2( + this.ctx, + tokenTraitA, + tokenTraitB, + tickSpacing, + initialSqrtPrice, + mintAmount, + ); + + this.poolInitInfo = poolInitInfo; + this.configInitInfo = configInitInfo; + this.configKeypairs = configKeypairs; + this.tokenAccountA = tokenAccountA; + this.tokenAccountB = tokenAccountB; + + if (positions) { + await initTickArraysV2(this.ctx, poolInitInfo, positions); + + this.positions = await fundPositionsV2( + this.ctx, + poolInitInfo, + tokenAccountA, + tokenAccountB, + positions + ); + } + + if (rewards) { + const initRewards: InitializedRewardV2Info[] = []; + for (let i = 0; i < rewards.length; i++) { + // Iterate because we enforce sequential initialization on the smart contract + initRewards.push( + await initRewardAndSetEmissionsV2( + this.ctx, + rewards[i].rewardTokenTrait, + configKeypairs.rewardEmissionsSuperAuthorityKeypair, + poolInitInfo.whirlpoolsConfig, + poolInitInfo.whirlpoolPda.publicKey, + i, + rewards[i].vaultAmount, + rewards[i].emissionsPerSecondX64, + configExtension.configExtensionKeypairs.tokenBadgeAuthorityKeypair, + ) + ); + } + this.rewards = initRewards; + } + this.initialized = true; + return this; + } + + getInfos() { + if (!this.initialized) { + throw new Error("Test fixture is not initialized"); + } + return { + poolInitInfo: this.poolInitInfo, + configInitInfo: this.configInitInfo, + configKeypairs: this.configKeypairs, + tokenAccountA: this.tokenAccountA, + tokenAccountB: this.tokenAccountB, + positions: this.positions, + rewards: this.rewards, + }; + } +} + +async function initTickArraysV2( + ctx: WhirlpoolContext, + poolInitInfo: InitPoolV2Params, + positions: FundedPositionV2Params[] +) { + const startTickSet = new Set(); + positions.forEach((p) => { + startTickSet.add(TickUtil.getStartTickIndex(p.tickLowerIndex, poolInitInfo.tickSpacing)); + startTickSet.add(TickUtil.getStartTickIndex(p.tickUpperIndex, poolInitInfo.tickSpacing)); + }); + + return Promise.all( + Array.from(startTickSet).map((startTick) => + initTickArray(ctx, poolInitInfo.whirlpoolPda.publicKey, startTick) + ) + ); +} + +const defaultPoolInitInfoV2: InitPoolV2Params = { + initSqrtPrice: ZERO_BN, + whirlpoolsConfig: PublicKey.default, + tokenProgramA: PublicKey.default, + tokenProgramB: PublicKey.default, + tokenMintA: PublicKey.default, + tokenMintB: PublicKey.default, + tokenBadgeA: PublicKey.default, + tokenBadgeB: PublicKey.default, + whirlpoolPda: { publicKey: PublicKey.default, bump: 0 }, + tokenVaultAKeypair: Keypair.generate(), + tokenVaultBKeypair: Keypair.generate(), + tickSpacing: TickSpacing.Standard, + feeTierKey: PublicKey.default, + funder: PublicKey.default, +}; + +const defaultConfigInitInfoV2 = { + whirlpoolsConfigKeypair: Keypair.generate(), + feeAuthority: PublicKey.default, + collectProtocolFeesAuthority: PublicKey.default, + rewardEmissionsSuperAuthority: PublicKey.default, + defaultProtocolFeeRate: 0, + funder: PublicKey.default, +}; + +const defaultConfigKeypairsV2 = { + feeAuthorityKeypair: Keypair.generate(), + collectProtocolFeesAuthorityKeypair: Keypair.generate(), + rewardEmissionsSuperAuthorityKeypair: Keypair.generate(), +}; diff --git a/sdk/tests/utils/v2/init-utils-v2.ts b/sdk/tests/utils/v2/init-utils-v2.ts new file mode 100644 index 000000000..fa33d4464 --- /dev/null +++ b/sdk/tests/utils/v2/init-utils-v2.ts @@ -0,0 +1,781 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Instruction, MathUtil, PDA } from "@orca-so/common-sdk"; +import { ComputeBudgetProgram, Keypair, PublicKey, TransactionInstruction } from "@solana/web3.js"; +import BN from "bn.js"; +import Decimal from "decimal.js"; +import { + TEST_TOKEN_2022_PROGRAM_ID, + TEST_TOKEN_PROGRAM_ID, + TickSpacing, + ZERO_BN, +} from ".."; +import { + InitConfigParams, + InitPoolV2Params, + InitializeRewardV2Params, + OpenPositionParams, + PDAUtil, + PriceMath, + TICK_ARRAY_SIZE, + TickUtil, + WhirlpoolContext, + WhirlpoolIx, + toTx +} from "../../../src"; +import { PoolUtil } from "../../../src/utils/public/pool-utils"; +import { + TestWhirlpoolsConfigKeypairs, + generateDefaultConfigParams, +} from "../test-builders"; +import { + initFeeTier, + openPosition, + initTickArrayRange, +} from "../init-utils"; +import { + calculateTransferFeeIncludedAmount, + createAndMintToAssociatedTokenAccountV2, + createInOrderMintsV2, + createMintV2, + mintToDestinationV2 +} from "./token-2022"; +import { InitConfigExtensionParams, SetTokenBadgeAuthorityParams } from "../../../src/instructions"; +import { getExtraAccountMetasForTestTransferHookProgram } from "./test-transfer-hook-program"; +import { AccountState, getEpochFee, getMint, getTransferFeeConfig } from "@solana/spl-token"; + + +export interface TokenTrait { + isToken2022: boolean; + isNativeMint?: boolean; + hasFreezeAuthority?: boolean; + hasPermanentDelegate?: boolean; + hasTransferFeeExtension?: boolean; + transferFeeInitialBps?: number; + transferFeeInitialMax?: bigint; // u64 + hasTransferHookExtension?: boolean; + hasConfidentialTransferExtension?: boolean; + + hasInterestBearingExtension?: boolean; + hasMintCloseAuthorityExtension?: boolean; + hasDefaultAccountStateExtension?: boolean; + defaultAccountInitialState?: AccountState; + hasNonTransferableExtension?: boolean; + hasTokenMetadataExtension?: boolean; + hasMetadataPointerExtension?: boolean; + hasGroupExtension?: boolean; + hasGroupPointerExtension?: boolean; + hasGroupMemberExtension?: boolean; + hasGroupMemberPointerExtension?: boolean; +} + +interface TestPoolV2Params { + configInitInfo: InitConfigParams; + configKeypairs: TestWhirlpoolsConfigKeypairs; + poolInitInfo: InitPoolV2Params; + feeTierParams: any; + configExtension: TestConfigExtensionParams; +} + +interface InitTestPoolParams { + mintIndices: [number, number]; + tickSpacing: number; + feeTierIndex?: number; + initSqrtPrice?: anchor.BN; +} + +interface InitTestTickArrayRangeParams { + poolIndex: number; + startTickIndex: number; + arrayCount: number; + aToB: boolean; +} + +interface InitTestPositionParams { + poolIndex: number; + fundParams: FundedPositionV2Params[]; +} + +export type FundedPositionV2Params = { + tickLowerIndex: number; + tickUpperIndex: number; + liquidityAmount: anchor.BN; +}; + +export interface FundedPositionV2Info { + initParams: OpenPositionParams; + publicKey: PublicKey; + tokenAccount: PublicKey; + mintKeypair: Keypair; + tickArrayLower: PublicKey; + tickArrayUpper: PublicKey; +} + + +const DEFAULT_FEE_RATE = 3000; +const DEFAULT_MINT_AMOUNT = new anchor.BN("15000000000"); +const DEFAULT_SQRT_PRICE = MathUtil.toX64(new Decimal(5)); + +const DEFAULT_INIT_FEE_TIER = [{ tickSpacing: TickSpacing.Standard }]; +const DEFAULT_INIT_MINT = [{}, {}]; +const DEFAULT_INIT_TOKEN = [{ mintIndex: 0 }, { mintIndex: 1 }]; +const DEFAULT_INIT_POOL: InitTestPoolParams[] = [ + { mintIndices: [0, 1], tickSpacing: TickSpacing.Standard }, +]; +const DEFAULT_INIT_TICK_ARR: InitTestTickArrayRangeParams[] = []; +const DEFAULT_INIT_POSITION: InitTestPositionParams[] = []; + +/* + +export function getTokenAccsForPools( + pools: InitPoolParams[], + tokenAccounts: { mint: PublicKey; account: PublicKey }[] +) { + const mints = []; + for (const pool of pools) { + mints.push(pool.tokenMintA); + mints.push(pool.tokenMintB); + } + return mints.map((mint) => + tokenAccounts.find((acc) => acc.mint.equals(mint))!.account + ); +} +*/ + + +export async function initTestPoolWithTokensV2( + ctx: WhirlpoolContext, + tokenTraitA: TokenTrait, + tokenTraitB: TokenTrait, + tickSpacing: number, + initSqrtPrice = DEFAULT_SQRT_PRICE, + mintAmount = new anchor.BN("15000000000"), +) { + const provider = ctx.provider; + + const { poolInitInfo, configInitInfo, configKeypairs, feeTierParams, configExtension } = await initTestPoolV2( + ctx, + tokenTraitA, + tokenTraitB, + tickSpacing, + initSqrtPrice, + undefined, + ); + + const { tokenMintA, tokenMintB, whirlpoolPda } = poolInitInfo; + + // Airdrop SOL into provider's wallet for SOL native token testing. + const connection = ctx.provider.connection; + const airdropTx = await connection.requestAirdrop( + ctx.provider.wallet.publicKey, + 100_000_000_000_000 + ); + await ctx.connection.confirmTransaction({ + signature: airdropTx, + ...(await ctx.connection.getLatestBlockhash("confirmed")), + }, "confirmed"); + + const tokenAccountA = await createAndMintToAssociatedTokenAccountV2( + provider, + tokenTraitA, + tokenMintA, + mintAmount + ); + + const tokenAccountB = await createAndMintToAssociatedTokenAccountV2( + provider, + tokenTraitB, + tokenMintB, + mintAmount + ); + + return { + poolInitInfo, + configInitInfo, + configKeypairs, + feeTierParams, + configExtension, + whirlpoolPda, + tokenAccountA, + tokenAccountB, + }; +} + +export async function initTestPoolWithLiquidityV2( + ctx: WhirlpoolContext, + tokenTraitA: TokenTrait, + tokenTraitB: TokenTrait, + initSqrtPrice = DEFAULT_SQRT_PRICE, + mintAmount = new anchor.BN("15000000000"), +) { + const { + poolInitInfo, + configInitInfo, + configKeypairs, + feeTierParams, + whirlpoolPda, + tokenAccountA, + tokenAccountB, + } = await initTestPoolWithTokensV2( + ctx, + tokenTraitA, + tokenTraitB, + TickSpacing.Standard, + initSqrtPrice, + mintAmount, + ); + + const tickArrays = await initTickArrayRange( + ctx, + whirlpoolPda.publicKey, + 22528, // to 33792 + 3, + TickSpacing.Standard, + false + ); + + const fundParams: FundedPositionV2Params[] = [ + { + liquidityAmount: new anchor.BN(100_000), + tickLowerIndex: 27904, + tickUpperIndex: 33408, + }, + ]; + + const positionInfos = await fundPositionsV2( + ctx, + poolInitInfo, + tokenAccountA, + tokenAccountB, + fundParams + ); + + return { + poolInitInfo, + configInitInfo, + configKeypairs, + positionInfo: positionInfos[0].initParams, + tokenAccountA, + tokenAccountB, + tickArrays, + feeTierParams, + }; +} + +export async function initTestPoolV2( + ctx: WhirlpoolContext, + tokenTraitA: TokenTrait, + tokenTraitB: TokenTrait, + tickSpacing: number, + initSqrtPrice = DEFAULT_SQRT_PRICE, + funder?: Keypair, +) { + const poolParams = await buildTestPoolV2Params( + ctx, + tokenTraitA, + tokenTraitB, + tickSpacing, + 3000, + initSqrtPrice, + funder?.publicKey, + ); + + return initTestPoolFromParamsV2(ctx, poolParams, funder); +} + +export async function buildTestPoolV2Params( + ctx: WhirlpoolContext, + tokenTraitA: TokenTrait, + tokenTraitB: TokenTrait, + tickSpacing: number, + defaultFeeRate = 3000, + initSqrtPrice = DEFAULT_SQRT_PRICE, + funder?: PublicKey, + createTokenBadgeIfNeededA: boolean = true, + createTokenBadgeIfNeededB: boolean = true, +): Promise { + const { configInitInfo, configKeypairs } = generateDefaultConfigParams(ctx); + const { configExtensionInitInfo, configExtensionSetTokenBadgeAuthorityInfo, configExtensionKeypairs } = generateDefaultConfigExtensionParams(ctx, configInitInfo.whirlpoolsConfigKeypair.publicKey, configKeypairs.feeAuthorityKeypair.publicKey); + + await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute(); + await toTx(ctx, WhirlpoolIx.initializeConfigExtensionIx(ctx.program, configExtensionInitInfo)) + .addSigner(configKeypairs.feeAuthorityKeypair).buildAndExecute(); + await toTx(ctx, WhirlpoolIx.setTokenBadgeAuthorityIx(ctx.program, configExtensionSetTokenBadgeAuthorityInfo)) + .addSigner(configKeypairs.feeAuthorityKeypair).buildAndExecute(); + + const { params: feeTierParams } = await initFeeTier( + ctx, + configInitInfo, + configKeypairs.feeAuthorityKeypair, + tickSpacing, + defaultFeeRate + ); + const poolInitInfo = await generateDefaultInitPoolV2Params( + ctx, + configInitInfo.whirlpoolsConfigKeypair.publicKey, + feeTierParams.feeTierPda.publicKey, + tokenTraitA, + tokenTraitB, + tickSpacing, + initSqrtPrice, + funder, + ); + + if (isTokenBadgeRequired(tokenTraitA) && createTokenBadgeIfNeededA) { + await toTx(ctx, WhirlpoolIx.initializeTokenBadgeIx(ctx.program, { + tokenMint: poolInitInfo.tokenMintA, + tokenBadgeAuthority: configExtensionKeypairs.tokenBadgeAuthorityKeypair.publicKey, + tokenBadgePda: { publicKey: poolInitInfo.tokenBadgeA, bump: 0/* dummy */ }, + whirlpoolsConfig: poolInitInfo.whirlpoolsConfig, + whirlpoolsConfigExtension: configExtensionInitInfo.whirlpoolsConfigExtensionPda.publicKey, + funder: funder ?? ctx.wallet.publicKey, + })).addSigner(configExtensionKeypairs.tokenBadgeAuthorityKeypair).buildAndExecute(); + } + + if (isTokenBadgeRequired(tokenTraitB) && createTokenBadgeIfNeededB) { + await toTx(ctx, WhirlpoolIx.initializeTokenBadgeIx(ctx.program, { + tokenMint: poolInitInfo.tokenMintB, + tokenBadgeAuthority: configExtensionKeypairs.tokenBadgeAuthorityKeypair.publicKey, + tokenBadgePda: { publicKey: poolInitInfo.tokenBadgeB, bump: 0/* dummy */ }, + whirlpoolsConfig: poolInitInfo.whirlpoolsConfig, + whirlpoolsConfigExtension: configExtensionInitInfo.whirlpoolsConfigExtensionPda.publicKey, + funder: funder ?? ctx.wallet.publicKey, + })).addSigner(configExtensionKeypairs.tokenBadgeAuthorityKeypair).buildAndExecute(); + } + + return { + configInitInfo, + configKeypairs, + poolInitInfo, + feeTierParams, + configExtension: { + configExtensionInitInfo, + configExtensionSetTokenBadgeAuthorityInfo, + configExtensionKeypairs, + }, + }; +} + +export async function initializeRewardV2( + ctx: WhirlpoolContext, + tokenTrait: TokenTrait, + whirlpoolsConfig: PublicKey, + rewardAuthorityKeypair: anchor.web3.Keypair, + whirlpool: PublicKey, + rewardIndex: number, + tokenBadgeAuthorityKeypair: anchor.web3.Keypair, + funder?: Keypair +): Promise<{ txId: string; params: InitializeRewardV2Params }> { + const provider = ctx.provider; + const rewardMint = await createMintV2(provider, tokenTrait); + const rewardVaultKeypair = anchor.web3.Keypair.generate(); + + const rewardTokenBadgePda = PDAUtil.getTokenBadge( + ctx.program.programId, + whirlpoolsConfig, + rewardMint + ); + + if (isTokenBadgeRequired(tokenTrait)) { + const configExtensionPda = PDAUtil.getConfigExtension( + ctx.program.programId, + whirlpoolsConfig, + ); + await toTx(ctx, WhirlpoolIx.initializeTokenBadgeIx(ctx.program, { + tokenMint: rewardMint, + tokenBadgeAuthority: tokenBadgeAuthorityKeypair.publicKey, + tokenBadgePda: rewardTokenBadgePda, + whirlpoolsConfig: whirlpoolsConfig, + whirlpoolsConfigExtension: configExtensionPda.publicKey, + funder: funder?.publicKey ?? ctx.wallet.publicKey, + })).addSigner(tokenBadgeAuthorityKeypair).buildAndExecute(); + } + + const tokenProgram = tokenTrait.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID; + + const params: InitializeRewardV2Params = { + rewardAuthority: rewardAuthorityKeypair.publicKey, + funder: funder?.publicKey || ctx.wallet.publicKey, + whirlpool, + rewardMint, + rewardTokenBadge: rewardTokenBadgePda.publicKey, + rewardVaultKeypair, + rewardIndex, + rewardTokenProgram: tokenProgram, + }; + + const tx = toTx(ctx, WhirlpoolIx.initializeRewardV2Ix(ctx.program, params)).addSigner( + rewardAuthorityKeypair + ); + if (funder) { + tx.addSigner(funder); + } + + return { + txId: await tx.buildAndExecute(), + params, + }; +} + +export async function initRewardAndSetEmissionsV2( + ctx: WhirlpoolContext, + tokenTrait: TokenTrait, + rewardAuthorityKeypair: anchor.web3.Keypair, + whirlpoolsConfig: PublicKey, + whirlpool: PublicKey, + rewardIndex: number, + vaultAmount: BN | number, + emissionsPerSecondX64: anchor.BN, + tokenBadgeAuthorityKeypair: anchor.web3.Keypair, + funder?: Keypair +) { + const { + params: { rewardMint, rewardVaultKeypair, rewardTokenProgram }, + } = await initializeRewardV2(ctx, tokenTrait, whirlpoolsConfig, rewardAuthorityKeypair, whirlpool, rewardIndex, tokenBadgeAuthorityKeypair, funder); + + await mintToDestinationV2(ctx.provider, tokenTrait, rewardMint, rewardVaultKeypair.publicKey, vaultAmount); + + await toTx( + ctx, + WhirlpoolIx.setRewardEmissionsV2Ix(ctx.program, { + rewardAuthority: rewardAuthorityKeypair.publicKey, + whirlpool, + rewardIndex, + rewardVaultKey: rewardVaultKeypair.publicKey, + emissionsPerSecondX64, + }) + ) + .addSigner(rewardAuthorityKeypair) + .buildAndExecute(); + return { rewardMint, rewardVaultKeypair, tokenProgram: rewardTokenProgram }; +} + +//////////////////////////////////////////////////////////////////////////////// +// private +//////////////////////////////////////////////////////////////////////////////// +async function generateDefaultInitPoolV2Params( + context: WhirlpoolContext, + configKey: PublicKey, + feeTierKey: PublicKey, + tokenTraitA: TokenTrait, + tokenTraitB: TokenTrait, + tickSpacing: number, + initSqrtPrice = MathUtil.toX64(new Decimal(5)), + funder?: PublicKey, +): Promise { + const [tokenAMintPubKey, tokenBMintPubKey] = await createInOrderMintsV2(context.provider, tokenTraitA, tokenTraitB); + + const whirlpoolPda = PDAUtil.getWhirlpool( + context.program.programId, + configKey, + tokenAMintPubKey, + tokenBMintPubKey, + tickSpacing + ); + + const tokenBadgeAPda = PDAUtil.getTokenBadge(context.program.programId, configKey, tokenAMintPubKey); + const tokenBadgeBPda = PDAUtil.getTokenBadge(context.program.programId, configKey, tokenBMintPubKey); + + return { + initSqrtPrice, + whirlpoolsConfig: configKey, + tokenMintA: tokenAMintPubKey, + tokenMintB: tokenBMintPubKey, + tokenBadgeA: tokenBadgeAPda.publicKey, + tokenBadgeB: tokenBadgeBPda.publicKey, + tokenProgramA: tokenTraitA.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID, + tokenProgramB: tokenTraitB.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID, + whirlpoolPda, + tokenVaultAKeypair: Keypair.generate(), + tokenVaultBKeypair: Keypair.generate(), + feeTierKey, + tickSpacing, + funder: funder || context.wallet.publicKey, + }; +}; + +async function initTestPoolFromParamsV2( + ctx: WhirlpoolContext, + poolParams: TestPoolV2Params, + funder?: Keypair +) { + const { configInitInfo, poolInitInfo, configKeypairs, feeTierParams, configExtension } = poolParams; + const tx = toTx(ctx, WhirlpoolIx.initializePoolV2Ix(ctx.program, poolInitInfo)); + if (funder) { + tx.addSigner(funder); + } + + return { + txId: await tx.buildAndExecute(), + configInitInfo, + configKeypairs, + poolInitInfo, + feeTierParams, + configExtension, + }; +} + + +//////////////////////////////////////////////////////////////////////////////// +// position related +//////////////////////////////////////////////////////////////////////////////// + +export async function withdrawPositionsV2( + ctx: WhirlpoolContext, + tokenTraitA: TokenTrait, + tokenTraitB: TokenTrait, + positionInfos: FundedPositionV2Info[], + tokenOwnerAccountA: PublicKey, + tokenOwnerAccountB: PublicKey +) { + const fetcher = ctx.fetcher; + + const tokenProgramA = tokenTraitA.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID; + const tokenProgramB = tokenTraitB.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID; + + await Promise.all( + positionInfos.map(async (info) => { + const pool = await fetcher.getPool(info.initParams.whirlpool); + const position = await fetcher.getPosition(info.initParams.positionPda.publicKey); + + if (!pool) { + throw new Error(`Failed to fetch pool - ${info.initParams.whirlpool}`); + } + + if (!position) { + throw new Error(`Failed to fetch position - ${info.initParams.whirlpool}`); + } + + const priceLower = PriceMath.tickIndexToSqrtPriceX64(position.tickLowerIndex); + const priceUpper = PriceMath.tickIndexToSqrtPriceX64(position.tickUpperIndex); + + const { tokenA, tokenB } = PoolUtil.getTokenAmountsFromLiquidity( + position.liquidity, + pool.sqrtPrice, + priceLower, + priceUpper, + false + ); + + const numTicksInTickArray = pool.tickSpacing * TICK_ARRAY_SIZE; + const lowerStartTick = + position.tickLowerIndex - (position.tickLowerIndex % numTicksInTickArray); + const tickArrayLower = PDAUtil.getTickArray( + ctx.program.programId, + info.initParams.whirlpool, + lowerStartTick + ); + const upperStartTick = + position.tickUpperIndex - (position.tickUpperIndex % numTicksInTickArray); + const tickArrayUpper = PDAUtil.getTickArray( + ctx.program.programId, + info.initParams.whirlpool, + upperStartTick + ); + + await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + liquidityAmount: position.liquidity, + tokenMinA: tokenA, + tokenMinB: tokenB, + whirlpool: info.initParams.whirlpool, + positionAuthority: ctx.provider.wallet.publicKey, + position: info.initParams.positionPda.publicKey, + positionTokenAccount: info.initParams.positionTokenAccount, + tokenMintA: pool.tokenMintA, + tokenMintB: pool.tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA: pool.tokenVaultA, + tokenVaultB: pool.tokenVaultB, + tickArrayLower: tickArrayLower.publicKey, + tickArrayUpper: tickArrayUpper.publicKey, + }) + ).buildAndExecute(); + + await toTx( + ctx, + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + whirlpool: info.initParams.whirlpool, + positionAuthority: ctx.provider.wallet.publicKey, + position: info.initParams.positionPda.publicKey, + positionTokenAccount: info.initParams.positionTokenAccount, + tokenMintA: pool.tokenMintA, + tokenMintB: pool.tokenMintB, + tokenProgramA, + tokenProgramB, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA: pool.tokenVaultA, + tokenVaultB: pool.tokenVaultB, + }) + ).buildAndExecute(); + }) + ); +} + + + +export async function fundPositionsV2( + ctx: WhirlpoolContext, + poolInitInfo: InitPoolV2Params, + tokenAccountA: PublicKey, + tokenAccountB: PublicKey, + fundParams: FundedPositionV2Params[] +): Promise { + const { + whirlpoolPda: { publicKey: whirlpool }, + tickSpacing, + tokenVaultAKeypair, + tokenVaultBKeypair, + initSqrtPrice, + } = poolInitInfo; + + const mintA = await getMint(ctx.provider.connection, poolInitInfo.tokenMintA, "confirmed", poolInitInfo.tokenProgramA); + const mintB = await getMint(ctx.provider.connection, poolInitInfo.tokenMintB, "confirmed", poolInitInfo.tokenProgramB); + const feeConfigA = getTransferFeeConfig(mintA); + const feeConfigB = getTransferFeeConfig(mintB); + const epoch = await ctx.provider.connection.getEpochInfo("confirmed"); + + return await Promise.all( + fundParams.map(async (param): Promise => { + const { params: positionInfo, mint } = await openPosition( + ctx, + whirlpool, + param.tickLowerIndex, + param.tickUpperIndex + ); + + const tickArrayLower = PDAUtil.getTickArray( + ctx.program.programId, + whirlpool, + TickUtil.getStartTickIndex(param.tickLowerIndex, tickSpacing) + ).publicKey; + + const tickArrayUpper = PDAUtil.getTickArray( + ctx.program.programId, + whirlpool, + TickUtil.getStartTickIndex(param.tickUpperIndex, tickSpacing) + ).publicKey; + + if (param.liquidityAmount.gt(ZERO_BN)) { + const { tokenA, tokenB } = PoolUtil.getTokenAmountsFromLiquidity( + param.liquidityAmount, + initSqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(param.tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(param.tickUpperIndex), + true + ); + + // transfer fee + const transferFeeA = !feeConfigA + ? ZERO_BN + : calculateTransferFeeIncludedAmount(getEpochFee(feeConfigA, BigInt(epoch.epoch)), tokenA).fee; + const transferFeeB = !feeConfigB + ? ZERO_BN + : calculateTransferFeeIncludedAmount(getEpochFee(feeConfigB, BigInt(epoch.epoch)), tokenB).fee; + + //console.log("transfer feeA", transferFeeA.toString(), "/", tokenA.toString()); + //console.log("transfer feeB", transferFeeB.toString(), "/", tokenB.toString()); + + // transfer hook + const tokenTransferHookAccountsA = await getExtraAccountMetasForTestTransferHookProgram(ctx.provider, poolInitInfo.tokenMintA); + const tokenTransferHookAccountsB = await getExtraAccountMetasForTestTransferHookProgram(ctx.provider, poolInitInfo.tokenMintB); + + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + liquidityAmount: param.liquidityAmount, + tokenMaxA: tokenA.add(transferFeeA), + tokenMaxB: tokenB.add(transferFeeB), + whirlpool: whirlpool, + positionAuthority: ctx.provider.wallet.publicKey, + position: positionInfo.positionPda.publicKey, + positionTokenAccount: positionInfo.positionTokenAccount, + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + tokenOwnerAccountA: tokenAccountA, + tokenOwnerAccountB: tokenAccountB, + tokenVaultA: tokenVaultAKeypair.publicKey, + tokenVaultB: tokenVaultBKeypair.publicKey, + tokenProgramA: poolInitInfo.tokenProgramA, + tokenProgramB: poolInitInfo.tokenProgramB, + tickArrayLower, + tickArrayUpper, + tokenTransferHookAccountsA, + tokenTransferHookAccountsB, + }) + ).prependInstruction(useMaxCU()).buildAndExecute(); + } + return { + initParams: positionInfo, + publicKey: positionInfo.positionPda.publicKey, + tokenAccount: positionInfo.positionTokenAccount, + mintKeypair: mint, + tickArrayLower, + tickArrayUpper, + }; + }) + ); +} + +export interface TestWhirlpoolsConfigExtensionKeypairs { + tokenBadgeAuthorityKeypair: Keypair; +} + +export interface TestConfigExtensionParams { + configExtensionInitInfo: InitConfigExtensionParams; + configExtensionSetTokenBadgeAuthorityInfo: SetTokenBadgeAuthorityParams; + configExtensionKeypairs: TestWhirlpoolsConfigExtensionKeypairs; +} + +export const generateDefaultConfigExtensionParams = ( + context: WhirlpoolContext, + whirlpoolsConfig: PublicKey, + feeAuthority: PublicKey, + funder?: PublicKey +): TestConfigExtensionParams => { + const configExtensionKeypairs: TestWhirlpoolsConfigExtensionKeypairs = { + tokenBadgeAuthorityKeypair: Keypair.generate(), + }; + const configExtensionInitInfo: InitConfigExtensionParams = { + whirlpoolsConfig, + feeAuthority, + whirlpoolsConfigExtensionPda: PDAUtil.getConfigExtension(context.program.programId, whirlpoolsConfig), + funder: funder || context.wallet.publicKey, + }; + const configExtensionSetTokenBadgeAuthorityInfo: SetTokenBadgeAuthorityParams = { + whirlpoolsConfig, + whirlpoolsConfigExtension: configExtensionInitInfo.whirlpoolsConfigExtensionPda.publicKey, + configExtensionAuthority: feeAuthority, + newTokenBadgeAuthority: configExtensionKeypairs.tokenBadgeAuthorityKeypair.publicKey, + }; + return { configExtensionInitInfo, configExtensionKeypairs, configExtensionSetTokenBadgeAuthorityInfo }; +}; + +export function isTokenBadgeRequired( + tokenTrait: TokenTrait +): boolean { + if (tokenTrait.hasFreezeAuthority) return true; + if (tokenTrait.hasPermanentDelegate) return true; + if (tokenTrait.hasTransferHookExtension) return true; + return false; +} + +export function useCU(cu: number): Instruction { + return { + cleanupInstructions: [], + signers: [], + instructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ + units: cu, + }) + ] + }; +} + +export function useMaxCU(): Instruction { + return useCU(1_400_000); +} diff --git a/sdk/tests/utils/v2/swap-test-utils-v2.ts b/sdk/tests/utils/v2/swap-test-utils-v2.ts new file mode 100644 index 000000000..5666b89e5 --- /dev/null +++ b/sdk/tests/utils/v2/swap-test-utils-v2.ts @@ -0,0 +1,85 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Percentage } from "@orca-so/common-sdk"; +import { NATIVE_MINT } from "@solana/spl-token"; +import { PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; +import { TickSpacing } from ".."; +import { PoolUtil, PriceMath, TICK_ARRAY_SIZE, Whirlpool, WhirlpoolClient, WhirlpoolContext } from "../../../src"; +import { IGNORE_CACHE } from "../../../src/network/public/fetcher"; +import { + FundedPositionV2Params, + TokenTrait, + initTestPoolWithTokensV2, + fundPositionsV2, +} from "./init-utils-v2"; + +export interface SwapTestPoolParams { + ctx: WhirlpoolContext; + client: WhirlpoolClient; + tokenTraitA: TokenTrait, + tokenTraitB: TokenTrait, + tickSpacing: TickSpacing; + initSqrtPrice: anchor.BN; + initArrayStartTicks: number[]; + fundedPositions: FundedPositionV2Params[]; + tokenMintAmount?: anchor.BN; +} + +export interface SwapTestSwapParams { + swapAmount: BN; + aToB: boolean; + amountSpecifiedIsInput: boolean; + slippageTolerance: Percentage; + tickArrayAddresses: PublicKey[]; +} + +export interface SwapTestSetup { + whirlpool: Whirlpool; + tickArrayAddresses: PublicKey[]; +} + +export async function setupSwapTestV2(setup: SwapTestPoolParams) { + const { whirlpoolPda } = await initTestPoolWithTokensV2( + setup.ctx, + setup.tokenTraitA, + setup.tokenTraitB, + setup.tickSpacing, + setup.initSqrtPrice, + setup.tokenMintAmount, + ); + + const whirlpool = await setup.client.getPool(whirlpoolPda.publicKey, IGNORE_CACHE); + + await (await whirlpool.initTickArrayForTicks(setup.initArrayStartTicks))?.buildAndExecute(); + + await fundPositionsWithClient(setup.client, whirlpoolPda.publicKey, setup.fundedPositions); + + return whirlpool; +} + +export async function fundPositionsWithClient( + client: WhirlpoolClient, + whirlpoolKey: PublicKey, + fundParams: FundedPositionV2Params[] +) { + const whirlpool = await client.getPool(whirlpoolKey, IGNORE_CACHE); + const whirlpoolData = whirlpool.getData(); + await Promise.all( + fundParams.map(async (param, idx) => { + const { tokenA, tokenB } = PoolUtil.getTokenAmountsFromLiquidity( + param.liquidityAmount, + whirlpoolData.sqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(param.tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(param.tickUpperIndex), + true + ); + + const { tx } = await whirlpool.openPosition(param.tickLowerIndex, param.tickUpperIndex, { + liquidityAmount: param.liquidityAmount, + tokenMaxA: tokenA, + tokenMaxB: tokenB, + }); + await tx.buildAndExecute(); + }) + ); +} diff --git a/sdk/tests/utils/v2/test-transfer-hook-program.ts b/sdk/tests/utils/v2/test-transfer-hook-program.ts new file mode 100644 index 000000000..57bf6de6b --- /dev/null +++ b/sdk/tests/utils/v2/test-transfer-hook-program.ts @@ -0,0 +1,82 @@ +import { AnchorProvider, web3 } from "@coral-xyz/anchor"; +import { AccountMeta } from "@solana/web3.js"; +import { getExtraAccountMetasForHookProgram } from "./token-2022"; +import { TEST_TOKEN_2022_PROGRAM_ID, TEST_TRANSFER_HOOK_PROGRAM_ID } from "../test-consts"; +import { ASSOCIATED_TOKEN_PROGRAM_ID, createUpdateTransferHookInstruction } from "@solana/spl-token"; + +export async function getExtraAccountMetasForTestTransferHookProgram( + provider: AnchorProvider, + mint: web3.PublicKey, +): Promise { + const dummy = web3.PublicKey.default; + return getExtraAccountMetasForHookProgram( + provider, + TEST_TRANSFER_HOOK_PROGRAM_ID, + dummy, // not used to derive addresses + mint, + dummy, // not used to derive addresses + dummy, // not used to derive addresses + 0, // not used to derive addresses + ); +} + +export async function getTestTransferHookCounter( + provider: AnchorProvider, + mint: web3.PublicKey, +): Promise { + const [counterAccountPDA] = web3.PublicKey.findProgramAddressSync( + [Buffer.from("counter"), mint.toBuffer()], + TEST_TRANSFER_HOOK_PROGRAM_ID + ); + + const data = await provider.connection.getAccountInfo(counterAccountPDA); + return data!.data.readInt32LE(8); +} + +export async function updateTransferHookProgram( + provider: AnchorProvider, + mint: web3.PublicKey, + newTransferHookProgramId: web3.PublicKey, + authority?: web3.Keypair, +) { + const tx = new web3.Transaction(); + tx.add( + createUpdateTransferHookInstruction( + mint, + authority?.publicKey ?? provider.wallet.publicKey, + newTransferHookProgramId, + undefined, + TEST_TOKEN_2022_PROGRAM_ID, + ) + ); + return provider.sendAndConfirm(tx, !!authority ? [authority] : [], { commitment: "confirmed" }); +} + +export function createInitializeExtraAccountMetaListInstruction( + payer: web3.PublicKey, + mint: web3.PublicKey +): web3.TransactionInstruction { + // create ExtraAccountMetaList account + const [extraAccountMetaListPDA] = web3.PublicKey.findProgramAddressSync( + [Buffer.from("extra-account-metas"), mint.toBuffer()], + TEST_TRANSFER_HOOK_PROGRAM_ID + ); + const [counterAccountPDA] = web3.PublicKey.findProgramAddressSync( + [Buffer.from("counter"), mint.toBuffer()], + TEST_TRANSFER_HOOK_PROGRAM_ID + ); + + return { + programId: TEST_TRANSFER_HOOK_PROGRAM_ID, + keys: [ + {pubkey: payer, isSigner: true, isWritable: true}, + {pubkey: extraAccountMetaListPDA, isSigner: false, isWritable: true}, + {pubkey: mint, isSigner: false, isWritable: false}, + {pubkey: counterAccountPDA, isSigner: false, isWritable: true}, + {pubkey: TEST_TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false}, + {pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false}, + {pubkey: web3.SystemProgram.programId, isSigner: false, isWritable: false}, + ], + data: Buffer.from([0x5c, 0xc5, 0xae, 0xc5, 0x29, 0x7c, 0x13, 0x03]), // InitializeExtraAccountMetaList + } +} \ No newline at end of file diff --git a/sdk/tests/utils/v2/token-2022.ts b/sdk/tests/utils/v2/token-2022.ts new file mode 100644 index 000000000..00d1fde46 --- /dev/null +++ b/sdk/tests/utils/v2/token-2022.ts @@ -0,0 +1,869 @@ +import { AnchorProvider, BN, web3 } from "@coral-xyz/anchor"; +import { AddressUtil, TokenUtil, TransactionBuilder, U64_MAX, ZERO } from "@orca-so/common-sdk"; +import { + AccountLayout, + AccountState, + ExtensionType, + LENGTH_SIZE, + NATIVE_MINT, + NATIVE_MINT_2022, + TYPE_SIZE, + TransferFee, + addExtraAccountMetasForExecute, + calculateFee, + createApproveInstruction, + createAssociatedTokenAccountInstruction, + createCreateNativeMintInstruction, + createDisableRequiredMemoTransfersInstruction, + createEnableRequiredMemoTransfersInstruction, + createInitializeAccount3Instruction, + createInitializeDefaultAccountStateInstruction, + createInitializeGroupMemberPointerInstruction, + createInitializeGroupPointerInstruction, + createInitializeInterestBearingMintInstruction, + createInitializeMetadataPointerInstruction, + createInitializeMintCloseAuthorityInstruction, + createInitializeMintInstruction, + createInitializeNonTransferableMintInstruction, + createInitializePermanentDelegateInstruction, + createInitializeTransferFeeConfigInstruction, + createInitializeTransferHookInstruction, + createMintToInstruction, + createReallocateInstruction, + getAccount, + getAccountLen, + getAccountLenForMint, + getAssociatedTokenAddressSync, + getExtensionTypes, + getMemoTransfer, + getMint, + getMintLen, + getTypeLen +} from "@solana/spl-token"; +import { + TokenMetadata, + pack as packTokenMetadata, + createInitializeInstruction as createInitializeTokenMetadataInstruction, +} from "@solana/spl-token-metadata"; +import { + createInitializeGroupInstruction, + createInitializeMemberInstruction, + packTokenGroup, + packTokenGroupMember, + TokenGroup, + TokenGroupMember, +} from "@solana/spl-token-group"; +import { TEST_TOKEN_PROGRAM_ID, TEST_TOKEN_2022_PROGRAM_ID, TEST_TRANSFER_HOOK_PROGRAM_ID, ZERO_BN } from "../test-consts"; +import { TokenTrait } from "./init-utils-v2"; +import { Keypair, TransactionInstruction, AccountMeta } from "@solana/web3.js"; +import invariant from "tiny-invariant"; +import { PoolUtil } from "../../../src"; +import * as assert from "assert"; +import { PublicKey } from "@solana/web3.js"; +import { createInitializeExtraAccountMetaListInstruction } from "./test-transfer-hook-program"; +import { createInitializeConfidentialTransferMintInstruction } from "./confidential-transfer"; + +export async function createMintV2( + provider: AnchorProvider, + tokenTrait: TokenTrait, + authority?: web3.PublicKey, + mintKeypair?: web3.Keypair, +): Promise { + if (authority === undefined) { + authority = provider.wallet.publicKey; + } + + if (tokenTrait.isNativeMint) { + if (tokenTrait.isToken2022) { + await initializeNativeMint2022Idempotent(provider); + return NATIVE_MINT_2022; + } + return NATIVE_MINT; + } + + const mint = mintKeypair ?? web3.Keypair.generate(); + const instructions = await createMintInstructions(provider, tokenTrait, authority, mint.publicKey); + + const tx = new web3.Transaction(); + tx.add(...instructions); + + await provider.sendAndConfirm(tx, [mint], { commitment: "confirmed" }); + + return mint.publicKey; +} + +async function createMintInstructions( + provider: AnchorProvider, + tokenTrait: TokenTrait, + authority: web3.PublicKey, + mint: web3.PublicKey +) { + invariant(!tokenTrait.isNativeMint, "Cannot create a mint for the native token"); + + if (!tokenTrait.isToken2022) { + const instructions = [ + web3.SystemProgram.createAccount({ + fromPubkey: provider.wallet.publicKey, + newAccountPubkey: mint, + space: 82, + lamports: await provider.connection.getMinimumBalanceForRentExemption(82), + programId: TEST_TOKEN_PROGRAM_ID, + }), + createInitializeMintInstruction(mint, 0, authority, tokenTrait.hasFreezeAuthority ? authority : null, TEST_TOKEN_PROGRAM_ID), + ]; + return instructions; + } else { + const fixedLengthExtensions: ExtensionType[] = []; + const rentReservedSpace: number[] = []; + const extensions: TransactionInstruction[] = []; + const postInitialization: TransactionInstruction[] = []; + + // PermanentDelegate + if (tokenTrait.hasPermanentDelegate) { + fixedLengthExtensions.push(ExtensionType.PermanentDelegate); + extensions.push( + createInitializePermanentDelegateInstruction( + mint, + authority, + TEST_TOKEN_2022_PROGRAM_ID + ) + ); + } + + // TransferFee + if (tokenTrait.hasTransferFeeExtension) { + fixedLengthExtensions.push(ExtensionType.TransferFeeConfig); + extensions.push( + createInitializeTransferFeeConfigInstruction( + mint, + authority, + authority, + tokenTrait.transferFeeInitialBps ?? 500, // default: 5% + tokenTrait.transferFeeInitialMax ?? BigInt(U64_MAX.toString()), // default: virtually unlimited + TEST_TOKEN_2022_PROGRAM_ID + ) + ); + } + + // TransferHook + if (tokenTrait.hasTransferHookExtension) { + fixedLengthExtensions.push(ExtensionType.TransferHook); + extensions.push( + createInitializeTransferHookInstruction( + mint, + authority, + TEST_TRANSFER_HOOK_PROGRAM_ID, + TEST_TOKEN_2022_PROGRAM_ID, + ) + ); + + // create ExtraAccountMetaList account + postInitialization.push(createInitializeExtraAccountMetaListInstruction( + provider.wallet.publicKey, + mint, + )); + } + + // ConfidentialTransfer + // [March 6, 2024] getTypeLen(ExtensionType.ConfidentialTransferMint) return 97, but 65 (2 pubkey + 1 bool) is valid + // https://github.com/solana-labs/solana-program-library/blob/d72289c79a04411c69a8bf1054f7156b6196f9b3/token/js/src/extensions/extensionType.ts#L74 + let confidentialTransferMintSizePatch = 0; + if (tokenTrait.hasConfidentialTransferExtension) { + fixedLengthExtensions.push(ExtensionType.ConfidentialTransferMint); + confidentialTransferMintSizePatch = (65 - getTypeLen(ExtensionType.ConfidentialTransferMint)); + extensions.push( + createInitializeConfidentialTransferMintInstruction( + mint, + authority, + true, // autoApproveNewAccounts + PublicKey.default, // auditorElgamal + TEST_TOKEN_2022_PROGRAM_ID, + ) + ); + } + + // InterestBearing + if (tokenTrait.hasInterestBearingExtension) { + fixedLengthExtensions.push(ExtensionType.InterestBearingConfig); + extensions.push( + createInitializeInterestBearingMintInstruction( + mint, + authority, + 1, + TEST_TOKEN_2022_PROGRAM_ID, + ) + ); + } + + // CloseMintAuthority + if (tokenTrait.hasMintCloseAuthorityExtension) { + fixedLengthExtensions.push(ExtensionType.MintCloseAuthority); + extensions.push( + createInitializeMintCloseAuthorityInstruction( + mint, + authority, + TEST_TOKEN_2022_PROGRAM_ID, + ) + ); + } + + // DefaultAccountState + if (tokenTrait.hasDefaultAccountStateExtension) { + fixedLengthExtensions.push(ExtensionType.DefaultAccountState); + extensions.push( + createInitializeDefaultAccountStateInstruction( + mint, + tokenTrait.defaultAccountInitialState ?? AccountState.Frozen, + TEST_TOKEN_2022_PROGRAM_ID, + ) + ); + } + + // NonTransferableMint + if (tokenTrait.hasNonTransferableExtension) { + fixedLengthExtensions.push(ExtensionType.NonTransferable); + extensions.push( + createInitializeNonTransferableMintInstruction( + mint, + TEST_TOKEN_2022_PROGRAM_ID, + ) + ); + } + + // TokenMetadata + if (tokenTrait.hasTokenMetadataExtension) { + const identifier = mint.toBase58().slice(0, 8); + const metadata: TokenMetadata = { + mint, + updateAuthority: authority, + name: `test token ${identifier}`, + symbol: identifier, + uri: `https://test.orca.so/${identifier}.json`, + additionalMetadata: [], + }; + + const tokenMetadataSize = packTokenMetadata(metadata).length; + const tokenMetadataExtensionSize = TYPE_SIZE + LENGTH_SIZE + tokenMetadataSize; + rentReservedSpace.push(tokenMetadataExtensionSize); + postInitialization.push( + createInitializeTokenMetadataInstruction({ + metadata: mint, + mint, + mintAuthority: authority, + updateAuthority: metadata.updateAuthority!, + name: metadata.name, + symbol: metadata.symbol, + uri: metadata.uri, + programId: TEST_TOKEN_2022_PROGRAM_ID, + }) + ); + } + + // MetadataPointer + if (tokenTrait.hasMetadataPointerExtension) { + fixedLengthExtensions.push(ExtensionType.MetadataPointer); + extensions.push( + createInitializeMetadataPointerInstruction( + mint, + authority, + mint, + TEST_TOKEN_2022_PROGRAM_ID, + ) + ); + } + + // GroupPointer + if (tokenTrait.hasGroupPointerExtension) { + fixedLengthExtensions.push(ExtensionType.GroupPointer); + extensions.push( + createInitializeGroupPointerInstruction( + mint, + authority, + mint, + TEST_TOKEN_2022_PROGRAM_ID, + ) + ); + } + + // MemberPointer + if (tokenTrait.hasGroupMemberPointerExtension) { + fixedLengthExtensions.push(ExtensionType.GroupMemberPointer); + extensions.push( + createInitializeGroupMemberPointerInstruction( + mint, + authority, + null, + TEST_TOKEN_2022_PROGRAM_ID, + ) + ); + } + + // Group + if (tokenTrait.hasGroupExtension) { + const groupData: TokenGroup = { + mint, + updateAuthority: authority, + maxSize: 10, + size: 10, + }; + + const tokenGroupSize = packTokenGroup(groupData).length; + const tokenGroupExtensionSize = TYPE_SIZE + LENGTH_SIZE + tokenGroupSize; + rentReservedSpace.push(tokenGroupExtensionSize); + postInitialization.push( + createInitializeGroupInstruction({ + // maybe this data is meaning less, but it is okay, because we use this to test rejecting it. + mint: mint, + mintAuthority: authority, + updateAuthority: PublicKey.default,// groupData.updateAuthority!, + group: mint, + maxSize: groupData.maxSize, + programId: TEST_TOKEN_2022_PROGRAM_ID, + }) + ); + } + + // Member + if (tokenTrait.hasGroupMemberExtension) { + const groupMemberData: TokenGroupMember = { + mint: mint, + group: mint, + memberNumber: 10, + }; + + const tokenGroupMemberSize = packTokenGroupMember(groupMemberData).length; + const tokenGroupMemberExtensionSize = TYPE_SIZE + LENGTH_SIZE + tokenGroupMemberSize; + rentReservedSpace.push(tokenGroupMemberExtensionSize); + postInitialization.push( + createInitializeMemberInstruction({ + // maybe this data is meaning less, but it is okay, because we use this to test rejecting it. + group: mint, + memberMint: mint, + groupUpdateAuthority: authority, + member: mint, + memberMintAuthority: authority, + programId: TEST_TOKEN_2022_PROGRAM_ID, + }) + ); + } + + const space = getMintLen(fixedLengthExtensions) + confidentialTransferMintSizePatch; + const rentOnlySpace = rentReservedSpace.reduce((sum, n) => { return sum + n; }, 0); + const instructions = [ + web3.SystemProgram.createAccount({ + fromPubkey: provider.wallet.publicKey, + newAccountPubkey: mint, + space, + lamports: await provider.connection.getMinimumBalanceForRentExemption(space + rentOnlySpace) , + programId: TEST_TOKEN_2022_PROGRAM_ID, + }), + ...extensions, + createInitializeMintInstruction(mint, 0, authority, tokenTrait.hasFreezeAuthority ? authority : null, TEST_TOKEN_2022_PROGRAM_ID), + ...postInitialization, + ]; + return instructions; + } +} + +export async function createTokenAccountV2( + provider: AnchorProvider, + tokenTrait: TokenTrait, + mint: web3.PublicKey, + owner: web3.PublicKey +) { + const tokenAccount = web3.Keypair.generate(); + const tx = new web3.Transaction(); + tx.add(...(await createTokenAccountInstructions(provider, tokenTrait, tokenAccount.publicKey, mint, owner))); + await provider.sendAndConfirm(tx, [tokenAccount], { commitment: "confirmed" }); + return tokenAccount.publicKey; +} + +export async function createAssociatedTokenAccountV2( + provider: AnchorProvider, + tokenTrait: TokenTrait, + mint: web3.PublicKey, + owner: web3.PublicKey, + payer: web3.PublicKey +) { + const tokenProgram = tokenTrait.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID; + const ataAddress = getAssociatedTokenAddressSync(mint, owner, undefined, tokenProgram); + const instr = createAssociatedTokenAccountInstruction( + payer, + ataAddress, + owner, + mint, + tokenProgram, + ); + const tx = new web3.Transaction(); + tx.add(instr); + await provider.sendAndConfirm(tx, [], { commitment: "confirmed" }); + return ataAddress; +} + +async function createTokenAccountInstructions( + provider: AnchorProvider, + tokenTrait: TokenTrait, + newAccountPubkey: web3.PublicKey, + mint: web3.PublicKey, + owner: web3.PublicKey, + lamports?: number +) { + const mintAccountInfo = await provider.connection.getAccountInfo(mint); + const mintData = await getMint(provider.connection, mint, undefined, mintAccountInfo!.owner); + + const isToken2022 = mintAccountInfo!.owner.equals(TEST_TOKEN_2022_PROGRAM_ID); + + if (!isToken2022) { + if (lamports === undefined) { + lamports = await provider.connection.getMinimumBalanceForRentExemption(165); + } + return [ + web3.SystemProgram.createAccount({ + fromPubkey: provider.wallet.publicKey, + newAccountPubkey, + space: 165, + lamports, + programId: TEST_TOKEN_PROGRAM_ID, + }), + createInitializeAccount3Instruction(newAccountPubkey, mint, owner, TEST_TOKEN_PROGRAM_ID) + ]; + } else { + const accountLen = getAccountLenForMint(mintData); + if (lamports === undefined) { + lamports = await provider.connection.getMinimumBalanceForRentExemption(accountLen); + } + return [ + web3.SystemProgram.createAccount({ + fromPubkey: provider.wallet.publicKey, + newAccountPubkey, + space: accountLen, + lamports, + programId: TEST_TOKEN_2022_PROGRAM_ID, + }), + createInitializeAccount3Instruction(newAccountPubkey, mint, owner, TEST_TOKEN_2022_PROGRAM_ID) + ]; + } +} + +export async function mintToDestinationV2( + provider: AnchorProvider, + tokenTrait: TokenTrait, + mint: web3.PublicKey, + destination: web3.PublicKey, + amount: number | BN +): Promise { + const tx = new web3.Transaction(); + const amountVal = amount instanceof BN ? BigInt(amount.toString()) : amount; + tx.add( + createMintToInstruction( + mint, + destination, + provider.wallet.publicKey, + amountVal, + undefined, + tokenTrait.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID + ) + ); + return provider.sendAndConfirm(tx, [], { commitment: "confirmed" }); +} + +export async function createAndMintToTokenAccountV2( + provider: AnchorProvider, + tokenTrait: TokenTrait, + mint: web3.PublicKey, + amount: number | BN +): Promise { + const tokenAccount = await createTokenAccountV2(provider, tokenTrait, mint, provider.wallet.publicKey); + await mintToDestinationV2(provider, tokenTrait, mint, tokenAccount, new BN(amount.toString())); + return tokenAccount; +} + +export async function createAndMintToAssociatedTokenAccountV2( + provider: AnchorProvider, + tokenTrait: TokenTrait, + mint: web3.PublicKey, + amount: number | BN, + destinationWallet?: web3.PublicKey, + payer?: web3.PublicKey +): Promise { + const destinationWalletKey = destinationWallet ? destinationWallet : provider.wallet.publicKey; + const payerKey = payer ? payer : provider.wallet.publicKey; + + // Workaround For SOL - just create a wSOL account to satisfy the rest of the test building pipeline. + // Tests who want to test with SOL will have to request their own airdrop. + if (mint.equals(NATIVE_MINT)) { + invariant(tokenTrait.isNativeMint, "Mint must be the native mint"); + const rentExemption = await provider.connection.getMinimumBalanceForRentExemption( + AccountLayout.span, + "confirmed" + ); + const txBuilder = new TransactionBuilder(provider.connection, provider.wallet); + const { address: tokenAccount, ...ix } = TokenUtil.createWrappedNativeAccountInstruction( + destinationWalletKey, + new BN(amount.toString()), + rentExemption + ); + txBuilder.addInstruction({ ...ix, cleanupInstructions: [] }); + await txBuilder.buildAndExecute(); + return tokenAccount; + } + if (mint.equals(NATIVE_MINT_2022)) { + invariant(tokenTrait.isNativeMint, "Mint must be the native mint"); + + const space = getAccountLen([]); + const rentExemption = await provider.connection.getMinimumBalanceForRentExemption(space, "confirmed"); + const tokenAccountKeypair = Keypair.generate(); + + const txBuilder = new TransactionBuilder(provider.connection, provider.wallet); + txBuilder.addInstruction({ + instructions: [ + web3.SystemProgram.createAccount({ + fromPubkey: provider.wallet.publicKey, + newAccountPubkey: tokenAccountKeypair.publicKey, + space, + lamports: rentExemption, + programId: TEST_TOKEN_2022_PROGRAM_ID, + }), + createInitializeAccount3Instruction(tokenAccountKeypair.publicKey, mint, destinationWalletKey, TEST_TOKEN_2022_PROGRAM_ID) + ], + cleanupInstructions: [], + signers: [tokenAccountKeypair] + }); + await txBuilder.buildAndExecute(); + return tokenAccountKeypair.publicKey; + } + + const tokenAccounts = await provider.connection.getParsedTokenAccountsByOwner(destinationWalletKey, { + programId: tokenTrait.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID, + }); + + let tokenAccount = tokenAccounts.value.map((account) => { + if (account.account.data.parsed.info.mint === mint.toString()) { + return account.pubkey + } + }).filter(Boolean)[0]; + + if (!tokenAccount) { + tokenAccount = await createAssociatedTokenAccountV2( + provider, + tokenTrait, + mint, + destinationWalletKey, + payerKey + ); + } + + await mintToDestinationV2(provider, tokenTrait, mint, tokenAccount!, new BN(amount.toString())); + return tokenAccount!; +} + +export async function getTokenBalance(provider: AnchorProvider, vault: web3.PublicKey) { + return (await provider.connection.getTokenAccountBalance(vault, "confirmed")).value.amount; +} + +export async function createInOrderMintsV2(provider: AnchorProvider, tokenTraitA: TokenTrait, tokenTraitB: TokenTrait) { + if (tokenTraitA.isNativeMint && !tokenTraitB.isNativeMint) { + const tokenXMintPubKey = tokenTraitA.isToken2022 ? NATIVE_MINT_2022 : NATIVE_MINT; + + let ordered; + do { + const tokenYMintPubKey = await createMintV2(provider, tokenTraitB); + ordered = PoolUtil.orderMints(tokenXMintPubKey, tokenYMintPubKey).map(AddressUtil.toPubKey); + } while (!ordered[0].equals(tokenXMintPubKey)); + return ordered; + } else if (!tokenTraitA.isNativeMint && tokenTraitB.isNativeMint) { + const tokenYMintPubKey = tokenTraitB.isToken2022 ? NATIVE_MINT_2022 : NATIVE_MINT; + + let ordered; + do { + const tokenXMintPubKey = await createMintV2(provider, tokenTraitA); + ordered = PoolUtil.orderMints(tokenXMintPubKey, tokenYMintPubKey).map(AddressUtil.toPubKey); + } while (!ordered[1].equals(tokenYMintPubKey)); + return ordered; + } + else if (!tokenTraitA.isNativeMint && !tokenTraitB.isNativeMint) { + while (true) { + const tokenXMintPubKey = await createMintV2(provider, tokenTraitA); + const tokenYMintPubKey = await createMintV2(provider, tokenTraitB); + const ordered = PoolUtil.orderMints(tokenXMintPubKey, tokenYMintPubKey).map(AddressUtil.toPubKey); + if (ordered[0].equals(tokenXMintPubKey)) { + return ordered; + } + } + } else { + // A must be WSOL: So11111111111111111111111111111111111111112 + // B must be WSOL-2022: 9pan9bMn5HatX4EJdBwg9VgCa7Uz5HL8N1m5D3NdXejP + invariant(!tokenTraitA.isToken2022, "A must be the native mint"); + invariant(tokenTraitB.isToken2022, "B must be the native mint 2022"); + return [NATIVE_MINT, NATIVE_MINT_2022]; + } +}; + +export async function initializeNativeMint2022Idempotent( + provider: AnchorProvider, +) { + const accountInfo = await provider.connection.getAccountInfo(NATIVE_MINT_2022, "confirmed"); + + // already initialized + if (accountInfo !== null) return; + + const ix = createCreateNativeMintInstruction( + provider.wallet.publicKey, + NATIVE_MINT_2022, + TEST_TOKEN_2022_PROGRAM_ID, + ); + + const txBuilder = new TransactionBuilder(provider.connection, provider.wallet); + txBuilder.addInstruction({ instructions: [ix], cleanupInstructions: [], signers: [] }); + await txBuilder.buildAndExecute(); +} + +export async function approveTokenV2( + provider: AnchorProvider, + tokenTrait: TokenTrait, + tokenAccount: web3.PublicKey, + delegate: web3.PublicKey, + amount: number | BN, + owner?: web3.Keypair +) { + const tx = new web3.Transaction(); + const tokenProgram = tokenTrait.isToken2022 ? TEST_TOKEN_2022_PROGRAM_ID : TEST_TOKEN_PROGRAM_ID; + const amountVal = amount instanceof BN ? BigInt(amount.toString()) : amount; + tx.add( + createApproveInstruction( + tokenAccount, + delegate, + owner?.publicKey || provider.wallet.publicKey, + amountVal, + undefined, + tokenProgram, + ) + ); + return provider.sendAndConfirm(tx, !!owner ? [owner] : [], { commitment: "confirmed" }); +} + +export async function enableRequiredMemoTransfers( + provider: AnchorProvider, + tokenAccount: web3.PublicKey, + owner?: web3.Keypair, +) { + const tx = new web3.Transaction(); + tx.add( + createReallocateInstruction( + tokenAccount, + owner?.publicKey || provider.wallet.publicKey, + [ExtensionType.MemoTransfer], + owner?.publicKey || provider.wallet.publicKey, + undefined, + TEST_TOKEN_2022_PROGRAM_ID, + ) + ); + tx.add( + createEnableRequiredMemoTransfersInstruction( + tokenAccount, + owner?.publicKey || provider.wallet.publicKey, + undefined, + TEST_TOKEN_2022_PROGRAM_ID + ) + ); + return provider.sendAndConfirm(tx, !!owner ? [owner] : [], { commitment: "confirmed" }); +} + +export async function disableRequiredMemoTransfers( + provider: AnchorProvider, + tokenAccount: web3.PublicKey, + owner?: web3.Keypair, +) { + const tx = new web3.Transaction(); + tx.add( + createDisableRequiredMemoTransfersInstruction( + tokenAccount, + owner?.publicKey || provider.wallet.publicKey, + undefined, + TEST_TOKEN_2022_PROGRAM_ID + ) + ); + return provider.sendAndConfirm(tx, !!owner ? [owner] : [], { commitment: "confirmed" }); +} + +export async function isRequiredMemoTransfersEnabled( + provider: AnchorProvider, + tokenAccount: web3.PublicKey, +) { + const account = await getAccount(provider.connection, tokenAccount, "confirmed", TEST_TOKEN_2022_PROGRAM_ID); + + const extensions = getExtensionTypes(account.tlvData); + if (!extensions.includes(ExtensionType.MemoTransfer)) return false; + + const memoTransferData = getMemoTransfer(account); + return memoTransferData?.requireIncomingTransferMemos; +} + +export async function asyncAssertTokenVaultV2( + provider: AnchorProvider, + account: web3.PublicKey, + expectedMint: web3.PublicKey, + expectedAccountOwner: web3.PublicKey, + expectedTokenProgram: web3.PublicKey, +) { + const accountInfo = await provider.connection.getAccountInfo(account); + assert.ok(accountInfo); + assert.ok(accountInfo.owner.equals(expectedTokenProgram)); + const parsedAccount = AccountLayout.decode(accountInfo.data); + assert.ok(parsedAccount.mint.equals(expectedMint)); + assert.ok(parsedAccount.owner.equals(expectedAccountOwner)); +} + +export async function asyncAssertOwnerProgram( + provider: AnchorProvider, + account: web3.PublicKey, + programId: web3.PublicKey +) { + const accountInfo = await provider.connection.getAccountInfo(account); + assert.ok(accountInfo); + assert.ok(accountInfo.owner.equals(programId)); +} + +export async function getExtraAccountMetasForHookProgram( + provider: AnchorProvider, + hookProgramId: web3.PublicKey, + source: web3.PublicKey, + mint: web3.PublicKey, + destination: web3.PublicKey, + owner: web3.PublicKey, + amount: number | bigint, +): Promise { + const instruction = new TransactionInstruction({ + programId: TEST_TOKEN_2022_PROGRAM_ID, + keys: [ + {pubkey: source, isSigner: false, isWritable: false}, + {pubkey: mint, isSigner: false, isWritable: false}, + {pubkey: destination, isSigner: false, isWritable: false}, + {pubkey: owner, isSigner: false, isWritable: false}, + {pubkey: owner, isSigner: false, isWritable: false}, + ] + }); + + await addExtraAccountMetasForExecute( + provider.connection, + instruction, + hookProgramId, + source, + mint, + destination, + owner, + amount, + "confirmed" + ); + + const extraAccountMetas = instruction.keys.slice(5); + return extraAccountMetas.length > 0 + ? extraAccountMetas + : undefined; +} + +function ceil_div_bn(num: BN, denom: BN): BN { + return num.add(denom.subn(1)).div(denom); +} + +export function calculateTransferFeeIncludedAmount( + transferFee: TransferFee, + amount: BN, +): { amount: BN, fee: BN } { + // https://github.com/solana-labs/solana-program-library/blob/master/token/program-2022/src/extension/transfer_fee/mod.rs#L90 + + const ONE_IN_BASIS_POINTS = 10_000; + const maxFeeBN = new BN(transferFee.maximumFee.toString()); + + // edge cases + + if (transferFee.transferFeeBasisPoints === 0) { + return { + amount, + fee: ZERO_BN, + }; + } + + if (amount.isZero()) { + return { + amount: ZERO_BN, + fee: ZERO_BN, + }; + } + + if (transferFee.transferFeeBasisPoints === ONE_IN_BASIS_POINTS) { + if (amount.add(maxFeeBN).gt(U64_MAX)) { + throw new Error("TransferFeeIncludedAmount exceeds U64_MAX"); + } + return { + amount: amount.add(maxFeeBN), + fee: maxFeeBN, + }; + } + + // normal case + + const num = amount.muln(ONE_IN_BASIS_POINTS); + const denom = new BN(ONE_IN_BASIS_POINTS - transferFee.transferFeeBasisPoints); + const rawFeeIncludedAmount = ceil_div_bn(num, denom); + + if (rawFeeIncludedAmount.sub(amount).gte(maxFeeBN)) { + if (amount.add(maxFeeBN).gt(U64_MAX)) { + throw new Error("TransferFeeIncludedAmount exceeds U64_MAX"); + } + + return { + amount: amount.add(maxFeeBN), + fee: maxFeeBN, + }; + } + + if (rawFeeIncludedAmount.gt(U64_MAX)) { + throw new Error("TransferFeeIncludedAmount exceeds U64_MAX"); + } + + return { + amount: rawFeeIncludedAmount, + fee: rawFeeIncludedAmount.sub(amount), + }; +} + +export function calculateTransferFeeExcludedAmount( + transferFee: TransferFee, + amount: BN, +): { amount: BN, fee: BN } { + const fee = calculateFee(transferFee, BigInt(amount.toString())); + const feeBN = new BN(fee.toString()); + return { + amount: amount.sub(feeBN), + fee: feeBN, + }; +} + +export async function mintTokensToTestAccountV2( + provider: AnchorProvider, + tokenAMint: PublicKey, + tokenTraitA: TokenTrait, + tokenMintForA: number, + tokenBMint: PublicKey, + tokenTraitB: TokenTrait, + tokenMintForB: number, + destinationWallet?: PublicKey +) { + const userTokenAAccount = await createAndMintToAssociatedTokenAccountV2( + provider, + tokenTraitA, + tokenAMint, + tokenMintForA, + destinationWallet + ); + const userTokenBAccount = await createAndMintToAssociatedTokenAccountV2( + provider, + tokenTraitB, + tokenBMint, + tokenMintForB, + destinationWallet + ); + + return [userTokenAAccount, userTokenBAccount]; +} \ No newline at end of file diff --git a/sdk/tests/utils/v2/transfer-fee.ts b/sdk/tests/utils/v2/transfer-fee.ts new file mode 100644 index 000000000..5ec6662be --- /dev/null +++ b/sdk/tests/utils/v2/transfer-fee.ts @@ -0,0 +1,49 @@ +// [Mar 12, 2024] SetTransferFee instruction is not supported in @solana/spl-token, so we need to build instructions manually... + +import { TOKEN_2022_PROGRAM_ID, TokenInstruction, TokenUnsupportedInstructionError, TransferFeeInstruction, programSupportsExtensions } from "@solana/spl-token"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { struct, u16, u8 } from '@solana/buffer-layout'; +import { u64 } from '@solana/buffer-layout-utils'; + +export interface SetTransferFeeInstructionData { + instruction: TokenInstruction.TransferFeeExtension; + transferFeeInstruction: TransferFeeInstruction.SetTransferFee; + transferFeeBasisPoints: number; + maximumFee: bigint; +} + +export const setTransferFeeInstructionData = struct([ + u8('instruction'), + u8('transferFeeInstruction'), + u16('transferFeeBasisPoints'), + u64('maximumFee'), +]); + +export function createSetTransferFeeInstruction( + mint: PublicKey, + newTransferFeeBasisPoints: number, + newMaximumFee: bigint, + transferFeeConfigAuthority: PublicKey, + programId: PublicKey = TOKEN_2022_PROGRAM_ID, +) { + if (!programSupportsExtensions(programId)) { + throw new TokenUnsupportedInstructionError(); + } + + const keys = [ + { pubkey: mint, isSigner: false, isWritable: true }, + { pubkey: transferFeeConfigAuthority, isSigner: true, isWritable: false }, + ]; + const data = Buffer.alloc(setTransferFeeInstructionData.span); + setTransferFeeInstructionData.encode( + { + instruction: TokenInstruction.TransferFeeExtension, + transferFeeInstruction: TransferFeeInstruction.SetTransferFee, + transferFeeBasisPoints: newTransferFeeBasisPoints, + maximumFee: newMaximumFee, + }, + data + ); + + return new TransactionInstruction({ keys, programId, data }); +} diff --git a/yarn.lock b/yarn.lock index bda7bdbdc..72cad5046 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21,14 +21,14 @@ dependencies: regenerator-runtime "^0.14.0" -"@coral-xyz/anchor@^0.27.0": - version "0.27.0" - resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.27.0.tgz#621e5ef123d05811b97e49973b4ed7ede27c705c" - integrity sha512-+P/vPdORawvg3A9Wj02iquxb4T0C5m4P6aZBVYysKl4Amk+r6aMPZkUhilBkD6E4Nuxnoajv3CFykUfkGE0n5g== +"@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.27.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" @@ -36,16 +36,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.27.0": - version "0.27.0" - resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.27.0.tgz#700c647ea5262b1488957ac7fb4e8acf72c72b63" - integrity sha512-tJKzhLukghTWPLy+n8K8iJKgBq1yLT/AxaNd10yJrX8mI56ao5+OFAKAqW/h0i79KCvb4BK0VGO5ECmmolFz9A== +"@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" @@ -138,44 +137,6 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@metaplex-foundation/beet-solana@^0.4.0": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet-solana/-/beet-solana-0.4.1.tgz#255747aa7feee1c20202146a752c057feca1948f" - integrity sha512-/6o32FNUtwK8tjhotrvU/vorP7umBuRFvBZrC6XCk51aKidBHe5LPVPA5AjGPbV3oftMfRuXPNd9yAGeEqeCDQ== - dependencies: - "@metaplex-foundation/beet" ">=0.1.0" - "@solana/web3.js" "^1.56.2" - bs58 "^5.0.0" - debug "^4.3.4" - -"@metaplex-foundation/beet@>=0.1.0", "@metaplex-foundation/beet@^0.7.1": - version "0.7.2" - resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet/-/beet-0.7.2.tgz#fa4726e4cfd4fb6fed6cddc9b5213c1c2a2d0b77" - integrity sha512-K+g3WhyFxKPc0xIvcIjNyV1eaTVJTiuaHZpig7Xx0MuYRMoJLLvhLTnUXhFdR5Tu2l2QSyKwfyXDgZlzhULqFg== - dependencies: - ansicolors "^0.3.2" - assert "^2.1.0" - bn.js "^5.2.0" - debug "^4.3.3" - -"@metaplex-foundation/cusper@^0.0.2": - version "0.0.2" - resolved "https://registry.yarnpkg.com/@metaplex-foundation/cusper/-/cusper-0.0.2.tgz#dc2032a452d6c269e25f016aa4dd63600e2af975" - integrity sha512-S9RulC2fFCFOQraz61bij+5YCHhSO9llJegK8c8Y6731fSi6snUSQJdCUqYS8AIgR0TKbQvdvgSyIIdbDFZbBA== - -"@metaplex-foundation/mpl-token-metadata@2.12.0": - version "2.12.0" - resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-token-metadata/-/mpl-token-metadata-2.12.0.tgz#9817b2d133c5af46c28ab284316b6985ef62b331" - integrity sha512-DetC2F5MwMRt4TmLXwj8PJ8nClRYGMecSQ4pr9iKKa+rWertHgKoJHl2XhheRa084GtL7i0ssOKbX2gfYFosuQ== - dependencies: - "@metaplex-foundation/beet" "^0.7.1" - "@metaplex-foundation/beet-solana" "^0.4.0" - "@metaplex-foundation/cusper" "^0.0.2" - "@solana/spl-token" "^0.3.6" - "@solana/web3.js" "^1.66.2" - bn.js "^5.2.0" - debug "^4.3.4" - "@noble/curves@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" @@ -195,11 +156,16 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== -"@noble/hashes@1.3.3", "@noble/hashes@^1.3.2": +"@noble/hashes@1.3.3": version "1.3.3" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== +"@noble/hashes@^1.3.1", "@noble/hashes@^1.3.3": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -221,10 +187,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@orca-so/common-sdk@^0.5.3": - version "0.5.3" - resolved "https://registry.yarnpkg.com/@orca-so/common-sdk/-/common-sdk-0.5.3.tgz#bec524984996a494ac00d9aa30be41d9457a2a92" - integrity sha512-PmlEZk6qidBZUenlx6qXWm4xdunyPj71N5bQvhkRb5iSo0rMs0zaevMGkZrIWNN80qf5tVgScsRfI9aAG9oQjg== +"@orca-so/common-sdk@0.6.0-alpha.1": + version "0.6.0-alpha.1" + resolved "https://registry.yarnpkg.com/@orca-so/common-sdk/-/common-sdk-0.6.0-alpha.1.tgz#9ed814ca4d6e095b2c74ecf1de6eddf6b4bbe556" + integrity sha512-G5pVGybh3U5CcxyMSiBqD6qugr3HqsafJXbLn/3/7SLky/YR0I7AvMBF+SiGooHb4ROIZ4jKVXRFXD1/43wKWg== dependencies: tiny-invariant "^1.3.1" @@ -284,6 +250,11 @@ resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.0.0-experimental.8618508.tgz#4f6709dd50e671267f3bea7d09209bc6471b7ad0" integrity sha512-JCz7mKjVKtfZxkuDtwMAUgA7YvJcA2BwpZaA1NOLcted4OMC4Prwa3DUe3f3181ixPYaRyptbF0Ikq2MbDkYEA== +"@solana/codecs-core@2.0.0-experimental.9741939": + version "2.0.0-experimental.9741939" + resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.0.0-experimental.9741939.tgz#1fa7f1f3b10ff924bd02c274615b2df1f5bda64d" + integrity sha512-7E51aEwLW+1ta6VWbEq0CbvwSUOcIwmaQCLpkOKsF6cwSNqE5GDv/jw9zKuiYOynlBFQO1Ws59a/hlb2wwwWKg== + "@solana/codecs-data-structures@2.0.0-experimental.8618508": version "2.0.0-experimental.8618508" resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-experimental.8618508.tgz#c16a704ac0f743a2e0bf73ada42d830b3402d848" @@ -292,6 +263,14 @@ "@solana/codecs-core" "2.0.0-experimental.8618508" "@solana/codecs-numbers" "2.0.0-experimental.8618508" +"@solana/codecs-data-structures@2.0.0-experimental.9741939": + version "2.0.0-experimental.9741939" + resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-experimental.9741939.tgz#0d38aecd4b15471b16c947643069f5d13a2b27e5" + integrity sha512-Ghjx0pFEA22T/cpqg3zXHYlnxqKytjTr1QjgfhWvBrlVFkqr3ZQL4av9/V6sHKvpoi+JNkEwM1wE+tlpt/54CA== + dependencies: + "@solana/codecs-core" "2.0.0-experimental.9741939" + "@solana/codecs-numbers" "2.0.0-experimental.9741939" + "@solana/codecs-numbers@2.0.0-experimental.8618508": version "2.0.0-experimental.8618508" resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.0.0-experimental.8618508.tgz#d84f9ed0521b22e19125eefc7d51e217fcaeb3e4" @@ -299,6 +278,13 @@ dependencies: "@solana/codecs-core" "2.0.0-experimental.8618508" +"@solana/codecs-numbers@2.0.0-experimental.9741939": + version "2.0.0-experimental.9741939" + resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.0.0-experimental.9741939.tgz#5e5be75187779ee43a7f515cd14abc809e5ba588" + integrity sha512-VXBvw8LZdGUJGuC33EFzvaUxCvgNtr9y9Td8zjK9wJDqKSzR0+G53CJv5K4AF25LUu8du8XHBK3VEz3YHl44nQ== + dependencies: + "@solana/codecs-core" "2.0.0-experimental.9741939" + "@solana/codecs-strings@2.0.0-experimental.8618508": version "2.0.0-experimental.8618508" resolved "https://registry.yarnpkg.com/@solana/codecs-strings/-/codecs-strings-2.0.0-experimental.8618508.tgz#72457b884d9be80b59b263bcce73892b081e9402" @@ -307,6 +293,14 @@ "@solana/codecs-core" "2.0.0-experimental.8618508" "@solana/codecs-numbers" "2.0.0-experimental.8618508" +"@solana/codecs-strings@2.0.0-experimental.9741939": + version "2.0.0-experimental.9741939" + resolved "https://registry.yarnpkg.com/@solana/codecs-strings/-/codecs-strings-2.0.0-experimental.9741939.tgz#1253c55149c410327f499336e702ab311e3f3571" + integrity sha512-J1DCTJsAMhFcIIvys/yNWM2Vr4HEWi+LzFB9xxdH+FHTynApjTMd4vI7frgnckuWH7ynp8EZFPnBTje1BphQrw== + dependencies: + "@solana/codecs-core" "2.0.0-experimental.9741939" + "@solana/codecs-numbers" "2.0.0-experimental.9741939" + "@solana/options@2.0.0-experimental.8618508": version "2.0.0-experimental.8618508" resolved "https://registry.yarnpkg.com/@solana/options/-/options-2.0.0-experimental.8618508.tgz#95385340e85f9e8a81b2bfba089404a61c8e9520" @@ -315,6 +309,25 @@ "@solana/codecs-core" "2.0.0-experimental.8618508" "@solana/codecs-numbers" "2.0.0-experimental.8618508" +"@solana/options@2.0.0-experimental.9741939": + version "2.0.0-experimental.9741939" + resolved "https://registry.yarnpkg.com/@solana/options/-/options-2.0.0-experimental.9741939.tgz#f9ee7f6cb41a3c23d85333e9e6d891d7f12c9b84" + integrity sha512-Fj76WDb+SWEEN3i0gEVQHGPHR2v54ECHILluQ5r18deLHjtZpD48a0dZepf0YcyAG7OHofkRdPxInZ3YYDuZeQ== + dependencies: + "@solana/codecs-core" "2.0.0-experimental.9741939" + "@solana/codecs-numbers" "2.0.0-experimental.9741939" + +"@solana/spl-token-group@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@solana/spl-token-group/-/spl-token-group-0.0.1.tgz#c892f7cc19ad1ed28b05713ddb95d3b1fe3c493d" + integrity sha512-dnBpjFhAskL+LzLfZhgeZVpDoO5ialtwear3vcHy3f+WNzJn/HITCmf341kekMHcLEeVaJts+4i/daVsfjqq7A== + dependencies: + "@solana/codecs-data-structures" "2.0.0-experimental.9741939" + "@solana/codecs-numbers" "2.0.0-experimental.9741939" + "@solana/codecs-strings" "2.0.0-experimental.9741939" + "@solana/options" "2.0.0-experimental.9741939" + "@solana/spl-type-length-value" "0.1.0" + "@solana/spl-token-metadata@^0.1.2": version "0.1.2" resolved "https://registry.yarnpkg.com/@solana/spl-token-metadata/-/spl-token-metadata-0.1.2.tgz#876e13432bd2960bd3cac16b9b0af63e69e37719" @@ -339,10 +352,10 @@ buffer-layout "^1.2.0" dotenv "10.0.0" -"@solana/spl-token@^0.3.11", "@solana/spl-token@^0.3.6": - version "0.3.11" - resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.3.11.tgz#cdc10f9472b29b39c8983c92592cadd06627fb9a" - integrity sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ== +"@solana/spl-token@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.4.1.tgz#7302c8052803f63012bd8189d42ca7d74d7917a5" + integrity sha512-DEe15GI0l+XLHwtau/3GUwGQJ9YY/VWNE0k/QuXaaGKo4adMZLEAIQUktRc/S2sRqPjvUdR5anZGxQ9p5khWZw== dependencies: "@solana/buffer-layout" "^4.0.0" "@solana/buffer-layout-utils" "^0.2.0" @@ -377,14 +390,14 @@ rpc-websockets "^7.5.1" superstruct "^0.14.2" -"@solana/web3.js@^1.56.2", "@solana/web3.js@^1.66.2", "@solana/web3.js@^1.88.0": - version "1.88.0" - resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.88.0.tgz#24e1482f63ac54914430b4ce5ab36eaf433ecdb8" - integrity sha512-E4BdfB0HZpb66OPFhIzPApNE2tG75Mc6XKIoeymUkx/IV+USSYuxDX29sjgE/KGNYxggrOf4YuYnRMI6UiPL8w== +"@solana/web3.js@^1.90.0": + version "1.91.1" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.91.1.tgz#d49d2f982b52070be3b987fd8d892fcbddd064b5" + integrity sha512-cPgjZXm688oM9cULvJ8u2VH6Qp5rvptE1N1VODVxn2mAbpZsWrvWNPjmASkMYT/HzyrtqFkPvFdSHg8Xjt7aQA== dependencies: "@babel/runtime" "^7.23.4" "@noble/curves" "^1.2.0" - "@noble/hashes" "^1.3.2" + "@noble/hashes" "^1.3.3" "@solana/buffer-layout" "^4.0.1" agentkeepalive "^4.5.0" bigint-buffer "^1.1.5" @@ -670,11 +683,6 @@ ansi-styles@^6.1.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -ansicolors@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" - integrity sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg== - any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -708,17 +716,6 @@ arrify@^1.0.0: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== -assert@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd" - integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw== - dependencies: - call-bind "^1.0.2" - is-nan "^1.3.2" - object-is "^1.1.5" - object.assign "^4.1.4" - util "^0.12.5" - assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" @@ -729,11 +726,6 @@ assertion-error@^2.0.1: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== -available-typed-arrays@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" - integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -746,11 +738,6 @@ base-x@^3.0.2: dependencies: safe-buffer "^5.0.1" -base-x@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a" - integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw== - base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -828,13 +815,6 @@ bs58@^4.0.0, bs58@^4.0.1: dependencies: base-x "^3.0.2" -bs58@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279" - integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ== - dependencies: - base-x "^4.0.0" - buffer-from@^1.0.0, buffer-from@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -860,15 +840,6 @@ bufferutil@^4.0.1: dependencies: node-gyp-build "^4.3.0" -call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" - integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== - dependencies: - function-bind "^1.1.2" - get-intrinsic "^1.2.1" - set-function-length "^1.1.1" - callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1005,7 +976,7 @@ debug@4.3.3: dependencies: ms "2.1.2" -debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -1039,24 +1010,6 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -define-data-property@^1.0.1, define-data-property@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" - integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== - dependencies: - get-intrinsic "^1.2.1" - gopd "^1.0.1" - has-property-descriptors "^1.0.0" - -define-properties@^1.1.3, define-properties@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" - integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== - dependencies: - define-data-property "^1.0.1" - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - delay@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" @@ -1335,13 +1288,6 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - foreground-child@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" @@ -1360,11 +1306,6 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -1380,16 +1321,6 @@ get-func-name@^2.0.1: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" - integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== - dependencies: - function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" - glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -1458,13 +1389,6 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - graphemer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" @@ -1480,37 +1404,6 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340" - integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== - dependencies: - get-intrinsic "^1.2.2" - -has-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" - integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== - -has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== - dependencies: - has-symbols "^1.0.2" - -hasown@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" - integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== - dependencies: - function-bind "^1.1.2" - he@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -1559,19 +1452,11 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3: +inherits@2: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -is-arguments@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -1579,11 +1464,6 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-callable@^1.1.3: - version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -1594,13 +1474,6 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-generator-function@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== - dependencies: - has-tostringtag "^1.0.0" - is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -1608,14 +1481,6 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-nan@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" - integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -1631,13 +1496,6 @@ is-plain-obj@^2.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== -is-typed-array@^1.1.3: - version "1.1.12" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" - integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== - dependencies: - which-typed-array "^1.1.11" - is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" @@ -1998,29 +1856,6 @@ object-assign@^4.0.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-is@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.4: - version "4.1.5" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" - integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== - dependencies: - call-bind "^1.0.5" - define-properties "^1.2.1" - has-symbols "^1.0.3" - object-keys "^1.1.1" - once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -2226,16 +2061,6 @@ serialize-javascript@6.0.0: dependencies: randombytes "^2.1.0" -set-function-length@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed" - integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ== - dependencies: - define-data-property "^1.1.1" - get-intrinsic "^1.2.1" - gopd "^1.0.1" - has-property-descriptors "^1.0.0" - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -2535,17 +2360,6 @@ utf-8-validate@^5.0.2: dependencies: node-gyp-build "^4.3.0" -util@^0.12.5: - version "0.12.5" - resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" - integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - is-typed-array "^1.1.3" - which-typed-array "^1.1.2" - uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -2579,17 +2393,6 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" -which-typed-array@^1.1.11, which-typed-array@^1.1.2: - version "1.1.13" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.13.tgz#870cd5be06ddb616f504e7b039c4c24898184d36" - integrity sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow== - dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.4" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.0" - which@2.0.2, which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"