diff --git a/Cargo.lock b/Cargo.lock index 44adf2f..984f037 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,30 +2,67 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures", + "opaque-debug", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589c637f0e68c877bbd59a4599bbe849cac8e5f3e4b5a3ebae8f528cd218dcdc" +dependencies = [ + "aead", + "aes", + "cipher 0.3.0", + "ctr", + "polyval", + "subtle", + "zeroize", +] + [[package]] name = "ahash" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.3", + "getrandom 0.2.8", "once_cell", "version_check", ] [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] [[package]] name = "anchor-attribute-access-control" -version = "0.20.1" -source = "git+https://github.com/project-serum/anchor?tag=v0.20.1#a81ff88d76956533a4ca5ae74d5dec37d7d76b51" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf7d535e1381be3de2c0716c0a1c1e32ad9df1042cddcf7bc18d743569e53319" dependencies = [ "anchor-syn", "anyhow", @@ -37,8 +74,9 @@ dependencies = [ [[package]] name = "anchor-attribute-account" -version = "0.20.1" -source = "git+https://github.com/project-serum/anchor?tag=v0.20.1#a81ff88d76956533a4ca5ae74d5dec37d7d76b51" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bcd731f21048a032be27c7791701120e44f3f6371358fc4261a7f716283d29" dependencies = [ "anchor-syn", "anyhow", @@ -51,8 +89,9 @@ dependencies = [ [[package]] name = "anchor-attribute-constant" -version = "0.20.1" -source = "git+https://github.com/project-serum/anchor?tag=v0.20.1#a81ff88d76956533a4ca5ae74d5dec37d7d76b51" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1be64a48e395fe00b8217287f226078be2cf32dae42fdf8a885b997945c3d28" dependencies = [ "anchor-syn", "proc-macro2", @@ -61,8 +100,9 @@ dependencies = [ [[package]] name = "anchor-attribute-error" -version = "0.20.1" -source = "git+https://github.com/project-serum/anchor?tag=v0.20.1#a81ff88d76956533a4ca5ae74d5dec37d7d76b51" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ea6713d1938c0da03656ff8a693b17dc0396da66d1ba320557f07e86eca0d4" dependencies = [ "anchor-syn", "proc-macro2", @@ -72,8 +112,9 @@ dependencies = [ [[package]] name = "anchor-attribute-event" -version = "0.20.1" -source = "git+https://github.com/project-serum/anchor?tag=v0.20.1#a81ff88d76956533a4ca5ae74d5dec37d7d76b51" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401f11efb3644285685f8339829a9786d43ed7490bb1699f33c478d04d5a582" dependencies = [ "anchor-syn", "anyhow", @@ -84,8 +125,9 @@ dependencies = [ [[package]] name = "anchor-attribute-interface" -version = "0.20.1" -source = "git+https://github.com/project-serum/anchor?tag=v0.20.1#a81ff88d76956533a4ca5ae74d5dec37d7d76b51" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6700a6f5c888a9c33fe8afc0c64fd8575fa28d05446037306d0f96102ae4480" dependencies = [ "anchor-syn", "anyhow", @@ -97,8 +139,9 @@ dependencies = [ [[package]] name = "anchor-attribute-program" -version = "0.20.1" -source = "git+https://github.com/project-serum/anchor?tag=v0.20.1#a81ff88d76956533a4ca5ae74d5dec37d7d76b51" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ad769993b5266714e8939e47fbdede90e5c030333c7522d99a4d4748cf26712" dependencies = [ "anchor-syn", "anyhow", @@ -109,8 +152,9 @@ dependencies = [ [[package]] name = "anchor-attribute-state" -version = "0.20.1" -source = "git+https://github.com/project-serum/anchor?tag=v0.20.1#a81ff88d76956533a4ca5ae74d5dec37d7d76b51" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e677fae4a016a554acdd0e3b7f178d3acafaa7e7ffac6b8690cf4e171f1c116" dependencies = [ "anchor-syn", "anyhow", @@ -121,8 +165,9 @@ dependencies = [ [[package]] name = "anchor-derive-accounts" -version = "0.20.1" -source = "git+https://github.com/project-serum/anchor?tag=v0.20.1#a81ff88d76956533a4ca5ae74d5dec37d7d76b51" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340beef6809d1c3fcc7ae219153d981e95a8a277ff31985bd7050e32645dc9a8" dependencies = [ "anchor-syn", "anyhow", @@ -133,8 +178,9 @@ dependencies = [ [[package]] name = "anchor-lang" -version = "0.20.1" -source = "git+https://github.com/project-serum/anchor?tag=v0.20.1#a81ff88d76956533a4ca5ae74d5dec37d7d76b51" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662ceafe667448ee4199a4be2ee83b6bb76da28566eee5cea05f96ab38255af8" dependencies = [ "anchor-attribute-access-control", "anchor-attribute-account", @@ -146,7 +192,7 @@ dependencies = [ "anchor-attribute-state", "anchor-derive-accounts", "arrayref", - "base64 0.13.0", + "base64 0.13.1", "bincode", "borsh", "bytemuck", @@ -156,8 +202,9 @@ dependencies = [ [[package]] name = "anchor-spl" -version = "0.20.1" -source = "git+https://github.com/project-serum/anchor?tag=v0.20.1#a81ff88d76956533a4ca5ae74d5dec37d7d76b51" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f32390ce8356f54c0f0245ea156f8190717e37285b8bf4f406a613dc4b954cde" dependencies = [ "anchor-lang", "solana-program", @@ -167,8 +214,9 @@ dependencies = [ [[package]] name = "anchor-syn" -version = "0.20.1" -source = "git+https://github.com/project-serum/anchor?tag=v0.20.1#a81ff88d76956533a4ca5ae74d5dec37d7d76b51" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0418bcb5daac3b8cb1b60d8fdb1d468ca36f5509f31fb51179326fae1028fdcc" dependencies = [ "anyhow", "bs58 0.3.1", @@ -178,16 +226,16 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.9.9", "syn", "thiserror", ] [[package]] name = "anyhow" -version = "1.0.44" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" [[package]] name = "arrayref" @@ -201,22 +249,28 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" @@ -226,9 +280,9 @@ checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "bincode" @@ -241,9 +295,9 @@ dependencies = [ [[package]] name = "bit-set" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] @@ -261,17 +315,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "blake3" -version = "1.3.0" +name = "bitmaps" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882e99e4a0cb2ae6cb6e442102e8e6b7131718d94110e64c3e6a34ea9b106f37" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "blake3" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", - "digest 0.10.1", + "digest 0.10.6", ] [[package]] @@ -286,9 +349,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ "generic-array", ] @@ -306,7 +369,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" dependencies = [ "borsh-derive", - "hashbrown", + "hashbrown 0.11.2", ] [[package]] @@ -358,9 +421,9 @@ checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" [[package]] name = "bumpalo" -version = "3.9.1" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "bv" @@ -374,18 +437,18 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.7.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" +checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.0.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e215f8c2f9f79cb53c8335e687ffd07d5bfcb6fe5fc80723762d0be46e7cc54" +checksum = "5fe233b960f12f8007e3db2d136e3cb1c291bfd7396e384ee76025fc1a3932b4" dependencies = [ "proc-macro2", "quote", @@ -400,9 +463,12 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cc" -version = "1.0.71" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" +dependencies = [ + "jobserver", +] [[package]] name = "cfg-if" @@ -410,6 +476,35 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +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" @@ -432,19 +527,62 @@ dependencies = [ [[package]] name = "constant_time_eq" -version = "0.1.5" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +checksum = "f3ad85c1f65dc7b37604eb0e89748faf0b9653065f2a8ef69f96a687ec1e9279" [[package]] name = "cpufeatures" -version = "0.2.1" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" dependencies = [ "libc", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset 0.7.1", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -453,11 +591,12 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-common" -version = "0.1.1" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "typenum", ] [[package]] @@ -471,23 +610,33 @@ dependencies = [ ] [[package]] -name = "curve25519-dalek" -version = "3.2.0" +name = "ctr" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher 0.3.0", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" dependencies = [ "byteorder", "digest 0.9.0", "rand_core 0.5.1", + "serde", "subtle", "zeroize", ] [[package]] name = "darling" -version = "0.13.1" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0d720b8683f8dd83c65155f0530560cba68cd2bf395f6513a483caee57ff7f4" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ "darling_core", "darling_macro", @@ -495,9 +644,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.13.1" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a340f241d2ceed1deb47ae36c4144b2707ec7dd0b649f894cb39bb595986324" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" dependencies = [ "fnv", "ident_case", @@ -509,9 +658,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.13.1" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c41b3b7352feb3211a0d743dc5700a4e3b60f51bd2b368892d1e0f9a95f44b" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core", "quote", @@ -519,15 +668,10 @@ dependencies = [ ] [[package]] -name = "derivative" -version = "2.2.0" +name = "derivation-path" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" [[package]] name = "digest" @@ -540,27 +684,61 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.1" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ - "block-buffer 0.10.0", + "block-buffer 0.10.3", "crypto-common", - "generic-array", "subtle", ] [[package]] -name = "either" -version = "1.6.1" +name = "ed25519" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "ed25519-dalek-bip32" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2be62a4061b872c8c0873ee4fc6f101ce7b889d039f019c5fa2af471a59908" +dependencies = [ + "derivation-path", + "ed25519-dalek", + "hmac 0.12.1", + "sha2 0.10.6", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "env_logger" -version = "0.9.0" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" dependencies = [ "atty", "humantime", @@ -569,6 +747,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + [[package]] name = "feature-probe" version = "0.1.1" @@ -583,9 +770,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "generic-array" -version = "0.14.4" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "serde", "typenum", @@ -607,13 +794,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.3" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -625,6 +814,15 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + [[package]] name = "heck" version = "0.3.3" @@ -643,6 +841,15 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "hex" version = "0.4.3" @@ -659,6 +866,15 @@ dependencies = [ "digest 0.9.0", ] +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.6", +] + [[package]] name = "hmac-drbg" version = "0.3.0" @@ -667,7 +883,7 @@ checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" dependencies = [ "digest 0.9.0", "generic-array", - "hmac", + "hmac 0.8.1", ] [[package]] @@ -682,6 +898,31 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "rayon", + "serde", + "sized-chunks", + "typenum", + "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" @@ -693,33 +934,45 @@ dependencies = [ [[package]] name = "itertools" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "jobserver" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +dependencies = [ + "libc", +] [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] [[package]] name = "keccak" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" +checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +dependencies = [ + "cpufeatures", +] [[package]] name = "lazy_static" @@ -729,9 +982,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.104" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libsecp256k1" @@ -748,7 +1001,7 @@ dependencies = [ "libsecp256k1-gen-genmult", "rand 0.7.3", "serde", - "sha2", + "sha2 0.9.9", "typenum", ] @@ -783,48 +1036,100 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.5" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ + "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.14" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", ] [[package]] name = "memchr" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memmap2" -version = "0.5.2" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe3179b85e1fd8b14447cbebadb75e45a1002f541b925f0bfec366d56a81c56d" +checksum = "4b182332558b18d807c4ce1ca8ca983b34c3ee32765e47b3f0f69b90355cc1dc" dependencies = [ "libc", ] [[package]] -name = "mpl-token-metadata" -version = "1.2.5" +name = "memoffset" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8f072b3bd212fb020389e6d6244e4b35b99708193d063cf3844bac8c182ae8f" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "mpl-token-auth-rules" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69803fbfbc4bb0327de86f49d2639692c7c60276cb87d6cced84bb8189f2000" +dependencies = [ + "borsh", + "mpl-token-metadata-context-derive", + "num-derive", + "num-traits", + "rmp-serde", + "serde", + "shank", + "solana-program", + "solana-zk-token-sdk", + "thiserror", +] + +[[package]] +name = "mpl-token-metadata" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678bb3110abc45bb32e81f80ede7420ce9069d0feb872e7423779bf9a20d1f0" dependencies = [ "arrayref", "borsh", - "mpl-token-vault", + "mpl-token-auth-rules", + "mpl-token-metadata-context-derive", + "mpl-utils", "num-derive", "num-traits", + "shank", "solana-program", "spl-associated-token-account", "spl-token", @@ -832,17 +1137,25 @@ dependencies = [ ] [[package]] -name = "mpl-token-vault" +name = "mpl-token-metadata-context-derive" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12989bc45715b0ee91944855130131479f9c772e198a910c3eb0ea327d5bffc3" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "mpl-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ade4ef15bc06a6033076c4ff28cba9b42521df5ec61211d6f419415ace2746a" +checksum = "7fc48e64c50dba956acb46eec86d6968ef0401ef37031426da479f1f2b592066" dependencies = [ + "arrayref", "borsh", - "num-derive", - "num-traits", "solana-program", "spl-token", - "thiserror", ] [[package]] @@ -857,31 +1170,50 @@ dependencies = [ ] [[package]] -name = "num-traits" -version = "0.2.14" +name = "num-integer" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] [[package]] -name = "num_enum" -version = "0.5.4" +name = "num_cpus" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" dependencies = [ - "derivative", "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.4" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" +checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ - "proc-macro-crate 1.1.0", + "proc-macro-crate 1.2.1", "proc-macro2", "quote", "syn", @@ -889,9 +1221,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.10.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "opaque-debug" @@ -901,34 +1233,74 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "parking_lot" -version = "0.11.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ - "instant", "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.8.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf" dependencies = [ "cfg-if", - "instant", "libc", "redox_syscall", "smallvec", - "winapi", + "windows-sys", +] + +[[package]] +name = "paste" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" + +[[package]] +name = "pbkdf2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" +dependencies = [ + "crypto-mac", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.6", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", ] [[package]] name = "ppv-lite86" -version = "0.2.14" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ca011bd0129ff4ae15cd04c4eef202cadf6c51c21e47aba319b4e0501db741" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" @@ -941,21 +1313,22 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" dependencies = [ + "once_cell", "thiserror", "toml", ] [[package]] name = "proc-macro2" -version = "1.0.30" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -983,7 +1356,7 @@ dependencies = [ "lazy_static", "num-traits", "quick-error 2.0.1", - "rand 0.8.4", + "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", "regex-syntax", @@ -991,6 +1364,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -1005,9 +1387,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.10" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] @@ -1022,19 +1404,18 @@ dependencies = [ "libc", "rand_chacha 0.2.2", "rand_core 0.5.1", - "rand_hc 0.2.0", + "rand_hc", ] [[package]] name = "rand" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", - "rand_core 0.6.3", - "rand_hc 0.3.1", + "rand_core 0.6.4", ] [[package]] @@ -1054,7 +1435,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -1068,11 +1449,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.3", + "getrandom 0.2.8", ] [[package]] @@ -1084,38 +1465,60 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core 0.6.3", -] - [[package]] name = "rand_xorshift" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core 0.6.3", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rayon" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", ] [[package]] name = "redox_syscall" -version = "0.2.10" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.5.4" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" dependencies = [ "aho-corasick", "memchr", @@ -1124,9 +1527,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "remove_dir_all" @@ -1137,6 +1540,34 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.0" @@ -1148,9 +1579,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.5" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" [[package]] name = "rusty-fork" @@ -1166,9 +1597,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" [[package]] name = "scopeguard" @@ -1178,33 +1609,33 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "semver" -version = "1.0.4" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" +checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" [[package]] name = "serde" -version = "1.0.130" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" dependencies = [ "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.5" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16ae07dd2f88a366f15bd0632ba725227018c69a1c8550a927324f8eb8368bb9" +checksum = "718dc5fff5b36f99093fc49b280cfc96ce6fc824317783bff5a1fed0c7a64819" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.130" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", @@ -1213,9 +1644,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.68" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" dependencies = [ "itoa", "ryu", @@ -1224,11 +1655,10 @@ dependencies = [ [[package]] name = "serde_with" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1e6ec4d8950e5b1e894eac0d360742f3b1407a6078a604a731c4b3f49cefbc" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" dependencies = [ - "rustversion", "serde", "serde_json", "serde_with_macros", @@ -1236,9 +1666,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12e47be9471c72889ebafb5e14d5ff930d89ae7a67bbdb5f8abb564f845a927e" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ "darling", "proc-macro2", @@ -1248,9 +1678,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", @@ -1259,6 +1689,17 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.6", +] + [[package]] name = "sha3" version = "0.9.1" @@ -1272,36 +1713,110 @@ dependencies = [ ] [[package]] -name = "smallvec" -version = "1.8.0" +name = "sha3" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" +dependencies = [ + "digest 0.10.6", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "solana-frozen-abi" -version = "1.9.12" +version = "1.14.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae24bb5a815c2796908b131b7e44973d6f2e0e16eb7e67e0f0b5fcd95de0da1" +checksum = "1c39813ee5b249cb8ccb325d3639323eb3616e7bb9a2b1502936d7ea20530097" dependencies = [ + "ahash", + "blake3", + "block-buffer 0.9.0", "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", - "sha2", + "serde_json", + "sha2 0.10.6", "solana-frozen-abi-macro", - "solana-logger", + "subtle", "thiserror", ] [[package]] name = "solana-frozen-abi-macro" -version = "1.9.12" +version = "1.14.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cff377ed697595d3041052940730acefca4f4bfc23eb26bb695595cee737fe5" +checksum = "dad43ac27c4b8d7a3ce0e2cb8642a7e3b8ea5e3c29ecea38045a8518519adccf" dependencies = [ "proc-macro2", "quote", @@ -1311,9 +1826,9 @@ dependencies = [ [[package]] name = "solana-logger" -version = "1.9.12" +version = "1.14.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e3e1c7bf616aa4926d70dd9015077fcccd88c9271cef29c9038e76a2da1ac5d" +checksum = "13a18f8d7490f712a4340998fca2b0d35afcdef671320a0e51f40b537363d592" dependencies = [ "env_logger", "lazy_static", @@ -1322,11 +1837,11 @@ dependencies = [ [[package]] name = "solana-program" -version = "1.9.12" +version = "1.14.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36bead9a6131b48cffc3a3105a6e469592442715528d9d0187e114776533b01c" +checksum = "0dafff676128fe508ab83147b6fb19534fc33f43ec14789da1f1867e9ea06887" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "bincode", "bitflags", "blake3", @@ -1335,39 +1850,96 @@ dependencies = [ "bs58 0.4.0", "bv", "bytemuck", + "cc", "console_error_panic_hook", "console_log", "curve25519-dalek", - "getrandom 0.1.16", + "getrandom 0.2.8", "itertools", "js-sys", "lazy_static", + "libc", "libsecp256k1", "log", + "memoffset 0.6.5", "num-derive", "num-traits", "parking_lot", "rand 0.7.3", + "rand_chacha 0.2.2", "rustc_version", "rustversion", "serde", "serde_bytes", "serde_derive", - "sha2", - "sha3", + "serde_json", + "sha2 0.10.6", + "sha3 0.10.6", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-sdk-macro", + "thiserror", + "tiny-bip39", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "solana-sdk" +version = "1.14.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c702cc57432bc16eab54ad7b5668c2a3cdc72b0f820175972b4857e26ac4f49" +dependencies = [ + "assert_matches", + "base64 0.13.1", + "bincode", + "bitflags", + "borsh", + "bs58 0.4.0", + "bytemuck", + "byteorder", + "chrono", + "derivation-path", + "digest 0.10.6", + "ed25519-dalek", + "ed25519-dalek-bip32", + "generic-array", + "hmac 0.12.1", + "itertools", + "js-sys", + "lazy_static", + "libsecp256k1", + "log", + "memmap2", + "num-derive", + "num-traits", + "pbkdf2 0.11.0", + "qstring", + "rand 0.7.3", + "rand_chacha 0.2.2", + "rustc_version", + "rustversion", + "serde", + "serde_bytes", + "serde_derive", + "serde_json", + "sha2 0.10.6", + "sha3 0.10.6", "solana-frozen-abi", "solana-frozen-abi-macro", "solana-logger", + "solana-program", "solana-sdk-macro", "thiserror", + "uriparse", "wasm-bindgen", ] [[package]] name = "solana-sdk-macro" -version = "1.9.12" +version = "1.14.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "333726c958386c09937aba55ca4390bcfb4621c6120f47610682a94ea4d4533c" +checksum = "f89a14a8f1e7708fe19ee3140125e9d8279945ead74cb09e65c94dd5cf0640c3" dependencies = [ "bs58 0.4.0", "proc-macro2", @@ -1377,22 +1949,69 @@ dependencies = [ ] [[package]] -name = "spl-associated-token-account" -version = "1.0.3" +name = "solana-zk-token-sdk" +version = "1.14.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "393e2240d521c3dd770806bff25c2c00d761ac962be106e14e22dd912007f428" +checksum = "32395c4561673f7b4aa1f3a5b5a654eaa363041f67d92f5d680de72293ef7d1b" dependencies = [ + "aes-gcm-siv", + "arrayref", + "base64 0.13.1", + "bincode", + "bytemuck", + "byteorder", + "cipher 0.4.3", + "curve25519-dalek", + "getrandom 0.1.16", + "itertools", + "lazy_static", + "merlin", + "num-derive", + "num-traits", + "rand 0.7.3", + "serde", + "serde_json", + "sha3 0.9.1", + "solana-program", + "solana-sdk", + "subtle", + "thiserror", + "zeroize", +] + +[[package]] +name = "spl-associated-token-account" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc000f0fdf1f12f99d77d398137c1751345b18c88258ce0f99b7872cf6c9bd6" +dependencies = [ + "assert_matches", + "borsh", + "num-derive", + "num-traits", "solana-program", "spl-token", + "spl-token-2022", + "thiserror", +] + +[[package]] +name = "spl-memo" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0dc6f70db6bacea7ff25870b016a65ba1d1b6013536f08e4fd79a8f9005325" +dependencies = [ + "solana-program", ] [[package]] name = "spl-token" -version = "3.2.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93bfdd5bd7c869cb565c7d7635c4fafe189b988a0bdef81063cd9585c6b8dc01" +checksum = "8e85e168a785e82564160dcb87b2a8e04cee9bfd1f4d488c729d53d6a4bd300d" dependencies = [ "arrayref", + "bytemuck", "num-derive", "num-traits", "num_enum", @@ -1400,6 +2019,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "spl-token-2022" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edb869dbe159b018f17fb9bfa67118c30f232d7f54a73742bc96794dff77ed8" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-program", + "solana-zk-token-sdk", + "spl-memo", + "spl-token", + "thiserror", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -1420,24 +2057,36 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.80" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", "unicode-xid", ] [[package]] name = "tempfile" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if", + "fastrand", "libc", - "rand 0.8.4", "redox_syscall", "remove_dir_all", "winapi", @@ -1445,27 +2094,27 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] [[package]] name = "thiserror" -version = "1.0.30" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", @@ -1473,25 +2122,59 @@ dependencies = [ ] [[package]] -name = "toml" -version = "0.5.8" +name = "tiny-bip39" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d" +dependencies = [ + "anyhow", + "hmac 0.8.1", + "once_cell", + "pbkdf2 0.4.0", + "rand 0.7.3", + "rustc-hash", + "sha2 0.9.9", + "thiserror", + "unicode-normalization", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "toml" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" dependencies = [ "serde", ] [[package]] name = "typenum" -version = "1.14.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "uint" -version = "0.9.1" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" dependencies = [ "byteorder", "crunchy", @@ -1500,22 +2183,57 @@ dependencies = [ ] [[package]] -name = "unicode-segmentation" -version = "1.8.0" +name = "unicode-ident" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" [[package]] name = "unicode-xid" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "uriparse" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" +dependencies = [ + "fnv", + "lazy_static", +] [[package]] name = "version_check" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wait-timeout" @@ -1534,15 +2252,15 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1550,13 +2268,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", "syn", @@ -1565,9 +2283,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1575,9 +2293,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", @@ -1588,15 +2306,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "web-sys" -version = "0.3.55" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" dependencies = [ "js-sys", "wasm-bindgen", @@ -1604,7 +2322,7 @@ dependencies = [ [[package]] name = "whirlpool" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anchor-lang", "anchor-spl", @@ -1652,13 +2370,85 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "yansi" -version = "0.5.0" +name = "windows-sys" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zeroize" -version = "1.4.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf68b08513768deaa790264a7fac27a58cbf2705cfcdc9448362229217d7e970" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/programs/whirlpool/Cargo.toml b/programs/whirlpool/Cargo.toml index 5c6a3cb..88fec26 100644 --- a/programs/whirlpool/Cargo.toml +++ b/programs/whirlpool/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "whirlpool" -version = "0.1.0" +version = "0.2.0" description = "Created with Anchor" edition = "2018" @@ -15,14 +15,14 @@ cpi = ["no-entrypoint"] default = [] [dependencies] -anchor-lang = { git = "https://github.com/project-serum/anchor", tag = "v0.20.1", version = "0.20.1", package = "anchor-lang" } -anchor-spl = { git = "https://github.com/project-serum/anchor", tag = "v0.20.1", version = "0.20.1", package = "anchor-spl" } -spl-token = { version = "3.1.1", features = ["no-entrypoint"] } -solana-program = "1.8.12" +anchor-lang = "0.26" +anchor-spl = "0.26" +spl-token = {version = "3.3", features = ["no-entrypoint"]} +solana-program = "1.14.12" thiserror = "1.0" -uint = { version = "0.9.1", default-features = false } +uint = {version = "0.9.1", default-features = false} borsh = "0.9.1" -mpl-token-metadata = { version = "1.2.5", features = ["no-entrypoint"] } +mpl-token-metadata = { version = "1.7", features = ["no-entrypoint"] } [dev-dependencies] proptest = "1.0" diff --git a/programs/whirlpool/src/constants/mod.rs b/programs/whirlpool/src/constants/mod.rs index 7a14842..e8085c4 100644 --- a/programs/whirlpool/src/constants/mod.rs +++ b/programs/whirlpool/src/constants/mod.rs @@ -1,3 +1,5 @@ +pub mod nft; pub mod test_constants; +pub use nft::*; pub use test_constants::*; diff --git a/programs/whirlpool/src/constants/nft.rs b/programs/whirlpool/src/constants/nft.rs new file mode 100644 index 0000000..9972190 --- /dev/null +++ b/programs/whirlpool/src/constants/nft.rs @@ -0,0 +1,18 @@ +use anchor_lang::prelude::*; + +pub mod whirlpool_nft_update_auth { + use super::*; + declare_id!("3axbTs2z5GBy6usVbNVoqEgZMng3vZvMnAoX29BFfwhr"); +} + +// METADATA_NAME : max 32 bytes +// METADATA_SYMBOL : max 10 bytes +// METADATA_URI : max 200 bytes +pub const WP_METADATA_NAME: &str = "Orca Whirlpool Position"; +pub const WP_METADATA_SYMBOL: &str = "OWP"; +pub const WP_METADATA_URI: &str = "https://arweave.net/E19ZNY2sqMqddm1Wx7mrXPUZ0ZZ5ISizhebb0UsVEws"; + +pub const WPB_METADATA_NAME_PREFIX: &str = "Orca Position Bundle"; +pub const WPB_METADATA_SYMBOL: &str = "OPB"; +pub const WPB_METADATA_URI: &str = + "https://arweave.net/A_Wo8dx2_3lSUwMIi7bdT_sqxi8soghRNAWXXiqXpgE"; diff --git a/programs/whirlpool/src/errors.rs b/programs/whirlpool/src/errors.rs index 56c94bb..bdf11e6 100644 --- a/programs/whirlpool/src/errors.rs +++ b/programs/whirlpool/src/errors.rs @@ -1,8 +1,8 @@ use std::num::TryFromIntError; -use anchor_lang::error; +use anchor_lang::prelude::*; -#[error] +#[error_code] #[derive(PartialEq)] pub enum ErrorCode { #[msg("Enum value could not be converted")] @@ -106,6 +106,15 @@ pub enum ErrorCode { InvalidIntermediaryMint, //0x1799 #[msg("Duplicate two hop pool")] DuplicateTwoHopPool, //0x179a + + #[msg("Bundle index is out of bounds")] + InvalidBundleIndex, //0x179b + #[msg("Position has already been opened")] + BundledPositionAlreadyOpened, //0x179c + #[msg("Position has already been closed")] + BundledPositionAlreadyClosed, //0x179d + #[msg("Unable to delete PositionBundle with open positions")] + PositionBundleNotDeletable, //0x179e } impl From for ErrorCode { diff --git a/programs/whirlpool/src/instructions/close_bundled_position.rs b/programs/whirlpool/src/instructions/close_bundled_position.rs new file mode 100644 index 0000000..5d6be44 --- /dev/null +++ b/programs/whirlpool/src/instructions/close_bundled_position.rs @@ -0,0 +1,56 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::TokenAccount; + +use crate::errors::ErrorCode; +use crate::{state::*, util::verify_position_bundle_authority}; + +#[derive(Accounts)] +#[instruction(bundle_index: u16)] +pub struct CloseBundledPosition<'info> { + #[account(mut, + close = receiver, + seeds = [ + b"bundled_position".as_ref(), + position_bundle.position_bundle_mint.key().as_ref(), + bundle_index.to_string().as_bytes() + ], + bump, + )] + pub bundled_position: Account<'info, Position>, + + #[account(mut)] + pub position_bundle: Box>, + + #[account( + constraint = position_bundle_token_account.mint == bundled_position.position_mint, + constraint = position_bundle_token_account.mint == position_bundle.position_bundle_mint, + constraint = position_bundle_token_account.amount == 1 + )] + pub position_bundle_token_account: Box>, + + pub position_bundle_authority: Signer<'info>, + + /// CHECK: safe, for receiving rent only + #[account(mut)] + pub receiver: UncheckedAccount<'info>, +} + +pub fn handler(ctx: Context, bundle_index: u16) -> Result<()> { + let position_bundle = &mut ctx.accounts.position_bundle; + + // Allow delegation + verify_position_bundle_authority( + &ctx.accounts.position_bundle_token_account, + &ctx.accounts.position_bundle_authority, + )?; + + if !Position::is_position_empty(&ctx.accounts.bundled_position) { + return Err(ErrorCode::ClosePositionNotEmpty.into()); + } + + position_bundle.close_bundled_position(bundle_index)?; + + // Anchor will close the Position account + + Ok(()) +} diff --git a/programs/whirlpool/src/instructions/close_position.rs b/programs/whirlpool/src/instructions/close_position.rs index bba77a8..4562f1c 100644 --- a/programs/whirlpool/src/instructions/close_position.rs +++ b/programs/whirlpool/src/instructions/close_position.rs @@ -9,10 +9,15 @@ use crate::util::{burn_and_close_user_position_token, verify_position_authority} pub struct ClosePosition<'info> { pub position_authority: Signer<'info>, + /// CHECK: safe, for receiving rent only #[account(mut)] pub receiver: UncheckedAccount<'info>, - #[account(mut, close = receiver)] + #[account(mut, + close = receiver, + seeds = [b"position".as_ref(), position_mint.key().as_ref()], + bump, + )] pub position: Account<'info, Position>, #[account(mut, address = position.position_mint)] @@ -27,7 +32,7 @@ pub struct ClosePosition<'info> { pub token_program: Program<'info, Token>, } -pub fn handler(ctx: Context) -> ProgramResult { +pub fn handler(ctx: Context) -> Result<()> { verify_position_authority( &ctx.accounts.position_token_account, &ctx.accounts.position_authority, diff --git a/programs/whirlpool/src/instructions/collect_fees.rs b/programs/whirlpool/src/instructions/collect_fees.rs index dcf3f45..d1cb978 100644 --- a/programs/whirlpool/src/instructions/collect_fees.rs +++ b/programs/whirlpool/src/instructions/collect_fees.rs @@ -34,7 +34,7 @@ pub struct CollectFees<'info> { pub token_program: Program<'info, Token>, } -pub fn handler(ctx: Context) -> ProgramResult { +pub fn handler(ctx: Context) -> Result<()> { verify_position_authority( &ctx.accounts.position_token_account, &ctx.accounts.position_authority, diff --git a/programs/whirlpool/src/instructions/collect_protocol_fees.rs b/programs/whirlpool/src/instructions/collect_protocol_fees.rs index d8eca30..360fb3f 100644 --- a/programs/whirlpool/src/instructions/collect_protocol_fees.rs +++ b/programs/whirlpool/src/instructions/collect_protocol_fees.rs @@ -28,7 +28,7 @@ pub struct CollectProtocolFees<'info> { pub token_program: Program<'info, Token>, } -pub fn handler(ctx: Context) -> ProgramResult { +pub fn handler(ctx: Context) -> Result<()> { let whirlpool = &ctx.accounts.whirlpool; transfer_from_vault_to_owner( diff --git a/programs/whirlpool/src/instructions/collect_reward.rs b/programs/whirlpool/src/instructions/collect_reward.rs index d955f35..88ced66 100644 --- a/programs/whirlpool/src/instructions/collect_reward.rs +++ b/programs/whirlpool/src/instructions/collect_reward.rs @@ -46,7 +46,7 @@ pub struct CollectReward<'info> { /// - `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(ctx: Context, reward_index: u8) -> ProgramResult { +pub fn handler(ctx: Context, reward_index: u8) -> Result<()> { verify_position_authority( &ctx.accounts.position_token_account, &ctx.accounts.position_authority, diff --git a/programs/whirlpool/src/instructions/decrease_liquidity.rs b/programs/whirlpool/src/instructions/decrease_liquidity.rs index 3cbda6f..4ebb3cb 100644 --- a/programs/whirlpool/src/instructions/decrease_liquidity.rs +++ b/programs/whirlpool/src/instructions/decrease_liquidity.rs @@ -17,7 +17,7 @@ pub fn handler( liquidity_amount: u128, token_min_a: u64, token_min_b: u64, -) -> ProgramResult { +) -> Result<()> { verify_position_authority( &ctx.accounts.position_token_account, &ctx.accounts.position_authority, diff --git a/programs/whirlpool/src/instructions/delete_position_bundle.rs b/programs/whirlpool/src/instructions/delete_position_bundle.rs new file mode 100644 index 0000000..068a742 --- /dev/null +++ b/programs/whirlpool/src/instructions/delete_position_bundle.rs @@ -0,0 +1,47 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::{self, Mint, Token, TokenAccount}; + +use crate::errors::ErrorCode; +use crate::state::*; +use crate::util::burn_and_close_position_bundle_token; + +#[derive(Accounts)] +pub struct DeletePositionBundle<'info> { + #[account(mut, close = receiver)] + pub position_bundle: Account<'info, PositionBundle>, + + #[account(mut, address = position_bundle.position_bundle_mint)] + pub position_bundle_mint: Account<'info, Mint>, + + #[account(mut, + constraint = position_bundle_token_account.mint == position_bundle.position_bundle_mint, + constraint = position_bundle_token_account.owner == position_bundle_owner.key(), + constraint = position_bundle_token_account.amount == 1, + )] + pub position_bundle_token_account: Box>, + + pub position_bundle_owner: Signer<'info>, + + /// CHECK: safe, for receiving rent only + #[account(mut)] + pub receiver: UncheckedAccount<'info>, + + #[account(address = token::ID)] + pub token_program: Program<'info, Token>, +} + +pub fn handler(ctx: Context) -> Result<()> { + let position_bundle = &ctx.accounts.position_bundle; + + if !position_bundle.is_deletable() { + return Err(ErrorCode::PositionBundleNotDeletable.into()); + } + + burn_and_close_position_bundle_token( + &ctx.accounts.position_bundle_owner, + &ctx.accounts.receiver, + &ctx.accounts.position_bundle_mint, + &ctx.accounts.position_bundle_token_account, + &ctx.accounts.token_program, + ) +} diff --git a/programs/whirlpool/src/instructions/increase_liquidity.rs b/programs/whirlpool/src/instructions/increase_liquidity.rs index 7e87a2a..f46616c 100644 --- a/programs/whirlpool/src/instructions/increase_liquidity.rs +++ b/programs/whirlpool/src/instructions/increase_liquidity.rs @@ -48,7 +48,7 @@ pub fn handler( liquidity_amount: u128, token_max_a: u64, token_max_b: u64, -) -> ProgramResult { +) -> Result<()> { verify_position_authority( &ctx.accounts.position_token_account, &ctx.accounts.position_authority, diff --git a/programs/whirlpool/src/instructions/initialize_config.rs b/programs/whirlpool/src/instructions/initialize_config.rs index c6c5833..f504da6 100644 --- a/programs/whirlpool/src/instructions/initialize_config.rs +++ b/programs/whirlpool/src/instructions/initialize_config.rs @@ -19,7 +19,7 @@ pub fn handler( collect_protocol_fees_authority: Pubkey, reward_emissions_super_authority: Pubkey, default_protocol_fee_rate: u16, -) -> ProgramResult { +) -> Result<()> { let config = &mut ctx.accounts.config; Ok(config.initialize( diff --git a/programs/whirlpool/src/instructions/initialize_fee_tier.rs b/programs/whirlpool/src/instructions/initialize_fee_tier.rs index 1bab3c5..b56f853 100644 --- a/programs/whirlpool/src/instructions/initialize_fee_tier.rs +++ b/programs/whirlpool/src/instructions/initialize_fee_tier.rs @@ -27,7 +27,7 @@ pub fn handler( ctx: Context, tick_spacing: u16, default_fee_rate: u16, -) -> ProgramResult { +) -> Result<()> { Ok(ctx .accounts .fee_tier diff --git a/programs/whirlpool/src/instructions/initialize_pool.rs b/programs/whirlpool/src/instructions/initialize_pool.rs index c02532b..ff5a583 100644 --- a/programs/whirlpool/src/instructions/initialize_pool.rs +++ b/programs/whirlpool/src/instructions/initialize_pool.rs @@ -21,7 +21,7 @@ pub struct InitializePool<'info> { token_mint_b.key().as_ref(), tick_spacing.to_le_bytes().as_ref() ], - bump = bumps.whirlpool_bump, + bump, payer = funder, space = Whirlpool::LEN)] pub whirlpool: Box>, @@ -49,10 +49,10 @@ pub struct InitializePool<'info> { pub fn handler( ctx: Context, - bumps: WhirlpoolBumps, + _bumps: WhirlpoolBumps, tick_spacing: u16, initial_sqrt_price: u128, -) -> ProgramResult { +) -> Result<()> { let token_mint_a = ctx.accounts.token_mint_a.key(); let token_mint_b = ctx.accounts.token_mint_b.key(); @@ -61,9 +61,12 @@ 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(); + Ok(whirlpool.initialize( whirlpools_config, - bumps.whirlpool_bump, + bump, tick_spacing, initial_sqrt_price, default_fee_rate, diff --git a/programs/whirlpool/src/instructions/initialize_position_bundle.rs b/programs/whirlpool/src/instructions/initialize_position_bundle.rs new file mode 100644 index 0000000..ffd0429 --- /dev/null +++ b/programs/whirlpool/src/instructions/initialize_position_bundle.rs @@ -0,0 +1,63 @@ +use anchor_lang::prelude::*; +use anchor_spl::associated_token::AssociatedToken; +use anchor_spl::token::{self, Mint, Token, TokenAccount}; + +use crate::{state::*, util::mint_position_bundle_token_and_remove_authority}; + +#[derive(Accounts)] +pub struct InitializePositionBundle<'info> { + #[account(init, + payer = funder, + space = PositionBundle::LEN, + seeds = [b"position_bundle".as_ref(), position_bundle_mint.key().as_ref()], + bump, + )] + pub position_bundle: Box>, + + #[account(init, + payer = funder, + mint::authority = position_bundle, // will be removed in the transaction + mint::decimals = 0, + )] + pub position_bundle_mint: Account<'info, Mint>, + + #[account(init, + payer = funder, + associated_token::mint = position_bundle_mint, + associated_token::authority = position_bundle_owner, + )] + pub position_bundle_token_account: Box>, + + /// CHECK: safe, the account that will be the owner of the position bundle can be arbitrary + pub position_bundle_owner: UncheckedAccount<'info>, + + #[account(mut)] + pub funder: Signer<'info>, + + #[account(address = token::ID)] + pub token_program: Program<'info, Token>, + pub system_program: Program<'info, System>, + pub rent: Sysvar<'info, Rent>, + pub associated_token_program: Program<'info, AssociatedToken>, +} + +pub fn handler(ctx: Context) -> Result<()> { + let position_bundle_mint = &ctx.accounts.position_bundle_mint; + let position_bundle = &mut ctx.accounts.position_bundle; + + position_bundle.initialize(position_bundle_mint.key())?; + + let bump = *ctx.bumps.get("position_bundle").unwrap(); + + mint_position_bundle_token_and_remove_authority( + &ctx.accounts.position_bundle, + position_bundle_mint, + &ctx.accounts.position_bundle_token_account, + &ctx.accounts.token_program, + &[ + b"position_bundle".as_ref(), + position_bundle_mint.key().as_ref(), + &[bump], + ], + ) +} diff --git a/programs/whirlpool/src/instructions/initialize_position_bundle_with_metadata.rs b/programs/whirlpool/src/instructions/initialize_position_bundle_with_metadata.rs new file mode 100644 index 0000000..98a9553 --- /dev/null +++ b/programs/whirlpool/src/instructions/initialize_position_bundle_with_metadata.rs @@ -0,0 +1,83 @@ +use anchor_lang::prelude::*; +use anchor_spl::associated_token::AssociatedToken; +use anchor_spl::token::{self, Mint, Token, TokenAccount}; + +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}; + +#[derive(Accounts)] +pub struct InitializePositionBundleWithMetadata<'info> { + #[account(init, + payer = funder, + space = PositionBundle::LEN, + seeds = [b"position_bundle".as_ref(), position_bundle_mint.key().as_ref()], + bump, + )] + pub position_bundle: Box>, + + #[account(init, + payer = funder, + mint::authority = position_bundle, // will be removed in the transaction + mint::decimals = 0, + )] + pub position_bundle_mint: Account<'info, Mint>, + + /// CHECK: checked via the Metadata CPI call + /// https://github.com/metaplex-foundation/metaplex-program-library/blob/773a574c4b34e5b9f248a81306ec24db064e255f/token-metadata/program/src/utils/metadata.rs#L100 + #[account(mut)] + pub position_bundle_metadata: UncheckedAccount<'info>, + + #[account(init, + payer = funder, + associated_token::mint = position_bundle_mint, + associated_token::authority = position_bundle_owner, + )] + pub position_bundle_token_account: Box>, + + /// CHECK: safe, the account that will be the owner of the position bundle can be arbitrary + pub position_bundle_owner: UncheckedAccount<'info>, + + #[account(mut)] + pub funder: Signer<'info>, + + /// CHECK: checked via account constraints + #[account(address = WPB_NFT_UPDATE_AUTH)] + pub metadata_update_auth: UncheckedAccount<'info>, + + #[account(address = token::ID)] + pub token_program: Program<'info, Token>, + pub system_program: Program<'info, System>, + 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 fn handler(ctx: Context) -> Result<()> { + let position_bundle_mint = &ctx.accounts.position_bundle_mint; + let position_bundle = &mut ctx.accounts.position_bundle; + + position_bundle.initialize(position_bundle_mint.key())?; + + let bump = *ctx.bumps.get("position_bundle").unwrap(); + + mint_position_bundle_token_with_metadata_and_remove_authority( + &ctx.accounts.funder, + &ctx.accounts.position_bundle, + position_bundle_mint, + &ctx.accounts.position_bundle_token_account, + &ctx.accounts.position_bundle_metadata, + &ctx.accounts.metadata_update_auth, + &ctx.accounts.metadata_program, + &ctx.accounts.token_program, + &ctx.accounts.system_program, + &ctx.accounts.rent, + &[ + b"position_bundle".as_ref(), + position_bundle_mint.key().as_ref(), + &[bump], + ], + ) +} diff --git a/programs/whirlpool/src/instructions/initialize_reward.rs b/programs/whirlpool/src/instructions/initialize_reward.rs index 06c9b1b..2a6759b 100644 --- a/programs/whirlpool/src/instructions/initialize_reward.rs +++ b/programs/whirlpool/src/instructions/initialize_reward.rs @@ -31,7 +31,7 @@ pub struct InitializeReward<'info> { pub rent: Sysvar<'info, Rent>, } -pub fn handler(ctx: Context, reward_index: u8) -> ProgramResult { +pub fn handler(ctx: Context, reward_index: u8) -> Result<()> { let whirlpool = &mut ctx.accounts.whirlpool; Ok(whirlpool.initialize_reward( diff --git a/programs/whirlpool/src/instructions/initialize_tick_array.rs b/programs/whirlpool/src/instructions/initialize_tick_array.rs index aadd292..ea52018 100644 --- a/programs/whirlpool/src/instructions/initialize_tick_array.rs +++ b/programs/whirlpool/src/instructions/initialize_tick_array.rs @@ -21,7 +21,7 @@ pub struct InitializeTickArray<'info> { pub system_program: Program<'info, System>, } -pub fn handler(ctx: Context, start_tick_index: i32) -> ProgramResult { +pub fn handler(ctx: Context, start_tick_index: i32) -> Result<()> { let mut tick_array = ctx.accounts.tick_array.load_init()?; Ok(tick_array.initialize(&ctx.accounts.whirlpool, start_tick_index)?) } diff --git a/programs/whirlpool/src/instructions/mod.rs b/programs/whirlpool/src/instructions/mod.rs index 200a178..15f1d24 100644 --- a/programs/whirlpool/src/instructions/mod.rs +++ b/programs/whirlpool/src/instructions/mod.rs @@ -1,14 +1,19 @@ +pub mod close_bundled_position; pub mod close_position; pub mod collect_fees; pub mod collect_protocol_fees; pub mod collect_reward; pub mod decrease_liquidity; +pub mod delete_position_bundle; pub mod increase_liquidity; pub mod initialize_config; pub mod initialize_fee_tier; pub mod initialize_pool; +pub mod initialize_position_bundle; +pub mod initialize_position_bundle_with_metadata; pub mod initialize_reward; pub mod initialize_tick_array; +pub mod open_bundled_position; pub mod open_position; pub mod open_position_with_metadata; pub mod set_collect_protocol_fees_authority; @@ -25,17 +30,22 @@ pub mod swap; pub mod two_hop_swap; pub mod update_fees_and_rewards; +pub use close_bundled_position::*; pub use close_position::*; pub use collect_fees::*; pub use collect_protocol_fees::*; pub use collect_reward::*; pub use decrease_liquidity::*; +pub use delete_position_bundle::*; pub use increase_liquidity::*; pub use initialize_config::*; pub use initialize_fee_tier::*; pub use initialize_pool::*; +pub use initialize_position_bundle::*; +pub use initialize_position_bundle_with_metadata::*; pub use initialize_reward::*; pub use initialize_tick_array::*; +pub use open_bundled_position::*; pub use open_position::*; pub use open_position_with_metadata::*; pub use set_collect_protocol_fees_authority::*; diff --git a/programs/whirlpool/src/instructions/open_bundled_position.rs b/programs/whirlpool/src/instructions/open_bundled_position.rs new file mode 100644 index 0000000..dea68c4 --- /dev/null +++ b/programs/whirlpool/src/instructions/open_bundled_position.rs @@ -0,0 +1,67 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::TokenAccount; + +use crate::{state::*, util::verify_position_bundle_authority}; + +#[derive(Accounts)] +#[instruction(bundle_index: u16)] +pub struct OpenBundledPosition<'info> { + #[account(init, + payer = funder, + space = Position::LEN, + seeds = [ + b"bundled_position".as_ref(), + position_bundle.position_bundle_mint.key().as_ref(), + bundle_index.to_string().as_bytes() + ], + bump, + )] + pub bundled_position: Box>, + + #[account(mut)] + pub position_bundle: Box>, + + #[account( + constraint = position_bundle_token_account.mint == position_bundle.position_bundle_mint, + constraint = position_bundle_token_account.amount == 1 + )] + pub position_bundle_token_account: Box>, + + pub position_bundle_authority: Signer<'info>, + + pub whirlpool: Box>, + + #[account(mut)] + pub funder: Signer<'info>, + + pub system_program: Program<'info, System>, + pub rent: Sysvar<'info, Rent>, +} + +pub fn handler( + ctx: Context, + bundle_index: u16, + tick_lower_index: i32, + tick_upper_index: i32, +) -> Result<()> { + let whirlpool = &ctx.accounts.whirlpool; + let position_bundle = &mut ctx.accounts.position_bundle; + let position = &mut ctx.accounts.bundled_position; + + // Allow delegation + verify_position_bundle_authority( + &ctx.accounts.position_bundle_token_account, + &ctx.accounts.position_bundle_authority, + )?; + + position_bundle.open_bundled_position(bundle_index)?; + + position.open_position( + whirlpool, + position_bundle.position_bundle_mint, + tick_lower_index, + tick_upper_index, + )?; + + Ok(()) +} diff --git a/programs/whirlpool/src/instructions/open_position.rs b/programs/whirlpool/src/instructions/open_position.rs index 258089a..8738b83 100644 --- a/programs/whirlpool/src/instructions/open_position.rs +++ b/programs/whirlpool/src/instructions/open_position.rs @@ -10,19 +10,19 @@ pub struct OpenPosition<'info> { #[account(mut)] pub funder: Signer<'info>, + /// CHECK: safe, the account that will be the owner of the position can be arbitrary pub owner: UncheckedAccount<'info>, #[account(init, payer = funder, space = Position::LEN, seeds = [b"position".as_ref(), position_mint.key().as_ref()], - bump = bumps.position_bump, + bump, )] pub position: Box>, #[account(init, payer = funder, - space = Mint::LEN, mint::authority = whirlpool, mint::decimals = 0, )] @@ -52,7 +52,7 @@ pub fn handler( _bumps: OpenPositionBumps, tick_lower_index: i32, tick_upper_index: i32, -) -> ProgramResult { +) -> Result<()> { let whirlpool = &ctx.accounts.whirlpool; let position_mint = &ctx.accounts.position_mint; let position = &mut ctx.accounts.position; diff --git a/programs/whirlpool/src/instructions/open_position_with_metadata.rs b/programs/whirlpool/src/instructions/open_position_with_metadata.rs index f39c742..7505d7c 100644 --- a/programs/whirlpool/src/instructions/open_position_with_metadata.rs +++ b/programs/whirlpool/src/instructions/open_position_with_metadata.rs @@ -4,11 +4,7 @@ use anchor_spl::token::{self, Mint, Token, TokenAccount}; use crate::{state::*, util::mint_position_token_with_metadata_and_remove_authority}; -use whirlpool_nft_update_auth::ID as WP_NFT_UPDATE_AUTH; -mod whirlpool_nft_update_auth { - use super::*; - declare_id!("3axbTs2z5GBy6usVbNVoqEgZMng3vZvMnAoX29BFfwhr"); -} +use crate::constants::nft::whirlpool_nft_update_auth::ID as WP_NFT_UPDATE_AUTH; #[derive(Accounts)] #[instruction(bumps: OpenPositionWithMetadataBumps)] @@ -16,19 +12,19 @@ pub struct OpenPositionWithMetadata<'info> { #[account(mut)] pub funder: Signer<'info>, + /// CHECK: safe, the account that will be the owner of the position can be arbitrary pub owner: UncheckedAccount<'info>, #[account(init, payer = funder, space = Position::LEN, seeds = [b"position".as_ref(), position_mint.key().as_ref()], - bump = bumps.position_bump, + bump, )] pub position: Box>, #[account(init, payer = funder, - space = Mint::LEN, mint::authority = whirlpool, mint::decimals = 0, )] @@ -71,7 +67,7 @@ pub fn handler( _bumps: OpenPositionWithMetadataBumps, tick_lower_index: i32, tick_upper_index: i32, -) -> ProgramResult { +) -> Result<()> { let whirlpool = &ctx.accounts.whirlpool; let position_mint = &ctx.accounts.position_mint; let position = &mut ctx.accounts.position; diff --git a/programs/whirlpool/src/instructions/set_collect_protocol_fees_authority.rs b/programs/whirlpool/src/instructions/set_collect_protocol_fees_authority.rs index bf35e03..0414832 100644 --- a/programs/whirlpool/src/instructions/set_collect_protocol_fees_authority.rs +++ b/programs/whirlpool/src/instructions/set_collect_protocol_fees_authority.rs @@ -10,10 +10,11 @@ pub struct SetCollectProtocolFeesAuthority<'info> { #[account(address = whirlpools_config.collect_protocol_fees_authority)] pub collect_protocol_fees_authority: Signer<'info>, + /// CHECK: safe, the account that will be new authority can be arbitrary pub new_collect_protocol_fees_authority: UncheckedAccount<'info>, } -pub fn handler(ctx: Context) -> ProgramResult { +pub fn handler(ctx: Context) -> Result<()> { Ok(ctx .accounts .whirlpools_config diff --git a/programs/whirlpool/src/instructions/set_default_fee_rate.rs b/programs/whirlpool/src/instructions/set_default_fee_rate.rs index 7b2c554..e22687d 100644 --- a/programs/whirlpool/src/instructions/set_default_fee_rate.rs +++ b/programs/whirlpool/src/instructions/set_default_fee_rate.rs @@ -16,7 +16,7 @@ pub struct SetDefaultFeeRate<'info> { /* Updates the default fee rate on a FeeTier object. */ -pub fn handler(ctx: Context, default_fee_rate: u16) -> ProgramResult { +pub fn handler(ctx: Context, default_fee_rate: u16) -> Result<()> { Ok(ctx .accounts .fee_tier diff --git a/programs/whirlpool/src/instructions/set_default_protocol_fee_rate.rs b/programs/whirlpool/src/instructions/set_default_protocol_fee_rate.rs index ed50fdb..4b1843d 100644 --- a/programs/whirlpool/src/instructions/set_default_protocol_fee_rate.rs +++ b/programs/whirlpool/src/instructions/set_default_protocol_fee_rate.rs @@ -14,7 +14,7 @@ pub struct SetDefaultProtocolFeeRate<'info> { pub fn handler( ctx: Context, default_protocol_fee_rate: u16, -) -> ProgramResult { +) -> Result<()> { Ok(ctx .accounts .whirlpools_config diff --git a/programs/whirlpool/src/instructions/set_fee_authority.rs b/programs/whirlpool/src/instructions/set_fee_authority.rs index 3da7fd4..ac5c9ab 100644 --- a/programs/whirlpool/src/instructions/set_fee_authority.rs +++ b/programs/whirlpool/src/instructions/set_fee_authority.rs @@ -10,11 +10,12 @@ pub struct SetFeeAuthority<'info> { #[account(address = whirlpools_config.fee_authority)] pub fee_authority: Signer<'info>, + /// CHECK: safe, the account that will be new authority can be arbitrary pub new_fee_authority: UncheckedAccount<'info>, } /// Set the fee authority. Only the current fee authority has permission to invoke this instruction. -pub fn handler(ctx: Context) -> ProgramResult { +pub fn handler(ctx: Context) -> Result<()> { Ok(ctx .accounts .whirlpools_config diff --git a/programs/whirlpool/src/instructions/set_fee_rate.rs b/programs/whirlpool/src/instructions/set_fee_rate.rs index 00609d5..2846175 100644 --- a/programs/whirlpool/src/instructions/set_fee_rate.rs +++ b/programs/whirlpool/src/instructions/set_fee_rate.rs @@ -13,6 +13,6 @@ pub struct SetFeeRate<'info> { pub fee_authority: Signer<'info>, } -pub fn handler(ctx: Context, fee_rate: u16) -> ProgramResult { +pub fn handler(ctx: Context, fee_rate: u16) -> Result<()> { Ok(ctx.accounts.whirlpool.update_fee_rate(fee_rate)?) } diff --git a/programs/whirlpool/src/instructions/set_protocol_fee_rate.rs b/programs/whirlpool/src/instructions/set_protocol_fee_rate.rs index e8ea032..4e2ac4a 100644 --- a/programs/whirlpool/src/instructions/set_protocol_fee_rate.rs +++ b/programs/whirlpool/src/instructions/set_protocol_fee_rate.rs @@ -13,7 +13,7 @@ pub struct SetProtocolFeeRate<'info> { pub fee_authority: Signer<'info>, } -pub fn handler(ctx: Context, protocol_fee_rate: u16) -> ProgramResult { +pub fn handler(ctx: Context, protocol_fee_rate: u16) -> Result<()> { Ok(ctx .accounts .whirlpool diff --git a/programs/whirlpool/src/instructions/set_reward_authority.rs b/programs/whirlpool/src/instructions/set_reward_authority.rs index 8f4bd1c..5a3b89d 100644 --- a/programs/whirlpool/src/instructions/set_reward_authority.rs +++ b/programs/whirlpool/src/instructions/set_reward_authority.rs @@ -11,10 +11,11 @@ pub struct SetRewardAuthority<'info> { #[account(address = whirlpool.reward_infos[reward_index as usize].authority)] pub reward_authority: Signer<'info>, + /// CHECK: safe, the account that will be new authority can be arbitrary pub new_reward_authority: UncheckedAccount<'info>, } -pub fn handler(ctx: Context, reward_index: u8) -> ProgramResult { +pub fn handler(ctx: Context, reward_index: u8) -> Result<()> { Ok(ctx.accounts.whirlpool.update_reward_authority( reward_index as usize, ctx.accounts.new_reward_authority.key(), diff --git a/programs/whirlpool/src/instructions/set_reward_authority_by_super_authority.rs b/programs/whirlpool/src/instructions/set_reward_authority_by_super_authority.rs index addd683..448e311 100644 --- a/programs/whirlpool/src/instructions/set_reward_authority_by_super_authority.rs +++ b/programs/whirlpool/src/instructions/set_reward_authority_by_super_authority.rs @@ -13,15 +13,13 @@ pub struct SetRewardAuthorityBySuperAuthority<'info> { #[account(address = whirlpools_config.reward_emissions_super_authority)] pub reward_emissions_super_authority: Signer<'info>, + /// CHECK: safe, the account that will be new authority can be arbitrary pub new_reward_authority: UncheckedAccount<'info>, } /// Set the whirlpool reward authority at the provided `reward_index`. /// Only the current reward emissions super authority has permission to invoke this instruction. -pub fn handler( - ctx: Context, - reward_index: u8, -) -> ProgramResult { +pub fn handler(ctx: Context, reward_index: u8) -> Result<()> { Ok(ctx.accounts.whirlpool.update_reward_authority( reward_index as usize, ctx.accounts.new_reward_authority.key(), diff --git a/programs/whirlpool/src/instructions/set_reward_emissions.rs b/programs/whirlpool/src/instructions/set_reward_emissions.rs index 50f4e12..f5723ef 100644 --- a/programs/whirlpool/src/instructions/set_reward_emissions.rs +++ b/programs/whirlpool/src/instructions/set_reward_emissions.rs @@ -26,7 +26,7 @@ pub fn handler( ctx: Context, reward_index: u8, emissions_per_second_x64: u128, -) -> ProgramResult { +) -> Result<()> { let whirlpool = &ctx.accounts.whirlpool; let reward_vault = &ctx.accounts.reward_vault; diff --git a/programs/whirlpool/src/instructions/set_reward_emissions_super_authority.rs b/programs/whirlpool/src/instructions/set_reward_emissions_super_authority.rs index b64312b..1a23e05 100644 --- a/programs/whirlpool/src/instructions/set_reward_emissions_super_authority.rs +++ b/programs/whirlpool/src/instructions/set_reward_emissions_super_authority.rs @@ -10,10 +10,11 @@ pub struct SetRewardEmissionsSuperAuthority<'info> { #[account(address = whirlpools_config.reward_emissions_super_authority)] pub reward_emissions_super_authority: Signer<'info>, + /// CHECK: safe, the account that will be new authority can be arbitrary pub new_reward_emissions_super_authority: UncheckedAccount<'info>, } -pub fn handler(ctx: Context) -> ProgramResult { +pub fn handler(ctx: Context) -> Result<()> { Ok(ctx .accounts .whirlpools_config diff --git a/programs/whirlpool/src/instructions/swap.rs b/programs/whirlpool/src/instructions/swap.rs index 60049a7..6466365 100644 --- a/programs/whirlpool/src/instructions/swap.rs +++ b/programs/whirlpool/src/instructions/swap.rs @@ -5,11 +5,7 @@ use crate::{ errors::ErrorCode, manager::swap_manager::*, state::{TickArray, Whirlpool}, - util::{ - to_timestamp_u64, - SwapTickSequence, - update_and_swap_whirlpool - }, + util::{to_timestamp_u64, update_and_swap_whirlpool, SwapTickSequence}, }; #[derive(Accounts)] @@ -42,7 +38,7 @@ pub struct Swap<'info> { pub tick_array_2: AccountLoader<'info, TickArray>, #[account(seeds = [b"oracle", whirlpool.key().as_ref()],bump)] - /// Oracle is currently unused and will be enabled on subsequent updates + /// CHECK: Oracle is currently unused and will be enabled on subsequent updates pub oracle: UncheckedAccount<'info>, } @@ -53,7 +49,7 @@ pub fn handler( sqrt_price_limit: u128, amount_specified_is_input: bool, a_to_b: bool, // Zero for one -) -> ProgramResult { +) -> Result<()> { let whirlpool = &mut ctx.accounts.whirlpool; let clock = Clock::get()?; // Update the global reward growth which increases as a function of time. diff --git a/programs/whirlpool/src/instructions/two_hop_swap.rs b/programs/whirlpool/src/instructions/two_hop_swap.rs index 50659ac..a8faad0 100644 --- a/programs/whirlpool/src/instructions/two_hop_swap.rs +++ b/programs/whirlpool/src/instructions/two_hop_swap.rs @@ -5,11 +5,7 @@ use crate::{ errors::ErrorCode, manager::swap_manager::*, state::{TickArray, Whirlpool}, - util::{ - to_timestamp_u64, - SwapTickSequence, - update_and_swap_whirlpool, - }, + util::{to_timestamp_u64, update_and_swap_whirlpool, SwapTickSequence}, }; #[derive(Accounts)] @@ -64,11 +60,11 @@ pub struct TwoHopSwap<'info> { pub tick_array_two_2: AccountLoader<'info, TickArray>, #[account(seeds = [b"oracle", whirlpool_one.key().as_ref()],bump)] - /// Oracle is currently unused and will be enabled on subsequent updates + /// CHECK: Oracle is currently unused and will be enabled on subsequent updates pub oracle_one: UncheckedAccount<'info>, #[account(seeds = [b"oracle", whirlpool_two.key().as_ref()],bump)] - /// Oracle is currently unused and will be enabled on subsequent updates + /// CHECK: Oracle is currently unused and will be enabled on subsequent updates pub oracle_two: UncheckedAccount<'info>, } @@ -81,7 +77,7 @@ pub fn handler( a_to_b_two: bool, sqrt_price_limit_one: u128, sqrt_price_limit_two: u128, -) -> ProgramResult { +) -> 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)?; @@ -115,7 +111,7 @@ pub fn handler( ctx.accounts.tick_array_one_2.load_mut().ok(), ); - let mut swap_tick_sequence_two= SwapTickSequence::new( + 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(), diff --git a/programs/whirlpool/src/instructions/update_fees_and_rewards.rs b/programs/whirlpool/src/instructions/update_fees_and_rewards.rs index ceda291..9d822d7 100644 --- a/programs/whirlpool/src/instructions/update_fees_and_rewards.rs +++ b/programs/whirlpool/src/instructions/update_fees_and_rewards.rs @@ -1,5 +1,3 @@ -use anchor_lang::prelude::ProgramResult; - use anchor_lang::prelude::*; use crate::{ @@ -20,7 +18,7 @@ pub struct UpdateFeesAndRewards<'info> { pub tick_array_upper: AccountLoader<'info, TickArray>, } -pub fn handler(ctx: Context) -> ProgramResult { +pub fn handler(ctx: Context) -> Result<()> { let whirlpool = &mut ctx.accounts.whirlpool; let position = &mut ctx.accounts.position; let clock = Clock::get()?; diff --git a/programs/whirlpool/src/lib.rs b/programs/whirlpool/src/lib.rs index d2d029b..2c6864e 100644 --- a/programs/whirlpool/src/lib.rs +++ b/programs/whirlpool/src/lib.rs @@ -39,7 +39,7 @@ pub mod whirlpool { collect_protocol_fees_authority: Pubkey, reward_emissions_super_authority: Pubkey, default_protocol_fee_rate: u16, - ) -> ProgramResult { + ) -> Result<()> { return instructions::initialize_config::handler( ctx, fee_authority, @@ -66,7 +66,7 @@ pub mod whirlpool { bumps: WhirlpoolBumps, tick_spacing: u16, initial_sqrt_price: u128, - ) -> ProgramResult { + ) -> Result<()> { return instructions::initialize_pool::handler( ctx, bumps, @@ -87,7 +87,7 @@ pub mod whirlpool { pub fn initialize_tick_array( ctx: Context, start_tick_index: i32, - ) -> ProgramResult { + ) -> Result<()> { return instructions::initialize_tick_array::handler(ctx, start_tick_index); } @@ -107,7 +107,7 @@ pub mod whirlpool { ctx: Context, tick_spacing: u16, default_fee_rate: u16, - ) -> ProgramResult { + ) -> Result<()> { return instructions::initialize_fee_tier::handler(ctx, tick_spacing, default_fee_rate); } @@ -124,7 +124,7 @@ pub mod whirlpool { /// - `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(ctx: Context, reward_index: u8) -> ProgramResult { + pub fn initialize_reward(ctx: Context, reward_index: u8) -> Result<()> { return instructions::initialize_reward::handler(ctx, reward_index); } @@ -149,7 +149,7 @@ pub mod whirlpool { ctx: Context, reward_index: u8, emissions_per_second_x64: u128, - ) -> ProgramResult { + ) -> Result<()> { return instructions::set_reward_emissions::handler( ctx, reward_index, @@ -172,7 +172,7 @@ pub mod whirlpool { bumps: OpenPositionBumps, tick_lower_index: i32, tick_upper_index: i32, - ) -> ProgramResult { + ) -> Result<()> { return instructions::open_position::handler( ctx, bumps, @@ -197,7 +197,7 @@ pub mod whirlpool { bumps: OpenPositionWithMetadataBumps, tick_lower_index: i32, tick_upper_index: i32, - ) -> ProgramResult { + ) -> Result<()> { return instructions::open_position_with_metadata::handler( ctx, bumps, @@ -225,7 +225,7 @@ pub mod whirlpool { liquidity_amount: u128, token_max_a: u64, token_max_b: u64, - ) -> ProgramResult { + ) -> Result<()> { return instructions::increase_liquidity::handler( ctx, liquidity_amount, @@ -253,7 +253,7 @@ pub mod whirlpool { liquidity_amount: u128, token_min_a: u64, token_min_b: u64, - ) -> ProgramResult { + ) -> Result<()> { return instructions::decrease_liquidity::handler( ctx, liquidity_amount, @@ -267,7 +267,7 @@ pub mod whirlpool { /// #### 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. - pub fn update_fees_and_rewards(ctx: Context) -> ProgramResult { + pub fn update_fees_and_rewards(ctx: Context) -> Result<()> { return instructions::update_fees_and_rewards::handler(ctx); } @@ -275,7 +275,7 @@ pub mod whirlpool { /// /// ### Authority /// - `position_authority` - authority that owns the token corresponding to this desired position. - pub fn collect_fees(ctx: Context) -> ProgramResult { + pub fn collect_fees(ctx: Context) -> Result<()> { return instructions::collect_fees::handler(ctx); } @@ -283,7 +283,7 @@ pub mod whirlpool { /// /// ### Authority /// - `position_authority` - authority that owns the token corresponding to this desired position. - pub fn collect_reward(ctx: Context, reward_index: u8) -> ProgramResult { + pub fn collect_reward(ctx: Context, reward_index: u8) -> Result<()> { return instructions::collect_reward::handler(ctx, reward_index); } @@ -291,7 +291,7 @@ pub mod whirlpool { /// /// ### Authority /// - `collect_protocol_fees_authority` - assigned authority in the WhirlpoolConfig that can collect protocol fees - pub fn collect_protocol_fees(ctx: Context) -> ProgramResult { + pub fn collect_protocol_fees(ctx: Context) -> Result<()> { return instructions::collect_protocol_fees::handler(ctx); } @@ -323,7 +323,7 @@ pub mod whirlpool { sqrt_price_limit: u128, amount_specified_is_input: bool, a_to_b: bool, - ) -> ProgramResult { + ) -> Result<()> { return instructions::swap::handler( ctx, amount, @@ -341,7 +341,7 @@ pub mod whirlpool { /// /// #### Special Errors /// - `ClosePositionNotEmpty` - The provided position account is not empty. - pub fn close_position(ctx: Context) -> ProgramResult { + pub fn close_position(ctx: Context) -> Result<()> { return instructions::close_position::handler(ctx); } @@ -360,7 +360,7 @@ pub mod whirlpool { pub fn set_default_fee_rate( ctx: Context, default_fee_rate: u16, - ) -> ProgramResult { + ) -> Result<()> { return instructions::set_default_fee_rate::handler(ctx, default_fee_rate); } @@ -379,7 +379,7 @@ pub mod whirlpool { pub fn set_default_protocol_fee_rate( ctx: Context, default_protocol_fee_rate: u16, - ) -> ProgramResult { + ) -> Result<()> { return instructions::set_default_protocol_fee_rate::handler( ctx, default_protocol_fee_rate, @@ -398,7 +398,7 @@ pub mod whirlpool { /// /// #### Special Errors /// - `FeeRateMaxExceeded` - If the provided fee_rate exceeds MAX_FEE_RATE. - pub fn set_fee_rate(ctx: Context, fee_rate: u16) -> ProgramResult { + pub fn set_fee_rate(ctx: Context, fee_rate: u16) -> Result<()> { return instructions::set_fee_rate::handler(ctx, fee_rate); } @@ -417,7 +417,7 @@ pub mod whirlpool { pub fn set_protocol_fee_rate( ctx: Context, protocol_fee_rate: u16, - ) -> ProgramResult { + ) -> Result<()> { return instructions::set_protocol_fee_rate::handler(ctx, protocol_fee_rate); } @@ -428,7 +428,7 @@ pub mod whirlpool { /// /// ### Authority /// - "fee_authority" - Set authority that can modify pool fees in the WhirlpoolConfig - pub fn set_fee_authority(ctx: Context) -> ProgramResult { + pub fn set_fee_authority(ctx: Context) -> Result<()> { return instructions::set_fee_authority::handler(ctx); } @@ -439,7 +439,7 @@ pub mod whirlpool { /// - "fee_authority" - Set authority that can collect protocol fees in the WhirlpoolConfig pub fn set_collect_protocol_fees_authority( ctx: Context, - ) -> ProgramResult { + ) -> Result<()> { return instructions::set_collect_protocol_fees_authority::handler(ctx); } @@ -453,10 +453,7 @@ pub mod whirlpool { /// - `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_authority( - ctx: Context, - reward_index: u8, - ) -> ProgramResult { + pub fn set_reward_authority(ctx: Context, reward_index: u8) -> Result<()> { return instructions::set_reward_authority::handler(ctx, reward_index); } @@ -473,7 +470,7 @@ pub mod whirlpool { pub fn set_reward_authority_by_super_authority( ctx: Context, reward_index: u8, - ) -> ProgramResult { + ) -> Result<()> { return instructions::set_reward_authority_by_super_authority::handler(ctx, reward_index); } @@ -485,7 +482,7 @@ pub mod whirlpool { /// - "reward_emissions_super_authority" - Set authority that can control reward authorities for all pools in this config space. pub fn set_reward_emissions_super_authority( ctx: Context, - ) -> ProgramResult { + ) -> Result<()> { return instructions::set_reward_emissions_super_authority::handler(ctx); } @@ -523,7 +520,7 @@ pub mod whirlpool { a_to_b_two: bool, sqrt_price_limit_one: u128, sqrt_price_limit_two: u128, - ) -> ProgramResult { + ) -> Result<()> { return instructions::two_hop_swap::handler( ctx, amount, @@ -535,4 +532,78 @@ pub mod whirlpool { sqrt_price_limit_two, ); } + + /// Initializes a PositionBundle account that bundles several positions. + /// A unique token will be minted to represent the position bundle in the users wallet. + pub fn initialize_position_bundle(ctx: Context) -> Result<()> { + return instructions::initialize_position_bundle::handler(ctx); + } + + /// 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. + pub fn initialize_position_bundle_with_metadata( + ctx: Context, + ) -> Result<()> { + return instructions::initialize_position_bundle_with_metadata::handler(ctx); + } + + /// 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. + pub fn delete_position_bundle(ctx: Context) -> Result<()> { + return instructions::delete_position_bundle::handler(ctx); + } + + /// 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. + pub fn open_bundled_position( + ctx: Context, + bundle_index: u16, + tick_lower_index: i32, + tick_upper_index: i32, + ) -> Result<()> { + return instructions::open_bundled_position::handler( + ctx, + bundle_index, + tick_lower_index, + tick_upper_index, + ); + } + + /// 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. + pub fn close_bundled_position( + ctx: Context, + bundle_index: u16, + ) -> Result<()> { + return instructions::close_bundled_position::handler(ctx, bundle_index); + } } diff --git a/programs/whirlpool/src/manager/liquidity_manager.rs b/programs/whirlpool/src/manager/liquidity_manager.rs index 5e60793..11b8fa9 100644 --- a/programs/whirlpool/src/manager/liquidity_manager.rs +++ b/programs/whirlpool/src/manager/liquidity_manager.rs @@ -10,7 +10,7 @@ use crate::{ math::{get_amount_delta_a, get_amount_delta_b, sqrt_price_from_tick_index}, state::*, }; -use anchor_lang::prelude::{AccountLoader, ProgramError}; +use anchor_lang::prelude::{AccountLoader, *}; #[derive(Debug)] pub struct ModifyLiquidityUpdate { @@ -31,7 +31,7 @@ pub fn calculate_modify_liquidity<'info>( tick_array_upper: &AccountLoader<'info, TickArray>, liquidity_delta: i128, timestamp: u64, -) -> Result { +) -> Result { let tick_array_lower = tick_array_lower.load()?; let tick_lower = tick_array_lower.get_tick(position.tick_lower_index, whirlpool.tick_spacing)?; @@ -58,7 +58,7 @@ pub fn calculate_fee_and_reward_growths<'info>( tick_array_lower: &AccountLoader<'info, TickArray>, tick_array_upper: &AccountLoader<'info, TickArray>, timestamp: u64, -) -> Result<(PositionUpdate, [WhirlpoolRewardInfo; NUM_REWARDS]), ProgramError> { +) -> Result<(PositionUpdate, [WhirlpoolRewardInfo; NUM_REWARDS])> { let tick_array_lower = tick_array_lower.load()?; let tick_lower = tick_array_lower.get_tick(position.tick_lower_index, whirlpool.tick_spacing)?; @@ -92,7 +92,7 @@ fn _calculate_modify_liquidity( tick_upper_index: i32, liquidity_delta: i128, timestamp: u64, -) -> Result { +) -> Result { // Disallow only updating position fee and reward growth when position has zero liquidity if liquidity_delta == 0 && position.liquidity == 0 { return Err(ErrorCode::LiquidityZero.into()); @@ -170,7 +170,7 @@ pub fn calculate_liquidity_token_deltas( sqrt_price: u128, position: &Position, liquidity_delta: i128, -) -> Result<(u64, u64), ErrorCode> { +) -> Result<(u64, u64)> { if liquidity_delta == 0 { return Err(ErrorCode::LiquidityZero.into()); } @@ -206,7 +206,7 @@ pub fn sync_modify_liquidity_values<'info>( tick_array_upper: &AccountLoader<'info, TickArray>, modify_liquidity_update: ModifyLiquidityUpdate, reward_last_updated_timestamp: u64, -) -> Result<(), ProgramError> { +) -> Result<()> { position.update(&modify_liquidity_update.position_update); tick_array_lower.load_mut()?.update_tick( diff --git a/programs/whirlpool/src/manager/swap_manager.rs b/programs/whirlpool/src/manager/swap_manager.rs index c1b02b2..eaadeb1 100644 --- a/programs/whirlpool/src/manager/swap_manager.rs +++ b/programs/whirlpool/src/manager/swap_manager.rs @@ -7,6 +7,7 @@ use crate::{ state::*, util::SwapTickSequence, }; +use anchor_lang::prelude::*; use std::convert::TryInto; #[derive(Debug)] @@ -29,7 +30,7 @@ pub fn swap( amount_specified_is_input: bool, a_to_b: bool, timestamp: u64, -) -> Result { +) -> Result { if sqrt_price_limit < MIN_SQRT_PRICE_X64 || sqrt_price_limit > MAX_SQRT_PRICE_X64 { return Err(ErrorCode::SqrtPriceOutOfBounds.into()); } @@ -88,8 +89,8 @@ pub fn swap( amount_remaining = amount_remaining .checked_sub(swap_computation.amount_in) .ok_or(ErrorCode::AmountRemainingOverflow)?; - amount_remaining = amount_remaining. - checked_sub(swap_computation.fee_amount) + amount_remaining = amount_remaining + .checked_sub(swap_computation.fee_amount) .ok_or(ErrorCode::AmountRemainingOverflow)?; amount_calculated = amount_calculated @@ -233,7 +234,7 @@ fn calculate_update( fee_growth_global_a: u128, fee_growth_global_b: u128, reward_infos: &[WhirlpoolRewardInfo; NUM_REWARDS], -) -> Result<(TickUpdate, u128), ErrorCode> { +) -> Result<(TickUpdate, u128)> { // Use updated fee_growth for crossing tick // Use -liquidity_net if going left, +liquidity_net going right let signed_liquidity_net = if a_to_b { @@ -2499,7 +2500,6 @@ mod swap_error_tests { swap_test_info.run(&mut tick_sequence, 100); } - #[test] #[should_panic(expected = "AmountCalcOverflow")] // Swapping at high liquidity/price can lead to an amount calculated @@ -2511,7 +2511,8 @@ mod swap_error_tests { // Use filled arrays to minimize the the overflow from calculations, rather than accumulation let array_1_ticks: Vec = build_filled_tick_array(439296, TS_128); let array_2_ticks: Vec = build_filled_tick_array(439296 - 88 * 128, TS_128); - let array_3_ticks: Vec = build_filled_tick_array(439296 - 2 * 88 * 128, TS_128); + let array_3_ticks: Vec = + build_filled_tick_array(439296 - 2 * 88 * 128, TS_128); let swap_test_info = SwapTestFixture::new(SwapTestFixtureInfo { tick_spacing: TS_128, liquidity: (u32::MAX as u128) << 2, @@ -2533,5 +2534,4 @@ mod swap_error_tests { ); swap_test_info.run(&mut tick_sequence, 100); } - } diff --git a/programs/whirlpool/src/math/bit_math.rs b/programs/whirlpool/src/math/bit_math.rs index 9e238a6..b6c844b 100644 --- a/programs/whirlpool/src/math/bit_math.rs +++ b/programs/whirlpool/src/math/bit_math.rs @@ -57,11 +57,7 @@ pub fn checked_mul_shift_right_round_up_if( return Err(ErrorCode::MultiplicationOverflow); } - Ok(if should_round { - result + 1 - } else { - result - }) + Ok(if should_round { result + 1 } else { result }) } pub fn div_round_up(n: u128, d: u128) -> Result { @@ -112,8 +108,8 @@ mod fuzz_tests { assert!(rounded.is_err()); } else { let unrounded = n / d; - let div_unrounded = div_round_up_if(n, d, false)?; - let diff = rounded? - unrounded; + let div_unrounded = div_round_up_if(n, d, false).unwrap(); + let diff = rounded.unwrap() - unrounded; assert!(unrounded == div_unrounded); assert!(diff <= 1); assert!((diff == 1) == (n % d > 0)); @@ -143,7 +139,7 @@ mod fuzz_tests { let other_remainder = other_dividend % other_divisor; let unrounded = div_round_up_if_u256(dividend, divisor, false); - assert!(unrounded? == other_quotient.try_into_u128()?); + assert!(unrounded.unwrap() == other_quotient.try_into_u128().unwrap()); let diff = rounded.unwrap() - unrounded.unwrap(); assert!(diff <= 1); @@ -166,7 +162,7 @@ mod fuzz_tests { let other_d = U256::from(d); let other_result = other_p / other_d; - let unrounded = checked_mul_div_round_up_if(n0, n1, d, false)?; + let unrounded = checked_mul_div_round_up_if(n0, n1, d, false).unwrap(); assert!(U256::from(unrounded) == other_result); let diff = U256::from(result.unwrap()) - other_result; @@ -182,12 +178,12 @@ mod fuzz_tests { if n0.checked_mul(n1).is_none() { assert!(result.is_err()); } else { - let p = (U256::from(n0) * U256::from(n1)).try_into_u128()?; + let p = (U256::from(n0) * U256::from(n1)).try_into_u128().unwrap(); let i = (p >> 64) as u64; - assert!(i == checked_mul_shift_right_round_up_if(n0, n1, false)?); + assert!(i == checked_mul_shift_right_round_up_if(n0, n1, false).unwrap()); if i == u64::MAX && (p & Q64_MASK > 0) { assert!(result.is_err()); @@ -315,7 +311,6 @@ mod test_bit_math { ); assert_eq!(div_round_up(u128::MAX - 1, u128::MAX).unwrap(), 1); } - } mod test_mult_shift_right_round_up { @@ -323,21 +318,59 @@ mod test_bit_math { #[test] fn test_mul_shift_right_ok() { - assert_eq!(checked_mul_shift_right_round_up_if(u64::MAX as u128, 1, false).unwrap(), 0); - assert_eq!(checked_mul_shift_right_round_up_if(u64::MAX as u128, 1, true).unwrap(), 1); - assert_eq!(checked_mul_shift_right_round_up_if(u64::MAX as u128 + 1, 1, false).unwrap(), 1); - assert_eq!(checked_mul_shift_right_round_up_if(u64::MAX as u128 + 1, 1, true).unwrap(), 1); - assert_eq!(checked_mul_shift_right_round_up_if(u32::MAX as u128, u32::MAX as u128, false).unwrap(), 0); - assert_eq!(checked_mul_shift_right_round_up_if(u32::MAX as u128, u32::MAX as u128, true).unwrap(), 1); - assert_eq!(checked_mul_shift_right_round_up_if(u32::MAX as u128 + 1, u32::MAX as u128 + 2, false).unwrap(), 1); - assert_eq!(checked_mul_shift_right_round_up_if(u32::MAX as u128 + 1, u32::MAX as u128 + 2, true).unwrap(), 2); + assert_eq!( + checked_mul_shift_right_round_up_if(u64::MAX as u128, 1, false).unwrap(), + 0 + ); + assert_eq!( + checked_mul_shift_right_round_up_if(u64::MAX as u128, 1, true).unwrap(), + 1 + ); + assert_eq!( + checked_mul_shift_right_round_up_if(u64::MAX as u128 + 1, 1, false).unwrap(), + 1 + ); + assert_eq!( + checked_mul_shift_right_round_up_if(u64::MAX as u128 + 1, 1, true).unwrap(), + 1 + ); + assert_eq!( + checked_mul_shift_right_round_up_if(u32::MAX as u128, u32::MAX as u128, false) + .unwrap(), + 0 + ); + assert_eq!( + checked_mul_shift_right_round_up_if(u32::MAX as u128, u32::MAX as u128, true) + .unwrap(), + 1 + ); + assert_eq!( + checked_mul_shift_right_round_up_if( + u32::MAX as u128 + 1, + u32::MAX as u128 + 2, + false + ) + .unwrap(), + 1 + ); + assert_eq!( + checked_mul_shift_right_round_up_if( + u32::MAX as u128 + 1, + u32::MAX as u128 + 2, + true + ) + .unwrap(), + 2 + ); } #[test] fn test_mul_shift_right_u64_max() { assert!(checked_mul_shift_right_round_up_if(u128::MAX, 1, true).is_err()); - assert_eq!(checked_mul_shift_right_round_up_if(u128::MAX, 1, false).unwrap(), u64::MAX); + assert_eq!( + checked_mul_shift_right_round_up_if(u128::MAX, 1, false).unwrap(), + u64::MAX + ); } - } } diff --git a/programs/whirlpool/src/math/token_math.rs b/programs/whirlpool/src/math/token_math.rs index e5b7b58..e4bab93 100644 --- a/programs/whirlpool/src/math/token_math.rs +++ b/programs/whirlpool/src/math/token_math.rs @@ -307,7 +307,7 @@ mod fuzz_tests { if liquidity.leading_zeros() + sqrt_price.leading_zeros() < Q64_RESOLUTION.into() { assert!(case_1_price.is_err()); } else { - assert!(amount >= get_amount_delta_a(sqrt_price, case_1_price?, liquidity, true)?); + assert!(amount >= get_amount_delta_a(sqrt_price, case_1_price.unwrap(), liquidity, true).unwrap()); // Case 2. amount_specified_is_input = false, a_to_b = false // We are removing token A from the supply, causing price to increase (Eq 1.) @@ -327,12 +327,12 @@ mod fuzz_tests { if liquidity_x64 <= product { assert!(case_2_price.is_err()); } else { - assert!(amount <= get_amount_delta_a(sqrt_price, case_2_price?, liquidity, false)?); - assert!(case_2_price? >= sqrt_price); + assert!(amount <= get_amount_delta_a(sqrt_price, case_2_price.unwrap(), liquidity, false).unwrap()); + assert!(case_2_price.unwrap() >= sqrt_price); } if amount == 0 { - assert!(case_1_price? == case_2_price?); + assert!(case_1_price.unwrap() == case_2_price.unwrap()); } } } @@ -351,9 +351,9 @@ mod fuzz_tests { // Because a lower price is inversely correlated with an increased supply of B, // a lower price means that we are adding less B. Thus when performing math, we wish to round the // price down, since that means that we are guaranteed to not exceed the fixed amount of B provided. - let case_3_price = get_next_sqrt_price_from_b_round_down(sqrt_price, liquidity, amount, true)?; + let case_3_price = get_next_sqrt_price_from_b_round_down(sqrt_price, liquidity, amount, true).unwrap(); assert!(case_3_price >= sqrt_price); - assert!(amount >= get_amount_delta_b(sqrt_price, case_3_price, liquidity, true)?); + assert!(amount >= get_amount_delta_b(sqrt_price, case_3_price, liquidity, true).unwrap()); // Case 4. amount_specified_is_input = false, a_to_b = true // We are removing token B from the supply, causing price to decrease (Eq 1.) @@ -365,22 +365,22 @@ mod fuzz_tests { // Q64.0 << 64 => Q64.64 let amount_x64 = u128::from(amount) << Q64_RESOLUTION; - let delta = div_round_up(amount_x64, liquidity.into())?; + let delta = div_round_up(amount_x64, liquidity.into()).unwrap(); if sqrt_price < delta { // In Case 4, error if sqrt_price < delta assert!(case_4_price.is_err()); } else { - let calc_delta = get_amount_delta_b(sqrt_price, case_4_price?, liquidity, false); + let calc_delta = get_amount_delta_b(sqrt_price, case_4_price.unwrap(), liquidity, false); if calc_delta.is_ok() { - assert!(amount <= calc_delta?); + assert!(amount <= calc_delta.unwrap()); } // In Case 4, price is decreasing - assert!(case_4_price? <= sqrt_price); + assert!(case_4_price.unwrap() <= sqrt_price); } if amount == 0 { - assert!(case_3_price == case_4_price?); + assert!(case_3_price == case_4_price.unwrap()); } } @@ -398,17 +398,17 @@ mod fuzz_tests { if liquidity.leading_zeros() + (sqrt_price_upper - sqrt_price_lower).leading_zeros() < Q64_RESOLUTION.into() { assert!(rounded.is_err()) } else { - let unrounded = get_amount_delta_a(sqrt_price_0, sqrt_price_1, liquidity, false)?; + let unrounded = get_amount_delta_a(sqrt_price_0, sqrt_price_1, liquidity, false).unwrap(); // Price difference symmetry - assert_eq!(rounded?, get_amount_delta_a(sqrt_price_1, sqrt_price_0, liquidity, true)?); - assert_eq!(unrounded, get_amount_delta_a(sqrt_price_1, sqrt_price_0, liquidity, false)?); + assert_eq!(rounded.unwrap(), get_amount_delta_a(sqrt_price_1, sqrt_price_0, liquidity, true).unwrap()); + assert_eq!(unrounded, get_amount_delta_a(sqrt_price_1, sqrt_price_0, liquidity, false).unwrap()); // Rounded should always be larger - assert!(unrounded <= rounded?); + assert!(unrounded <= rounded.unwrap()); // Diff should be no more than 1 - assert!(rounded? - unrounded <= 1); + assert!(rounded.unwrap() - unrounded <= 1); } } @@ -440,17 +440,17 @@ mod fuzz_tests { } else if round_up_delta > u64_max_in_u256 { assert!(rounded.is_err()); // Price symmmetry - assert_eq!(unrounded?, get_amount_delta_b(sqrt_price_1, sqrt_price_0, liquidity, false)?); + assert_eq!(unrounded.unwrap(), get_amount_delta_b(sqrt_price_1, sqrt_price_0, liquidity, false).unwrap()); } else { // Price difference symmetry - assert_eq!(rounded?, get_amount_delta_b(sqrt_price_1, sqrt_price_0, liquidity, true)?); - assert_eq!(unrounded?, get_amount_delta_b(sqrt_price_1, sqrt_price_0, liquidity, false)?); + assert_eq!(rounded.unwrap(), get_amount_delta_b(sqrt_price_1, sqrt_price_0, liquidity, true).unwrap()); + assert_eq!(unrounded.unwrap(), get_amount_delta_b(sqrt_price_1, sqrt_price_0, liquidity, false).unwrap()); // Rounded should always be larger - assert!(unrounded? <= rounded? ); + assert!(unrounded.unwrap() <= rounded.unwrap()); // Diff should be no more than 1 - assert!(rounded? - unrounded? <= 1); + assert!(rounded.unwrap() - unrounded.unwrap() <= 1); } } diff --git a/programs/whirlpool/src/state/config.rs b/programs/whirlpool/src/state/config.rs index 416ece9..8747140 100644 --- a/programs/whirlpool/src/state/config.rs +++ b/programs/whirlpool/src/state/config.rs @@ -31,7 +31,7 @@ impl WhirlpoolsConfig { collect_protocol_fees_authority: Pubkey, reward_emissions_super_authority: Pubkey, default_protocol_fee_rate: u16, - ) -> Result<(), ErrorCode> { + ) -> Result<()> { self.fee_authority = fee_authority; self.collect_protocol_fees_authority = collect_protocol_fees_authority; self.reward_emissions_super_authority = reward_emissions_super_authority; @@ -50,7 +50,7 @@ impl WhirlpoolsConfig { pub fn update_default_protocol_fee_rate( &mut self, default_protocol_fee_rate: u16, - ) -> Result<(), ErrorCode> { + ) -> Result<()> { if default_protocol_fee_rate > MAX_PROTOCOL_FEE_RATE { return Err(ErrorCode::ProtocolFeeRateMaxExceeded.into()); } diff --git a/programs/whirlpool/src/state/fee_tier.rs b/programs/whirlpool/src/state/fee_tier.rs index fb59cdf..38e3acb 100644 --- a/programs/whirlpool/src/state/fee_tier.rs +++ b/programs/whirlpool/src/state/fee_tier.rs @@ -17,14 +17,14 @@ impl FeeTier { whirlpools_config: &Account, tick_spacing: u16, default_fee_rate: u16, - ) -> Result<(), ErrorCode> { + ) -> Result<()> { self.whirlpools_config = whirlpools_config.key(); self.tick_spacing = tick_spacing; self.update_default_fee_rate(default_fee_rate)?; Ok(()) } - pub fn update_default_fee_rate(&mut self, default_fee_rate: u16) -> Result<(), ErrorCode> { + pub fn update_default_fee_rate(&mut self, default_fee_rate: u16) -> Result<()> { if default_fee_rate > MAX_FEE_RATE { return Err(ErrorCode::FeeRateMaxExceeded.into()); } diff --git a/programs/whirlpool/src/state/mod.rs b/programs/whirlpool/src/state/mod.rs index c35582b..fa24e3f 100644 --- a/programs/whirlpool/src/state/mod.rs +++ b/programs/whirlpool/src/state/mod.rs @@ -1,6 +1,7 @@ pub mod config; pub mod fee_tier; pub mod position; +pub mod position_bundle; pub mod tick; pub mod whirlpool; @@ -8,4 +9,5 @@ pub use self::whirlpool::*; pub use config::*; pub use fee_tier::*; pub use position::*; +pub use position_bundle::*; pub use tick::*; diff --git a/programs/whirlpool/src/state/position.rs b/programs/whirlpool/src/state/position.rs index 9184fdc..39b2491 100644 --- a/programs/whirlpool/src/state/position.rs +++ b/programs/whirlpool/src/state/position.rs @@ -61,7 +61,7 @@ impl Position { position_mint: Pubkey, tick_lower_index: i32, tick_upper_index: i32, - ) -> Result<(), ErrorCode> { + ) -> Result<()> { if !Tick::check_is_usable_tick(tick_lower_index, whirlpool.tick_spacing) || !Tick::check_is_usable_tick(tick_upper_index, whirlpool.tick_spacing) || tick_lower_index >= tick_upper_index diff --git a/programs/whirlpool/src/state/position_bundle.rs b/programs/whirlpool/src/state/position_bundle.rs new file mode 100644 index 0000000..fd20aae --- /dev/null +++ b/programs/whirlpool/src/state/position_bundle.rs @@ -0,0 +1,270 @@ +use crate::errors::ErrorCode; +use anchor_lang::prelude::*; + +pub const POSITION_BITMAP_USIZE: usize = 32; +pub const POSITION_BUNDLE_SIZE: u16 = 8 * POSITION_BITMAP_USIZE as u16; + +#[account] +#[derive(Default)] +pub struct PositionBundle { + pub position_bundle_mint: Pubkey, // 32 + pub position_bitmap: [u8; POSITION_BITMAP_USIZE], // 32 + // 64 RESERVE +} + +impl PositionBundle { + pub const LEN: usize = 8 + 32 + 32 + 64; + + pub fn initialize(&mut self, position_bundle_mint: Pubkey) -> Result<()> { + self.position_bundle_mint = position_bundle_mint; + // position_bitmap is initialized using Default trait + Ok(()) + } + + pub fn is_deletable(&self) -> bool { + for bitmap in self.position_bitmap.iter() { + if *bitmap != 0 { + return false; + } + } + true + } + + pub fn open_bundled_position(&mut self, bundle_index: u16) -> Result<()> { + self.update_bitmap(bundle_index, true) + } + + pub fn close_bundled_position(&mut self, bundle_index: u16) -> Result<()> { + self.update_bitmap(bundle_index, false) + } + + fn update_bitmap(&mut self, bundle_index: u16, open: bool) -> Result<()> { + if !PositionBundle::is_valid_bundle_index(bundle_index) { + return Err(ErrorCode::InvalidBundleIndex.into()); + } + + let bitmap_index = bundle_index / 8; + let bitmap_offset = bundle_index % 8; + let bitmap = self.position_bitmap[bitmap_index as usize]; + + let mask = 1 << bitmap_offset; + let bit = bitmap & mask; + let opened = bit != 0; + + if open && opened { + // UNREACHABLE + // Anchor should reject with AccountDiscriminatorAlreadySet + return Err(ErrorCode::BundledPositionAlreadyOpened.into()); + } + if !open && !opened { + // UNREACHABLE + // Anchor should reject with AccountNotInitialized + return Err(ErrorCode::BundledPositionAlreadyClosed.into()); + } + + let updated_bitmap = bitmap ^ mask; + self.position_bitmap[bitmap_index as usize] = updated_bitmap; + + Ok(()) + } + + fn is_valid_bundle_index(bundle_index: u16) -> bool { + bundle_index < POSITION_BUNDLE_SIZE + } +} + +#[cfg(test)] +mod position_bundle_initialize_tests { + use super::*; + use std::str::FromStr; + + #[test] + fn test_default() { + let position_bundle = PositionBundle { + ..Default::default() + }; + assert_eq!(position_bundle.position_bundle_mint, Pubkey::default()); + for bitmap in position_bundle.position_bitmap.iter() { + assert_eq!(*bitmap, 0); + } + } + + #[test] + fn test_initialize() { + let mut position_bundle = PositionBundle { + ..Default::default() + }; + let position_bundle_mint = + Pubkey::from_str("orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE").unwrap(); + + let result = position_bundle.initialize(position_bundle_mint); + assert!(result.is_ok()); + + assert_eq!(position_bundle.position_bundle_mint, position_bundle_mint); + for bitmap in position_bundle.position_bitmap.iter() { + assert_eq!(*bitmap, 0); + } + } +} + +#[cfg(test)] +mod position_bundle_is_deletable_tests { + use super::*; + + #[test] + fn test_default_is_deletable() { + let position_bundle = PositionBundle { + ..Default::default() + }; + assert!(position_bundle.is_deletable()); + } + + #[test] + fn test_each_bit_detectable() { + let mut position_bundle = PositionBundle { + ..Default::default() + }; + for bundle_index in 0..POSITION_BUNDLE_SIZE { + let index = bundle_index / 8; + let offset = bundle_index % 8; + position_bundle.position_bitmap[index as usize] = 1 << offset; + assert!(!position_bundle.is_deletable()); + position_bundle.position_bitmap[index as usize] = 0; + assert!(position_bundle.is_deletable()); + } + } +} + +#[cfg(test)] +mod position_bundle_open_and_close_tests { + use super::*; + + #[test] + fn test_open_and_close_zero() { + let mut position_bundle = PositionBundle { + ..Default::default() + }; + + let r1 = position_bundle.open_bundled_position(0); + assert!(r1.is_ok()); + assert_eq!(position_bundle.position_bitmap[0], 1); + + let r2 = position_bundle.close_bundled_position(0); + assert!(r2.is_ok()); + assert_eq!(position_bundle.position_bitmap[0], 0); + } + + #[test] + fn test_open_and_close_middle() { + let mut position_bundle = PositionBundle { + ..Default::default() + }; + + let r1 = position_bundle.open_bundled_position(130); + assert!(r1.is_ok()); + assert_eq!(position_bundle.position_bitmap[16], 4); + + let r2 = position_bundle.close_bundled_position(130); + assert!(r2.is_ok()); + assert_eq!(position_bundle.position_bitmap[16], 0); + } + + #[test] + fn test_open_and_close_max() { + let mut position_bundle = PositionBundle { + ..Default::default() + }; + + let r1 = position_bundle.open_bundled_position(POSITION_BUNDLE_SIZE - 1); + assert!(r1.is_ok()); + assert_eq!( + position_bundle.position_bitmap[POSITION_BITMAP_USIZE - 1], + 128 + ); + + let r2 = position_bundle.close_bundled_position(POSITION_BUNDLE_SIZE - 1); + assert!(r2.is_ok()); + assert_eq!( + position_bundle.position_bitmap[POSITION_BITMAP_USIZE - 1], + 0 + ); + } + + #[test] + fn test_double_open_should_be_failed() { + let mut position_bundle = PositionBundle { + ..Default::default() + }; + + let r1 = position_bundle.open_bundled_position(0); + assert!(r1.is_ok()); + + let r2 = position_bundle.open_bundled_position(0); + assert!(r2.is_err()); + } + + #[test] + fn test_double_close_should_be_failed() { + let mut position_bundle = PositionBundle { + ..Default::default() + }; + + let r1 = position_bundle.open_bundled_position(0); + assert!(r1.is_ok()); + + let r2 = position_bundle.close_bundled_position(0); + assert!(r2.is_ok()); + + let r3 = position_bundle.close_bundled_position(0); + assert!(r3.is_err()); + } + + #[test] + fn test_all_open_and_all_close() { + let mut position_bundle = PositionBundle { + ..Default::default() + }; + + for bundle_index in 0..POSITION_BUNDLE_SIZE { + let r = position_bundle.open_bundled_position(bundle_index); + assert!(r.is_ok()); + } + + for bitmap in position_bundle.position_bitmap.iter() { + assert_eq!(*bitmap, 255); + } + + for bundle_index in 0..POSITION_BUNDLE_SIZE { + let r = position_bundle.close_bundled_position(bundle_index); + assert!(r.is_ok()); + } + + for bitmap in position_bundle.position_bitmap.iter() { + assert_eq!(*bitmap, 0); + } + } + + #[test] + fn test_open_bundle_index_out_of_bounds() { + let mut position_bundle = PositionBundle { + ..Default::default() + }; + + for bundle_index in POSITION_BUNDLE_SIZE..u16::MAX { + let r = position_bundle.open_bundled_position(bundle_index); + assert!(r.is_err()); + } + } + + #[test] + fn test_close_bundle_index_out_of_bounds() { + let mut position_bundle = PositionBundle { + ..Default::default() + }; + + for bundle_index in POSITION_BUNDLE_SIZE..u16::MAX { + let r = position_bundle.close_bundled_position(bundle_index); + assert!(r.is_err()); + } + } +} diff --git a/programs/whirlpool/src/state/tick.rs b/programs/whirlpool/src/state/tick.rs index bf0cbdc..a59b420 100644 --- a/programs/whirlpool/src/state/tick.rs +++ b/programs/whirlpool/src/state/tick.rs @@ -178,9 +178,9 @@ impl TickArray { tick_index: i32, tick_spacing: u16, a_to_b: bool, - ) -> Result, ErrorCode> { + ) -> Result> { if !self.in_search_range(tick_index, tick_spacing, !a_to_b) { - return Err(ErrorCode::InvalidTickArraySequence); + return Err(ErrorCode::InvalidTickArraySequence.into()); } let mut curr_offset = match self.tick_offset(tick_index, tick_spacing) { @@ -224,7 +224,7 @@ impl TickArray { &mut self, whirlpool: &Account, start_tick_index: i32, - ) -> Result<(), ErrorCode> { + ) -> Result<()> { if !Tick::check_is_valid_start_tick(start_tick_index, whirlpool.tick_spacing) { return Err(ErrorCode::InvalidStartTick.into()); } @@ -243,15 +243,15 @@ impl TickArray { /// # Returns /// - `&Tick`: A reference to the desired Tick object /// - `TickNotFound`: - The provided tick-index is not an initializable tick index in this Whirlpool w/ this tick-spacing. - pub fn get_tick(&self, tick_index: i32, tick_spacing: u16) -> Result<&Tick, ErrorCode> { + pub fn get_tick(&self, tick_index: i32, tick_spacing: u16) -> Result<&Tick> { if !self.check_in_array_bounds(tick_index, tick_spacing) || !Tick::check_is_usable_tick(tick_index, tick_spacing) { - return Err(ErrorCode::TickNotFound); + return Err(ErrorCode::TickNotFound.into()); } let offset = self.tick_offset(tick_index, tick_spacing)?; if offset < 0 { - return Err(ErrorCode::TickNotFound); + return Err(ErrorCode::TickNotFound.into()); } Ok(&self.ticks[offset as usize]) } @@ -270,15 +270,15 @@ impl TickArray { tick_index: i32, tick_spacing: u16, update: &TickUpdate, - ) -> Result<(), ErrorCode> { + ) -> Result<()> { if !self.check_in_array_bounds(tick_index, tick_spacing) || !Tick::check_is_usable_tick(tick_index, tick_spacing) { - return Err(ErrorCode::TickNotFound); + return Err(ErrorCode::TickNotFound.into()); } let offset = self.tick_offset(tick_index, tick_spacing)?; if offset < 0 { - return Err(ErrorCode::TickNotFound); + return Err(ErrorCode::TickNotFound.into()); } self.ticks.get_mut(offset as usize).unwrap().update(update); Ok(()) @@ -319,9 +319,9 @@ impl TickArray { } // Calculates an offset from a tick index that can be used to access the tick data - pub fn tick_offset(&self, tick_index: i32, tick_spacing: u16) -> Result { + pub fn tick_offset(&self, tick_index: i32, tick_spacing: u16) -> Result { if tick_spacing == 0 { - return Err(ErrorCode::InvalidTickSpacing); + return Err(ErrorCode::InvalidTickSpacing.into()); } Ok(get_offset(tick_index, self.start_tick_index, tick_spacing)) diff --git a/programs/whirlpool/src/state/whirlpool.rs b/programs/whirlpool/src/state/whirlpool.rs index 3e90b6d..ad1ff88 100644 --- a/programs/whirlpool/src/state/whirlpool.rs +++ b/programs/whirlpool/src/state/whirlpool.rs @@ -80,7 +80,7 @@ impl Whirlpool { token_vault_a: Pubkey, token_mint_b: Pubkey, token_vault_b: Pubkey, - ) -> Result<(), ErrorCode> { + ) -> Result<()> { if token_mint_a.ge(&token_mint_b) { return Err(ErrorCode::InvalidTokenMintOrder.into()); } @@ -145,11 +145,7 @@ impl Whirlpool { } /// Update the reward authority at the specified Whirlpool reward index. - pub fn update_reward_authority( - &mut self, - index: usize, - authority: Pubkey, - ) -> Result<(), ErrorCode> { + pub fn update_reward_authority(&mut self, index: usize, authority: Pubkey) -> Result<()> { if index >= NUM_REWARDS { return Err(ErrorCode::InvalidRewardIndex.into()); } @@ -164,7 +160,7 @@ impl Whirlpool { reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS], timestamp: u64, emissions_per_second_x64: u128, - ) -> Result<(), ErrorCode> { + ) -> Result<()> { if index >= NUM_REWARDS { return Err(ErrorCode::InvalidRewardIndex.into()); } @@ -174,12 +170,7 @@ impl Whirlpool { Ok(()) } - pub fn initialize_reward( - &mut self, - index: usize, - mint: Pubkey, - vault: Pubkey, - ) -> Result<(), ErrorCode> { + pub fn initialize_reward(&mut self, index: usize, mint: Pubkey, vault: Pubkey) -> Result<()> { if index >= NUM_REWARDS { return Err(ErrorCode::InvalidRewardIndex.into()); } @@ -226,7 +217,7 @@ impl Whirlpool { } } - pub fn update_fee_rate(&mut self, fee_rate: u16) -> Result<(), ErrorCode> { + pub fn update_fee_rate(&mut self, fee_rate: u16) -> Result<()> { if fee_rate > MAX_FEE_RATE { return Err(ErrorCode::FeeRateMaxExceeded.into()); } @@ -235,7 +226,7 @@ impl Whirlpool { Ok(()) } - pub fn update_protocol_fee_rate(&mut self, protocol_fee_rate: u16) -> Result<(), ErrorCode> { + pub fn update_protocol_fee_rate(&mut self, protocol_fee_rate: u16) -> Result<()> { if protocol_fee_rate > MAX_PROTOCOL_FEE_RATE { return Err(ErrorCode::ProtocolFeeRateMaxExceeded.into()); } diff --git a/programs/whirlpool/src/tests/swap_integration_tests.rs b/programs/whirlpool/src/tests/swap_integration_tests.rs index b788d8c..385dd7f 100644 --- a/programs/whirlpool/src/tests/swap_integration_tests.rs +++ b/programs/whirlpool/src/tests/swap_integration_tests.rs @@ -149,7 +149,9 @@ fn run_swap_integration_tests() { msg!(""); fail_cases += 1; - } else if expected_error.is_some() && !expected_error.unwrap().eq(&e) { + } else if expected_error.is_some() + && !anchor_lang::error!(expected_error.unwrap()).eq(&e) + { fail_cases += 1; msg!("Test case {} - {}", test_id, test.description); diff --git a/programs/whirlpool/src/util/swap_tick_sequence.rs b/programs/whirlpool/src/util/swap_tick_sequence.rs index c426a57..3950f32 100644 --- a/programs/whirlpool/src/util/swap_tick_sequence.rs +++ b/programs/whirlpool/src/util/swap_tick_sequence.rs @@ -1,5 +1,6 @@ use crate::errors::ErrorCode; use crate::state::*; +use anchor_lang::prelude::*; use std::cell::RefMut; pub struct SwapTickSequence<'info> { @@ -39,11 +40,11 @@ impl<'info> SwapTickSequence<'info> { array_index: usize, tick_index: i32, tick_spacing: u16, - ) -> Result<&Tick, ErrorCode> { + ) -> Result<&Tick> { let array = self.arrays.get(array_index); match array { Some(array) => array.get_tick(tick_index, tick_spacing), - _ => Err(ErrorCode::TickArrayIndexOutofBounds), + _ => Err(ErrorCode::TickArrayIndexOutofBounds.into()), } } @@ -64,14 +65,14 @@ impl<'info> SwapTickSequence<'info> { tick_index: i32, tick_spacing: u16, update: &TickUpdate, - ) -> Result<(), ErrorCode> { + ) -> Result<()> { let array = self.arrays.get_mut(array_index); match array { Some(array) => { array.update_tick(tick_index, tick_spacing, update)?; Ok(()) } - _ => Err(ErrorCode::TickArrayIndexOutofBounds), + _ => Err(ErrorCode::TickArrayIndexOutofBounds.into()), } } @@ -80,11 +81,11 @@ impl<'info> SwapTickSequence<'info> { array_index: usize, tick_index: i32, tick_spacing: u16, - ) -> Result { + ) -> Result { let array = self.arrays.get(array_index); match array { Some(array) => array.tick_offset(tick_index, tick_spacing), - _ => Err(ErrorCode::TickArrayIndexOutofBounds), + _ => Err(ErrorCode::TickArrayIndexOutofBounds.into()), } } @@ -108,7 +109,7 @@ impl<'info> SwapTickSequence<'info> { tick_spacing: u16, a_to_b: bool, start_array_index: usize, - ) -> Result<(usize, i32), ErrorCode> { + ) -> Result<(usize, i32)> { let ticks_in_array = TICK_ARRAY_SIZE * tick_spacing as i32; let mut search_index = tick_index; let mut array_index = start_array_index; @@ -118,7 +119,7 @@ impl<'info> SwapTickSequence<'info> { // If we get to the end of the array sequence and next_index is still not found, throw error let next_array = match self.arrays.get(array_index) { Some(array) => array, - None => return Err(ErrorCode::TickArraySequenceInvalidIndex), + None => return Err(ErrorCode::TickArraySequenceInvalidIndex.into()), }; let next_index = @@ -250,7 +251,7 @@ mod swap_tick_sequence_tests { TS_128, ); - assert_eq!(result.unwrap_err(), ErrorCode::TickNotFound); + assert_eq!(result.unwrap_err(), ErrorCode::TickNotFound.into()); let update_result = swap_tick_sequence.update_tick( uninitializable_search_tick.0, @@ -262,7 +263,7 @@ mod swap_tick_sequence_tests { ..Default::default() }, ); - assert_eq!(update_result.unwrap_err(), ErrorCode::TickNotFound); + assert_eq!(update_result.unwrap_err(), ErrorCode::TickNotFound.into()); } } @@ -327,7 +328,7 @@ mod swap_tick_sequence_tests { let get_result = swap_tick_sequence.get_tick(3, 5000, TS_128); assert_eq!( get_result.unwrap_err(), - ErrorCode::TickArrayIndexOutofBounds + ErrorCode::TickArrayIndexOutofBounds.into() ); let update_result = swap_tick_sequence.update_tick( @@ -340,7 +341,7 @@ mod swap_tick_sequence_tests { ); assert_eq!( update_result.unwrap_err(), - ErrorCode::TickArrayIndexOutofBounds + ErrorCode::TickArrayIndexOutofBounds.into() ); } } diff --git a/programs/whirlpool/src/util/swap_utils.rs b/programs/whirlpool/src/util/swap_utils.rs index c20c38d..a6e093c 100644 --- a/programs/whirlpool/src/util/swap_utils.rs +++ b/programs/whirlpool/src/util/swap_utils.rs @@ -1,9 +1,7 @@ use anchor_lang::prelude::*; -use anchor_spl::token::{TokenAccount, Token}; +use anchor_spl::token::{Token, TokenAccount}; -use crate::{ - manager::swap_manager::PostSwapUpdate, state::Whirlpool -}; +use crate::{manager::swap_manager::PostSwapUpdate, state::Whirlpool}; use super::{transfer_from_owner_to_vault, transfer_from_vault_to_owner}; @@ -18,7 +16,7 @@ pub fn update_and_swap_whirlpool<'info>( swap_update: PostSwapUpdate, is_token_fee_in_a: bool, reward_last_updated_timestamp: u64, -) -> ProgramResult { +) -> Result<()> { whirlpool.update_after_swap( swap_update.next_liquidity, swap_update.next_tick_index, @@ -45,60 +43,60 @@ pub fn update_and_swap_whirlpool<'info>( } fn perform_swap<'info>( - whirlpool: &Account<'info, Whirlpool>, - token_authority: &Signer<'info>, - token_owner_account_a: &Account<'info, TokenAccount>, - token_owner_account_b: &Account<'info, TokenAccount>, - token_vault_a: &Account<'info, TokenAccount>, - token_vault_b: &Account<'info, TokenAccount>, - token_program: &Program<'info, Token>, - amount_a: u64, - amount_b: u64, - a_to_b: bool, -) -> ProgramResult { - // Transfer from user to pool - let deposit_account_user; - let deposit_account_pool; - let deposit_amount; + whirlpool: &Account<'info, Whirlpool>, + token_authority: &Signer<'info>, + token_owner_account_a: &Account<'info, TokenAccount>, + token_owner_account_b: &Account<'info, TokenAccount>, + token_vault_a: &Account<'info, TokenAccount>, + token_vault_b: &Account<'info, TokenAccount>, + token_program: &Program<'info, Token>, + amount_a: u64, + amount_b: u64, + a_to_b: bool, +) -> Result<()> { + // Transfer from user to pool + let deposit_account_user; + let deposit_account_pool; + let deposit_amount; - // Transfer from pool to user - let withdrawal_account_user; - let withdrawal_account_pool; - let withdrawal_amount; + // Transfer from pool to user + let withdrawal_account_user; + let withdrawal_account_pool; + let withdrawal_amount; - if a_to_b { - deposit_account_user = token_owner_account_a; - deposit_account_pool = token_vault_a; - deposit_amount = amount_a; + if a_to_b { + deposit_account_user = token_owner_account_a; + deposit_account_pool = token_vault_a; + deposit_amount = amount_a; - withdrawal_account_user = token_owner_account_b; - withdrawal_account_pool = token_vault_b; - withdrawal_amount = amount_b; - } else { - deposit_account_user = token_owner_account_b; - deposit_account_pool = token_vault_b; - deposit_amount = amount_b; + withdrawal_account_user = token_owner_account_b; + withdrawal_account_pool = token_vault_b; + withdrawal_amount = amount_b; + } else { + deposit_account_user = token_owner_account_b; + deposit_account_pool = token_vault_b; + deposit_amount = amount_b; - withdrawal_account_user = token_owner_account_a; - withdrawal_account_pool = token_vault_a; - withdrawal_amount = amount_a; - } + withdrawal_account_user = token_owner_account_a; + withdrawal_account_pool = token_vault_a; + withdrawal_amount = amount_a; + } - transfer_from_owner_to_vault( - token_authority, - deposit_account_user, - deposit_account_pool, - token_program, - deposit_amount, - )?; + transfer_from_owner_to_vault( + token_authority, + deposit_account_user, + deposit_account_pool, + token_program, + deposit_amount, + )?; - transfer_from_vault_to_owner( - whirlpool, - withdrawal_account_pool, - withdrawal_account_user, - token_program, - withdrawal_amount, - )?; + transfer_from_vault_to_owner( + whirlpool, + withdrawal_account_pool, + withdrawal_account_user, + token_program, + withdrawal_amount, + )?; - Ok(()) + Ok(()) } diff --git a/programs/whirlpool/src/util/test_utils/swap_test_fixture.rs b/programs/whirlpool/src/util/test_utils/swap_test_fixture.rs index 33b7bb8..aa03b70 100644 --- a/programs/whirlpool/src/util/test_utils/swap_test_fixture.rs +++ b/programs/whirlpool/src/util/test_utils/swap_test_fixture.rs @@ -1,4 +1,3 @@ -use crate::errors::ErrorCode; use crate::manager::swap_manager::*; use crate::math::tick_math::*; use crate::state::{ @@ -222,7 +221,7 @@ impl SwapTestFixture { &self, tick_sequence: &mut SwapTickSequence, next_timestamp: u64, - ) -> Result { + ) -> Result { swap( &self.whirlpool, tick_sequence, diff --git a/programs/whirlpool/src/util/token.rs b/programs/whirlpool/src/util/token.rs index 39ad40c..24c8a8d 100644 --- a/programs/whirlpool/src/util/token.rs +++ b/programs/whirlpool/src/util/token.rs @@ -1,17 +1,22 @@ -use crate::state::Whirlpool; +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_v2; +use mpl_token_metadata::instruction::create_metadata_accounts_v3; use solana_program::program::invoke_signed; use spl_token::instruction::{burn_checked, close_account, mint_to, set_authority, AuthorityType}; +use crate::constants::nft::{ + WPB_METADATA_NAME_PREFIX, WPB_METADATA_SYMBOL, WPB_METADATA_URI, WP_METADATA_NAME, + WP_METADATA_SYMBOL, WP_METADATA_URI, +}; + pub fn transfer_from_owner_to_vault<'info>( position_authority: &Signer<'info>, token_owner_account: &Account<'info, TokenAccount>, token_vault: &Account<'info, TokenAccount>, token_program: &Program<'info, Token>, amount: u64, -) -> Result<(), ProgramError> { +) -> Result<()> { token::transfer( CpiContext::new( token_program.to_account_info(), @@ -31,7 +36,7 @@ pub fn transfer_from_vault_to_owner<'info>( token_owner_account: &Account<'info, TokenAccount>, token_program: &Program<'info, Token>, amount: u64, -) -> Result<(), ProgramError> { +) -> Result<()> { token::transfer( CpiContext::new_with_signer( token_program.to_account_info(), @@ -52,7 +57,7 @@ pub fn burn_and_close_user_position_token<'info>( position_mint: &Account<'info, Mint>, position_token_account: &Account<'info, TokenAccount>, token_program: &Program<'info, Token>, -) -> ProgramResult { +) -> Result<()> { // Burn a single token in user account invoke_signed( &burn_checked( @@ -89,7 +94,8 @@ pub fn burn_and_close_user_position_token<'info>( token_authority.to_account_info(), ], &[], - ) + )?; + Ok(()) } pub fn mint_position_token_and_remove_authority<'info>( @@ -97,7 +103,7 @@ pub fn mint_position_token_and_remove_authority<'info>( position_mint: &Account<'info, Mint>, position_token_account: &Account<'info, TokenAccount>, token_program: &Program<'info, Token>, -) -> ProgramResult { +) -> Result<()> { mint_position_token( whirlpool, position_mint, @@ -107,10 +113,6 @@ pub fn mint_position_token_and_remove_authority<'info>( remove_position_token_mint_authority(whirlpool, position_mint, token_program) } -const WP_METADATA_NAME: &str = "Orca Whirlpool Position"; -const WP_METADATA_SYMBOL: &str = "OWP"; -const WP_METADATA_URI: &str = "https://arweave.net/E19ZNY2sqMqddm1Wx7mrXPUZ0ZZ5ISizhebb0UsVEws"; - pub fn mint_position_token_with_metadata_and_remove_authority<'info>( whirlpool: &Account<'info, Whirlpool>, position_mint: &Account<'info, Mint>, @@ -122,7 +124,7 @@ pub fn mint_position_token_with_metadata_and_remove_authority<'info>( token_program: &Program<'info, Token>, system_program: &Program<'info, System>, rent: &Sysvar<'info, Rent>, -) -> ProgramResult { +) -> Result<()> { mint_position_token( whirlpool, position_mint, @@ -132,7 +134,7 @@ pub fn mint_position_token_with_metadata_and_remove_authority<'info>( let metadata_mint_auth_account = whirlpool; invoke_signed( - &create_metadata_accounts_v2( + &create_metadata_accounts_v3( metadata_program.key(), position_metadata_account.key(), position_mint.key(), @@ -148,6 +150,7 @@ pub fn mint_position_token_with_metadata_and_remove_authority<'info>( true, None, None, + None, ), &[ position_metadata_account.to_account_info(), @@ -170,7 +173,7 @@ fn mint_position_token<'info>( position_mint: &Account<'info, Mint>, position_token_account: &Account<'info, TokenAccount>, token_program: &Program<'info, Token>, -) -> ProgramResult { +) -> Result<()> { invoke_signed( &mint_to( token_program.key, @@ -187,14 +190,15 @@ fn mint_position_token<'info>( token_program.to_account_info(), ], &[&whirlpool.seeds()], - ) + )?; + Ok(()) } fn remove_position_token_mint_authority<'info>( whirlpool: &Account<'info, Whirlpool>, position_mint: &Account<'info, Mint>, token_program: &Program<'info, Token>, -) -> ProgramResult { +) -> Result<()> { invoke_signed( &set_authority( token_program.key, @@ -210,5 +214,170 @@ fn remove_position_token_mint_authority<'info>( token_program.to_account_info(), ], &[&whirlpool.seeds()], + )?; + Ok(()) +} + +pub fn mint_position_bundle_token_and_remove_authority<'info>( + position_bundle: &Account<'info, PositionBundle>, + position_bundle_mint: &Account<'info, Mint>, + position_bundle_token_account: &Account<'info, TokenAccount>, + token_program: &Program<'info, Token>, + position_bundle_seeds: &[&[u8]], +) -> Result<()> { + mint_position_bundle_token( + position_bundle, + position_bundle_mint, + position_bundle_token_account, + token_program, + position_bundle_seeds, + )?; + remove_position_bundle_token_mint_authority( + position_bundle, + position_bundle_mint, + token_program, + position_bundle_seeds, + ) +} + +pub fn mint_position_bundle_token_with_metadata_and_remove_authority<'info>( + funder: &Signer<'info>, + position_bundle: &Account<'info, PositionBundle>, + position_bundle_mint: &Account<'info, Mint>, + position_bundle_token_account: &Account<'info, TokenAccount>, + position_bundle_metadata: &UncheckedAccount<'info>, + metadata_update_auth: &UncheckedAccount<'info>, + metadata_program: &UncheckedAccount<'info>, + token_program: &Program<'info, Token>, + system_program: &Program<'info, System>, + rent: &Sysvar<'info, Rent>, + position_bundle_seeds: &[&[u8]], +) -> Result<()> { + mint_position_bundle_token( + position_bundle, + position_bundle_mint, + position_bundle_token_account, + token_program, + position_bundle_seeds, + )?; + + // Create Metadata + // Orca Position Bundle xxxx...yyyy + // xxxx and yyyy are the first and last 4 chars of mint address + let mint_address = position_bundle_mint.key().to_string(); + let mut nft_name = String::from(WPB_METADATA_NAME_PREFIX); + nft_name += " "; + nft_name += &mint_address[0..4]; + 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_program.to_account_info(), + system_program.to_account_info(), + rent.to_account_info(), + ], + &[position_bundle_seeds], + )?; + + remove_position_bundle_token_mint_authority( + position_bundle, + position_bundle_mint, + token_program, + position_bundle_seeds, + ) +} + +fn mint_position_bundle_token<'info>( + position_bundle: &Account<'info, PositionBundle>, + position_bundle_mint: &Account<'info, Mint>, + position_bundle_token_account: &Account<'info, TokenAccount>, + token_program: &Program<'info, Token>, + position_bundle_seeds: &[&[u8]], +) -> Result<()> { + invoke_signed( + &mint_to( + token_program.key, + position_bundle_mint.to_account_info().key, + position_bundle_token_account.to_account_info().key, + position_bundle.to_account_info().key, + &[], + 1, + )?, + &[ + position_bundle_mint.to_account_info(), + position_bundle_token_account.to_account_info(), + position_bundle.to_account_info(), + token_program.to_account_info(), + ], + &[position_bundle_seeds], + )?; + + Ok(()) +} + +fn remove_position_bundle_token_mint_authority<'info>( + position_bundle: &Account<'info, PositionBundle>, + position_bundle_mint: &Account<'info, Mint>, + token_program: &Program<'info, Token>, + position_bundle_seeds: &[&[u8]], +) -> Result<()> { + invoke_signed( + &set_authority( + token_program.key, + position_bundle_mint.to_account_info().key, + Option::None, + AuthorityType::MintTokens, + position_bundle.to_account_info().key, + &[], + )?, + &[ + position_bundle_mint.to_account_info(), + position_bundle.to_account_info(), + token_program.to_account_info(), + ], + &[position_bundle_seeds], + )?; + + Ok(()) +} + +pub fn burn_and_close_position_bundle_token<'info>( + position_bundle_authority: &Signer<'info>, + receiver: &UncheckedAccount<'info>, + position_bundle_mint: &Account<'info, Mint>, + position_bundle_token_account: &Account<'info, TokenAccount>, + token_program: &Program<'info, Token>, +) -> Result<()> { + // use same logic + burn_and_close_user_position_token( + position_bundle_authority, + receiver, + position_bundle_mint, + position_bundle_token_account, + token_program, ) } diff --git a/programs/whirlpool/src/util/util.rs b/programs/whirlpool/src/util/util.rs index 06d99f5..496fc27 100644 --- a/programs/whirlpool/src/util/util.rs +++ b/programs/whirlpool/src/util/util.rs @@ -1,5 +1,5 @@ use anchor_lang::{ - prelude::{AccountInfo, ProgramError, Pubkey, Signer}, + prelude::{AccountInfo, Pubkey, Signer, *}, ToAccountInfo, }; use anchor_spl::token::TokenAccount; @@ -8,10 +8,18 @@ use std::convert::TryFrom; use crate::errors::ErrorCode; +pub fn verify_position_bundle_authority<'info>( + position_bundle_token_account: &TokenAccount, + position_bundle_authority: &Signer<'info>, +) -> Result<()> { + // use same logic + verify_position_authority(position_bundle_token_account, position_bundle_authority) +} + pub fn verify_position_authority<'info>( position_token_account: &TokenAccount, position_authority: &Signer<'info>, -) -> Result<(), ProgramError> { +) -> Result<()> { // Check token authority using validate_owner method... match position_token_account.delegate { COption::Some(ref delegate) if position_authority.key == delegate => { @@ -28,10 +36,7 @@ pub fn verify_position_authority<'info>( Ok(()) } -fn validate_owner( - expected_owner: &Pubkey, - owner_account_info: &AccountInfo, -) -> Result<(), ProgramError> { +fn validate_owner(expected_owner: &Pubkey, owner_account_info: &AccountInfo) -> Result<()> { if expected_owner != owner_account_info.key || !owner_account_info.is_signer { return Err(ErrorCode::MissingOrInvalidDelegate.into()); } @@ -39,6 +44,6 @@ fn validate_owner( Ok(()) } -pub fn to_timestamp_u64(t: i64) -> Result { - u64::try_from(t).or(Err(ErrorCode::InvalidTimestampConversion)) +pub fn to_timestamp_u64(t: i64) -> Result { + u64::try_from(t).or(Err(ErrorCode::InvalidTimestampConversion.into())) } diff --git a/sdk/src/artifacts/whirlpool.json b/sdk/src/artifacts/whirlpool.json index beebb24..be5999c 100644 --- a/sdk/src/artifacts/whirlpool.json +++ b/sdk/src/artifacts/whirlpool.json @@ -1,9 +1,18 @@ { - "version": "0.1.0", + "version": "0.2.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", @@ -42,6 +51,20 @@ }, { "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", @@ -118,6 +141,17 @@ }, { "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", @@ -149,6 +183,20 @@ }, { "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", @@ -189,6 +237,21 @@ }, { "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", @@ -240,6 +303,25 @@ }, { "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", @@ -270,6 +352,18 @@ }, { "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", @@ -341,6 +435,19 @@ }, { "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", @@ -365,7 +472,10 @@ { "name": "positionMetadataAccount", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "https://github.com/metaplex-foundation/metaplex-program-library/blob/master/token-metadata/program/src/utils.rs#L873" + ] }, { "name": "positionTokenAccount", @@ -427,6 +537,22 @@ }, { "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", @@ -501,6 +627,22 @@ }, { "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", @@ -575,6 +717,13 @@ }, { "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", @@ -601,6 +750,12 @@ }, { "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", @@ -652,6 +807,12 @@ }, { "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", @@ -698,6 +859,12 @@ }, { "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", @@ -744,6 +911,29 @@ }, { "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", @@ -826,6 +1016,15 @@ }, { "name": "closePosition", + "docs": [ + "Close a position in a Whirlpool. Burns the position token in the owner's wallet.", + "", + "### Authority", + "- \"position_authority\" - The authority that owns the position token.", + "", + "#### Special Errors", + "- `ClosePositionNotEmpty` - The provided position account is not empty." + ], "accounts": [ { "name": "positionAuthority", @@ -862,6 +1061,20 @@ }, { "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": "whirlpoolsConfig", @@ -888,6 +1101,20 @@ }, { "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": "whirlpoolsConfig", @@ -909,6 +1136,20 @@ }, { "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": "whirlpoolsConfig", @@ -935,6 +1176,20 @@ }, { "name": "setProtocolFeeRate", + "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.", + "", + "### Authority", + "- \"fee_authority\" - Set authority that can modify pool fees in the WhirlpoolConfig", + "", + "### Parameters", + "- `protocol_fee_rate` - The rate that the pool will use to calculate protocol fees going onwards.", + "", + "#### Special Errors", + "- `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE." + ], "accounts": [ { "name": "whirlpoolsConfig", @@ -961,6 +1216,15 @@ }, { "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", @@ -982,6 +1246,13 @@ }, { "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", @@ -1003,6 +1274,18 @@ }, { "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", @@ -1029,6 +1312,18 @@ }, { "name": "setRewardAuthorityBySuperAuthority", + "docs": [ + "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\" - 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": "whirlpoolsConfig", @@ -1060,6 +1355,14 @@ }, { "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", @@ -1081,6 +1384,33 @@ }, { "name": "twoHopSwap", + "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": "tokenProgram", @@ -1213,6 +1543,306 @@ "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 + }, + { + "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": "positionBundleTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleOwner", + "isMut": false, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "metadataUpdateAuth", + "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 + } + ], + "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." + ], + "accounts": [ + { + "name": "positionBundle", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleMint", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleOwner", + "isMut": false, + "isSigner": true + }, + { + "name": "receiver", + "isMut": true, + "isSigner": false + }, + { + "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": "bundledPosition", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundle", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "positionBundleAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "whirlpool", + "isMut": false, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "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" + } + ] + }, + { + "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." + ], + "accounts": [ + { + "name": "bundledPosition", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundle", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "positionBundleAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "receiver", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "bundleIndex", + "type": "u16" + } + ] } ], "accounts": [ @@ -1260,6 +1890,27 @@ ] } }, + { + "name": "PositionBundle", + "type": { + "kind": "struct", + "fields": [ + { + "name": "positionBundleMint", + "type": "publicKey" + }, + { + "name": "positionBitmap", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, { "name": "Position", "type": { @@ -1528,27 +2179,49 @@ }, { "name": "WhirlpoolRewardInfo", + "docs": [ + "Stores the state relevant for tracking liquidity mining rewards at the `Whirlpool` level.", + "These values are used in conjunction with `PositionRewardInfo`, `Tick.reward_growths_outside`,", + "and `Whirlpool.reward_last_updated_timestamp` to determine how many rewards are earned by open", + "positions." + ], "type": { "kind": "struct", "fields": [ { "name": "mint", + "docs": [ + "Reward token mint." + ], "type": "publicKey" }, { "name": "vault", + "docs": [ + "Reward vault token account." + ], "type": "publicKey" }, { "name": "authority", + "docs": [ + "Authority account that has permission to initialize the reward and set emissions." + ], "type": "publicKey" }, { "name": "emissionsPerSecondX64", + "docs": [ + "Q64.64 number that indicates how many tokens per second are earned per unit of liquidity." + ], "type": "u128" }, { "name": "growthGlobalX64", + "docs": [ + "Q64.64 number that tracks the total tokens earned per unit of liquidity since the reward", + "emissions were turned on." + ], "type": "u128" } ] @@ -1827,6 +2500,26 @@ "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" } ] } \ No newline at end of file diff --git a/sdk/src/artifacts/whirlpool.ts b/sdk/src/artifacts/whirlpool.ts index 66d8a8f..23d8a17 100644 --- a/sdk/src/artifacts/whirlpool.ts +++ b/sdk/src/artifacts/whirlpool.ts @@ -1,9 +1,18 @@ export type Whirlpool = { - "version": "0.1.0", + "version": "0.2.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", @@ -42,6 +51,20 @@ export type Whirlpool = { }, { "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", @@ -118,6 +141,17 @@ export type Whirlpool = { }, { "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", @@ -149,6 +183,20 @@ export type Whirlpool = { }, { "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", @@ -189,6 +237,21 @@ export type Whirlpool = { }, { "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", @@ -240,6 +303,25 @@ export type Whirlpool = { }, { "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", @@ -270,6 +352,18 @@ export type Whirlpool = { }, { "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", @@ -341,6 +435,19 @@ export type Whirlpool = { }, { "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", @@ -365,7 +472,10 @@ export type Whirlpool = { { "name": "positionMetadataAccount", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "https://github.com/metaplex-foundation/metaplex-program-library/blob/master/token-metadata/program/src/utils.rs#L873" + ] }, { "name": "positionTokenAccount", @@ -427,6 +537,22 @@ export type Whirlpool = { }, { "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", @@ -501,6 +627,22 @@ export type Whirlpool = { }, { "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", @@ -575,6 +717,13 @@ export type Whirlpool = { }, { "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", @@ -601,6 +750,12 @@ export type Whirlpool = { }, { "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", @@ -652,6 +807,12 @@ export type Whirlpool = { }, { "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", @@ -698,6 +859,12 @@ export type Whirlpool = { }, { "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", @@ -744,6 +911,29 @@ export type Whirlpool = { }, { "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", @@ -826,6 +1016,15 @@ export type Whirlpool = { }, { "name": "closePosition", + "docs": [ + "Close a position in a Whirlpool. Burns the position token in the owner's wallet.", + "", + "### Authority", + "- \"position_authority\" - The authority that owns the position token.", + "", + "#### Special Errors", + "- `ClosePositionNotEmpty` - The provided position account is not empty." + ], "accounts": [ { "name": "positionAuthority", @@ -862,6 +1061,20 @@ export type Whirlpool = { }, { "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": "whirlpoolsConfig", @@ -888,6 +1101,20 @@ export type Whirlpool = { }, { "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": "whirlpoolsConfig", @@ -909,6 +1136,20 @@ export type Whirlpool = { }, { "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": "whirlpoolsConfig", @@ -935,6 +1176,20 @@ export type Whirlpool = { }, { "name": "setProtocolFeeRate", + "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.", + "", + "### Authority", + "- \"fee_authority\" - Set authority that can modify pool fees in the WhirlpoolConfig", + "", + "### Parameters", + "- `protocol_fee_rate` - The rate that the pool will use to calculate protocol fees going onwards.", + "", + "#### Special Errors", + "- `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE." + ], "accounts": [ { "name": "whirlpoolsConfig", @@ -961,6 +1216,15 @@ export type Whirlpool = { }, { "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", @@ -982,6 +1246,13 @@ export type Whirlpool = { }, { "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", @@ -1003,6 +1274,18 @@ export type Whirlpool = { }, { "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", @@ -1029,6 +1312,18 @@ export type Whirlpool = { }, { "name": "setRewardAuthorityBySuperAuthority", + "docs": [ + "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\" - 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": "whirlpoolsConfig", @@ -1060,6 +1355,14 @@ export type Whirlpool = { }, { "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", @@ -1081,6 +1384,33 @@ export type Whirlpool = { }, { "name": "twoHopSwap", + "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": "tokenProgram", @@ -1213,6 +1543,306 @@ export type Whirlpool = { "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 + }, + { + "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": "positionBundleTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleOwner", + "isMut": false, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "metadataUpdateAuth", + "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 + } + ], + "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." + ], + "accounts": [ + { + "name": "positionBundle", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleMint", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleOwner", + "isMut": false, + "isSigner": true + }, + { + "name": "receiver", + "isMut": true, + "isSigner": false + }, + { + "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": "bundledPosition", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundle", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "positionBundleAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "whirlpool", + "isMut": false, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "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" + } + ] + }, + { + "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." + ], + "accounts": [ + { + "name": "bundledPosition", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundle", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "positionBundleAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "receiver", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "bundleIndex", + "type": "u16" + } + ] } ], "accounts": [ @@ -1260,6 +1890,27 @@ export type Whirlpool = { ] } }, + { + "name": "positionBundle", + "type": { + "kind": "struct", + "fields": [ + { + "name": "positionBundleMint", + "type": "publicKey" + }, + { + "name": "positionBitmap", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, { "name": "position", "type": { @@ -1528,27 +2179,49 @@ export type Whirlpool = { }, { "name": "WhirlpoolRewardInfo", + "docs": [ + "Stores the state relevant for tracking liquidity mining rewards at the `Whirlpool` level.", + "These values are used in conjunction with `PositionRewardInfo`, `Tick.reward_growths_outside`,", + "and `Whirlpool.reward_last_updated_timestamp` to determine how many rewards are earned by open", + "positions." + ], "type": { "kind": "struct", "fields": [ { "name": "mint", + "docs": [ + "Reward token mint." + ], "type": "publicKey" }, { "name": "vault", + "docs": [ + "Reward vault token account." + ], "type": "publicKey" }, { "name": "authority", + "docs": [ + "Authority account that has permission to initialize the reward and set emissions." + ], "type": "publicKey" }, { "name": "emissionsPerSecondX64", + "docs": [ + "Q64.64 number that indicates how many tokens per second are earned per unit of liquidity." + ], "type": "u128" }, { "name": "growthGlobalX64", + "docs": [ + "Q64.64 number that tracks the total tokens earned per unit of liquidity since the reward", + "emissions were turned on." + ], "type": "u128" } ] @@ -1827,16 +2500,45 @@ export type Whirlpool = { "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" } ] }; export const IDL: Whirlpool = { - "version": "0.1.0", + "version": "0.2.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", @@ -1875,6 +2577,20 @@ export const IDL: Whirlpool = { }, { "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", @@ -1951,6 +2667,17 @@ export const IDL: Whirlpool = { }, { "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", @@ -1982,6 +2709,20 @@ export const IDL: Whirlpool = { }, { "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", @@ -2022,6 +2763,21 @@ export const IDL: Whirlpool = { }, { "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", @@ -2073,6 +2829,25 @@ export const IDL: Whirlpool = { }, { "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", @@ -2103,6 +2878,18 @@ export const IDL: Whirlpool = { }, { "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", @@ -2174,6 +2961,19 @@ export const IDL: Whirlpool = { }, { "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", @@ -2198,7 +2998,10 @@ export const IDL: Whirlpool = { { "name": "positionMetadataAccount", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "https://github.com/metaplex-foundation/metaplex-program-library/blob/master/token-metadata/program/src/utils.rs#L873" + ] }, { "name": "positionTokenAccount", @@ -2260,6 +3063,22 @@ export const IDL: Whirlpool = { }, { "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", @@ -2334,6 +3153,22 @@ export const IDL: Whirlpool = { }, { "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", @@ -2408,6 +3243,13 @@ export const IDL: Whirlpool = { }, { "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", @@ -2434,6 +3276,12 @@ export const IDL: Whirlpool = { }, { "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", @@ -2485,6 +3333,12 @@ export const IDL: Whirlpool = { }, { "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", @@ -2531,6 +3385,12 @@ export const IDL: Whirlpool = { }, { "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", @@ -2577,6 +3437,29 @@ export const IDL: Whirlpool = { }, { "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", @@ -2659,6 +3542,15 @@ export const IDL: Whirlpool = { }, { "name": "closePosition", + "docs": [ + "Close a position in a Whirlpool. Burns the position token in the owner's wallet.", + "", + "### Authority", + "- \"position_authority\" - The authority that owns the position token.", + "", + "#### Special Errors", + "- `ClosePositionNotEmpty` - The provided position account is not empty." + ], "accounts": [ { "name": "positionAuthority", @@ -2695,6 +3587,20 @@ export const IDL: Whirlpool = { }, { "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": "whirlpoolsConfig", @@ -2721,6 +3627,20 @@ export const IDL: Whirlpool = { }, { "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": "whirlpoolsConfig", @@ -2742,6 +3662,20 @@ export const IDL: Whirlpool = { }, { "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": "whirlpoolsConfig", @@ -2768,6 +3702,20 @@ export const IDL: Whirlpool = { }, { "name": "setProtocolFeeRate", + "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.", + "", + "### Authority", + "- \"fee_authority\" - Set authority that can modify pool fees in the WhirlpoolConfig", + "", + "### Parameters", + "- `protocol_fee_rate` - The rate that the pool will use to calculate protocol fees going onwards.", + "", + "#### Special Errors", + "- `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE." + ], "accounts": [ { "name": "whirlpoolsConfig", @@ -2794,6 +3742,15 @@ export const IDL: Whirlpool = { }, { "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", @@ -2815,6 +3772,13 @@ export const IDL: Whirlpool = { }, { "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", @@ -2836,6 +3800,18 @@ export const IDL: Whirlpool = { }, { "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", @@ -2862,6 +3838,18 @@ export const IDL: Whirlpool = { }, { "name": "setRewardAuthorityBySuperAuthority", + "docs": [ + "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\" - 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": "whirlpoolsConfig", @@ -2893,6 +3881,14 @@ export const IDL: Whirlpool = { }, { "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", @@ -2914,6 +3910,33 @@ export const IDL: Whirlpool = { }, { "name": "twoHopSwap", + "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": "tokenProgram", @@ -3046,6 +4069,306 @@ export const IDL: Whirlpool = { "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 + }, + { + "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": "positionBundleTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleOwner", + "isMut": false, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "metadataUpdateAuth", + "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 + } + ], + "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." + ], + "accounts": [ + { + "name": "positionBundle", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleMint", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleOwner", + "isMut": false, + "isSigner": true + }, + { + "name": "receiver", + "isMut": true, + "isSigner": false + }, + { + "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": "bundledPosition", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundle", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "positionBundleAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "whirlpool", + "isMut": false, + "isSigner": false + }, + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "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" + } + ] + }, + { + "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." + ], + "accounts": [ + { + "name": "bundledPosition", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundle", + "isMut": true, + "isSigner": false + }, + { + "name": "positionBundleTokenAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "positionBundleAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "receiver", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "bundleIndex", + "type": "u16" + } + ] } ], "accounts": [ @@ -3093,6 +4416,27 @@ export const IDL: Whirlpool = { ] } }, + { + "name": "positionBundle", + "type": { + "kind": "struct", + "fields": [ + { + "name": "positionBundleMint", + "type": "publicKey" + }, + { + "name": "positionBitmap", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, { "name": "position", "type": { @@ -3361,27 +4705,49 @@ export const IDL: Whirlpool = { }, { "name": "WhirlpoolRewardInfo", + "docs": [ + "Stores the state relevant for tracking liquidity mining rewards at the `Whirlpool` level.", + "These values are used in conjunction with `PositionRewardInfo`, `Tick.reward_growths_outside`,", + "and `Whirlpool.reward_last_updated_timestamp` to determine how many rewards are earned by open", + "positions." + ], "type": { "kind": "struct", "fields": [ { "name": "mint", + "docs": [ + "Reward token mint." + ], "type": "publicKey" }, { "name": "vault", + "docs": [ + "Reward vault token account." + ], "type": "publicKey" }, { "name": "authority", + "docs": [ + "Authority account that has permission to initialize the reward and set emissions." + ], "type": "publicKey" }, { "name": "emissionsPerSecondX64", + "docs": [ + "Q64.64 number that indicates how many tokens per second are earned per unit of liquidity." + ], "type": "u128" }, { "name": "growthGlobalX64", + "docs": [ + "Q64.64 number that tracks the total tokens earned per unit of liquidity since the reward", + "emissions were turned on." + ], "type": "u128" } ] @@ -3660,6 +5026,26 @@ export const IDL: Whirlpool = { "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" } ] }; diff --git a/sdk/src/instructions/close-bundled-position-ix.ts b/sdk/src/instructions/close-bundled-position-ix.ts new file mode 100644 index 0000000..a2f4003 --- /dev/null +++ b/sdk/src/instructions/close-bundled-position-ix.ts @@ -0,0 +1,66 @@ +import { Instruction } from "@orca-so/common-sdk"; +import { PublicKey } from "@solana/web3.js"; +import { Program } from "@project-serum/anchor"; +import { Whirlpool } from "../artifacts/whirlpool"; + +/** + * Parameters to close a bundled position in a Whirlpool. + * + * @category Instruction Types + * @param bundledPosition - PublicKey for the bundled position. + * @param positionBundle - PublicKey for the position bundle. + * @param positionBundleTokenAccount - The associated token address for the position bundle token in the owners wallet. + * @param positionBundleAuthority - authority that owns the token corresponding to this desired bundled position. + * @param bundleIndex - The bundle index that holds the bundled position. + * @param receiver - PublicKey for the wallet that will receive the rented lamports. + */ +export type CloseBundledPositionParams = { + bundledPosition: PublicKey; + positionBundle: PublicKey; + positionBundleTokenAccount: PublicKey; + positionBundleAuthority: PublicKey; + bundleIndex: number; + receiver: PublicKey; +}; + +/** + * Close a bundled position in a Whirlpool. + * + * #### Special Errors + * `InvalidBundleIndex` - If the provided bundle index is out of bounds. + * `ClosePositionNotEmpty` - The provided position account is not empty. + * + * @category Instructions + * @param program - program object containing services required to generate the instruction + * @param params - CloseBundledPositionParams object + * @returns - Instruction to perform the action. + */ +export function closeBundledPositionIx( + program: Program, + params: CloseBundledPositionParams +): Instruction { + const { + bundledPosition, + positionBundle, + positionBundleTokenAccount, + positionBundleAuthority, + bundleIndex, + receiver, + } = params; + + const ix = program.instruction.closeBundledPosition(bundleIndex, { + accounts: { + bundledPosition, + positionBundle, + positionBundleTokenAccount, + positionBundleAuthority, + receiver, + }, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }; +} diff --git a/sdk/src/instructions/delete-position-bundle-ix.ts b/sdk/src/instructions/delete-position-bundle-ix.ts new file mode 100644 index 0000000..6838420 --- /dev/null +++ b/sdk/src/instructions/delete-position-bundle-ix.ts @@ -0,0 +1,64 @@ +import { Program } from "@project-serum/anchor"; +import { Whirlpool } from "../artifacts/whirlpool"; +import { Instruction } from "@orca-so/common-sdk"; +import { PublicKey } from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; + +/** + * Parameters to delete a PositionBundle account. + * + * @category Instruction Types + * @param owner - PublicKey for the wallet that owns the position bundle token. + * @param positionBundle - PublicKey for the position bundle. + * @param positionBundleMint - PublicKey for the mint for the position bundle token. + * @param positionBundleTokenAccount - The associated token address for the position bundle token in the owners wallet. + * @param receiver - PublicKey for the wallet that will receive the rented lamports. + */ +export type DeletePositionBundleParams = { + owner: PublicKey; + positionBundle: PublicKey; + positionBundleMint: PublicKey; + positionBundleTokenAccount: PublicKey; + receiver: PublicKey; +}; + +/** + * Deletes a PositionBundle account. + * + * #### Special Errors + * `PositionBundleNotDeletable` - The provided position bundle has open positions. + * + * @category Instructions + * @param program - program object containing services required to generate the instruction + * @param params - DeletePositionBundleParams object + * @returns - Instruction to perform the action. + */ +export function deletePositionBundleIx( + program: Program, + params: DeletePositionBundleParams +): Instruction { + const { + owner, + positionBundle, + positionBundleMint, + positionBundleTokenAccount, + receiver, + } = params; + + const ix = program.instruction.deletePositionBundle({ + accounts: { + positionBundle: positionBundle, + positionBundleMint: positionBundleMint, + positionBundleTokenAccount, + positionBundleOwner: owner, + receiver, + tokenProgram: TOKEN_PROGRAM_ID, + }, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }; +} diff --git a/sdk/src/instructions/index.ts b/sdk/src/instructions/index.ts index 9255a8b..b22aee1 100644 --- a/sdk/src/instructions/index.ts +++ b/sdk/src/instructions/index.ts @@ -1,15 +1,19 @@ +export * from "./close-bundled-position-ix"; export * from "./close-position-ix"; export * from "./collect-fees-ix"; export * from "./collect-protocol-fees-ix"; export * from "./collect-reward-ix"; export * from "./composites"; export * from "./decrease-liquidity-ix"; +export * from "./delete-position-bundle-ix"; export * from "./increase-liquidity-ix"; export * from "./initialize-config-ix"; export * from "./initialize-fee-tier-ix"; export * from "./initialize-pool-ix"; +export * from "./initialize-position-bundle-ix"; export * from "./initialize-reward-ix"; export * from "./initialize-tick-array-ix"; +export * from "./open-bundled-position-ix"; export * from "./open-position-ix"; export * from "./set-collect-protocol-fees-authority-ix"; export * from "./set-default-fee-rate-ix"; diff --git a/sdk/src/instructions/initialize-position-bundle-ix.ts b/sdk/src/instructions/initialize-position-bundle-ix.ts new file mode 100644 index 0000000..7bd95ed --- /dev/null +++ b/sdk/src/instructions/initialize-position-bundle-ix.ts @@ -0,0 +1,113 @@ +import { Program } from "@project-serum/anchor"; +import { Whirlpool } from "../artifacts/whirlpool"; +import { Instruction } from "@orca-so/common-sdk"; +import * as anchor from "@project-serum/anchor"; +import { PublicKey, SystemProgram, Keypair } from "@solana/web3.js"; +import { PDA } from "@orca-so/common-sdk"; +import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { METADATA_PROGRAM_ADDRESS, WHIRLPOOL_NFT_UPDATE_AUTH } from ".."; + +/** + * Parameters to initialize a PositionBundle account. + * + * @category Instruction Types + * @param owner - PublicKey for the wallet that will host the minted position bundle token. + * @param positionBundlePda - PDA for the derived position bundle address. + * @param positionBundleMintKeypair - Keypair for the mint for the position bundle token. + * @param positionBundleTokenAccount - The associated token address for the position bundle token in the owners wallet. + * @param funder - The account that would fund the creation of this account + */ +export type InitializePositionBundleParams = { + owner: PublicKey; + positionBundlePda: PDA; + positionBundleMintKeypair: Keypair; + positionBundleTokenAccount: PublicKey; + funder: PublicKey; +}; + +/** + * Initializes a PositionBundle account. + * + * @category Instructions + * @param program - program object containing services required to generate the instruction + * @param params - InitializePositionBundleParams object + * @returns - Instruction to perform the action. + */ +export function initializePositionBundleIx( + program: Program, + params: InitializePositionBundleParams +): Instruction { + const { + owner, + positionBundlePda, + positionBundleMintKeypair, + positionBundleTokenAccount, + funder, + } = params; + + const ix = program.instruction.initializePositionBundle({ + accounts: { + positionBundle: positionBundlePda.publicKey, + positionBundleMint: positionBundleMintKeypair.publicKey, + positionBundleTokenAccount, + positionBundleOwner: owner, + funder, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + tokenProgram: TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [positionBundleMintKeypair], + }; +} + +/** + * Initializes a PositionBundle account. + * Additional Metaplex metadata is appended to identify the token. + * + * @category Instructions + * @param program - program object containing services required to generate the instruction + * @param params - InitializePositionBundleParams object + * @returns - Instruction to perform the action. + */ + export function initializePositionBundleWithMetadataIx( + program: Program, + params: InitializePositionBundleParams & { positionBundleMetadataPda: PDA } +): Instruction { + const { + owner, + positionBundlePda, + positionBundleMintKeypair, + positionBundleTokenAccount, + positionBundleMetadataPda, + funder, + } = params; + + const ix = program.instruction.initializePositionBundleWithMetadata({ + accounts: { + positionBundle: positionBundlePda.publicKey, + positionBundleMint: positionBundleMintKeypair.publicKey, + positionBundleMetadata: positionBundleMetadataPda.publicKey, + positionBundleTokenAccount, + positionBundleOwner: owner, + funder, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + tokenProgram: TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + metadataProgram: METADATA_PROGRAM_ADDRESS, + metadataUpdateAuth: WHIRLPOOL_NFT_UPDATE_AUTH, + }, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [positionBundleMintKeypair], + }; +} diff --git a/sdk/src/instructions/open-bundled-position-ix.ts b/sdk/src/instructions/open-bundled-position-ix.ts new file mode 100644 index 0000000..fe0d0f1 --- /dev/null +++ b/sdk/src/instructions/open-bundled-position-ix.ts @@ -0,0 +1,81 @@ +import { Program } from "@project-serum/anchor"; +import { Whirlpool } from "../artifacts/whirlpool"; +import { PublicKey, SystemProgram } from "@solana/web3.js"; +import { PDA, Instruction } from "@orca-so/common-sdk"; +import * as anchor from "@project-serum/anchor"; + +/** + * Parameters to open a bundled position in a Whirlpool. + * + * @category Instruction Types + * @param whirlpool - PublicKey for the whirlpool that the bundled position will be opened for. + * @param bundledPositionPda - PDA for the derived bundled position address. + * @param positionBundle - PublicKey for the position bundle. + * @param positionBundleTokenAccount - The associated token address for the position bundle token in the owners wallet. + * @param positionBundleAuthority - authority that owns the token corresponding to this desired bundled position. + * @param bundleIndex - The bundle index that holds the bundled position. + * @param tickLowerIndex - The tick specifying the lower end of the bundled position range. + * @param tickUpperIndex - The tick specifying the upper end of the bundled position range. + * @param funder - The account that would fund the creation of this account + */ +export type OpenBundledPositionParams = { + whirlpool: PublicKey; + bundledPositionPda: PDA; + positionBundle: PublicKey; + positionBundleTokenAccount: PublicKey; + positionBundleAuthority: PublicKey; + bundleIndex: number; + tickLowerIndex: number; + tickUpperIndex: number; + funder: PublicKey; +}; + +/** + * 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. + * + * #### 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. + * + * @category Instructions + * @param program - program object containing services required to generate the instruction + * @param params - OpenBundledPositionParams object + * @returns - Instruction to perform the action. + */ +export function openBundledPositionIx( + program: Program, + params: OpenBundledPositionParams +): Instruction { + const { + whirlpool, + bundledPositionPda, + positionBundle, + positionBundleTokenAccount, + positionBundleAuthority, + bundleIndex, + tickLowerIndex, + tickUpperIndex, + funder, + } = params; + + const ix = program.instruction.openBundledPosition(bundleIndex, tickLowerIndex, tickUpperIndex, { + accounts: { + bundledPosition: bundledPositionPda.publicKey, + positionBundle, + positionBundleTokenAccount, + positionBundleAuthority, + whirlpool, + funder, + systemProgram: SystemProgram.programId, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + }); + + return { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }; +} diff --git a/sdk/src/instructions/open-position-ix.ts b/sdk/src/instructions/open-position-ix.ts index 868e00e..4759f7f 100644 --- a/sdk/src/instructions/open-position-ix.ts +++ b/sdk/src/instructions/open-position-ix.ts @@ -2,7 +2,7 @@ import { Program } from "@project-serum/anchor"; import { Whirlpool } from "../artifacts/whirlpool"; import { PublicKey } from "@solana/web3.js"; import { PDA, Instruction } from "@orca-so/common-sdk"; -import { METADATA_PROGRAM_ADDRESS } from ".."; +import { METADATA_PROGRAM_ADDRESS, WHIRLPOOL_NFT_UPDATE_AUTH } from ".."; import { OpenPositionBumpsData, OpenPositionWithMetadataBumpsData, @@ -96,7 +96,7 @@ export function openPositionWithMetadataIx( ...openPositionAccounts(params), positionMetadataAccount: metadataPda.publicKey, metadataProgram: METADATA_PROGRAM_ADDRESS, - metadataUpdateAuth: new PublicKey("3axbTs2z5GBy6usVbNVoqEgZMng3vZvMnAoX29BFfwhr"), + metadataUpdateAuth: WHIRLPOOL_NFT_UPDATE_AUTH, }, }); diff --git a/sdk/src/ix.ts b/sdk/src/ix.ts index 261ae1a..8edb24a 100644 --- a/sdk/src/ix.ts +++ b/sdk/src/ix.ts @@ -88,7 +88,6 @@ export class WhirlpoolIx { * #### 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. * - * @param program - program object containing services required to generate the instruction * @param params - OpenPositionParams object * @returns - Instruction to perform the action. @@ -105,7 +104,6 @@ export class WhirlpoolIx { * #### 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. * - * @param program - program object containing services required to generate the instruction * @param params - OpenPositionParams object and a derived PDA that hosts the position's metadata. * @returns - Instruction to perform the action. @@ -158,7 +156,6 @@ export class WhirlpoolIx { /** * Close a position in a Whirlpool. Burns the position token in the owner's wallet. * - * @param program - program object containing services required to generate the instruction * @param params - ClosePositionParams object * @returns - Instruction to perform the action. @@ -261,7 +258,6 @@ export class WhirlpoolIx { * Collect rewards accrued for this reward index in a position. * Call updateFeesAndRewards before this to update the position to the newest accrued values. * - * @param program - program object containing services required to generate the instruction * @param params - CollectRewardParams object * @returns - Instruction to perform the action. @@ -441,6 +437,90 @@ export class WhirlpoolIx { return ix.setRewardEmissionsSuperAuthorityIx(program, params); } + /** + * Initializes a PositionBundle account. + * + * @param program - program object containing services required to generate the instruction + * @param params - InitializePositionBundleParams object + * @returns - Instruction to perform the action. + */ + public static initializePositionBundleIx( + program: Program, + params: ix.InitializePositionBundleParams + ) { + return ix.initializePositionBundleIx(program, params); + } + + /** + * Initializes a PositionBundle account. + * Additional Metaplex metadata is appended to identify the token. + * + * @param program - program object containing services required to generate the instruction + * @param params - InitializePositionBundleParams object + * @returns - Instruction to perform the action. + */ + public static initializePositionBundleWithMetadataIx( + program: Program, + params: ix.InitializePositionBundleParams & { positionBundleMetadataPda: PDA } + ) { + return ix.initializePositionBundleWithMetadataIx(program, params); + } + + /** + * Deletes a PositionBundle account. + * + * #### Special Errors + * `PositionBundleNotDeletable` - The provided position bundle has open positions. + * + * @param program - program object containing services required to generate the instruction + * @param params - DeletePositionBundleParams object + * @returns - Instruction to perform the action. + */ + public static deletePositionBundleIx( + program: Program, + params: ix.DeletePositionBundleParams + ) { + return ix.deletePositionBundleIx(program, params); + } + + /** + * 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. + * + * #### 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. + * + * @param program - program object containing services required to generate the instruction + * @param params - OpenBundledPositionParams object + * @returns - Instruction to perform the action. + */ + public static openBundledPositionIx( + program: Program, + params: ix.OpenBundledPositionParams + ) { + return ix.openBundledPositionIx(program, params); + } + + /** + * Close a bundled position in a Whirlpool. + * + * #### Special Errors + * `InvalidBundleIndex` - If the provided bundle index is out of bounds. + * `ClosePositionNotEmpty` - The provided position account is not empty. + * + * @param program - program object containing services required to generate the instruction + * @param params - CloseBundledPositionParams object + * @returns - Instruction to perform the action. + */ + public static closeBundledPositionIx( + program: Program, + params: ix.CloseBundledPositionParams + ) { + return ix.closeBundledPositionIx(program, params); + } + /** * DEPRECATED - use ${@link WhirlpoolClient} collectFeesAndRewardsForPositions function * A set of transactions to collect all fees and rewards from a list of positions. diff --git a/sdk/src/network/public/fetcher.ts b/sdk/src/network/public/fetcher.ts index 2a925d5..9be1e03 100644 --- a/sdk/src/network/public/fetcher.ts +++ b/sdk/src/network/public/fetcher.ts @@ -5,6 +5,7 @@ import { Connection, PublicKey } from "@solana/web3.js"; import invariant from "tiny-invariant"; import { AccountName, + PositionBundleData, PositionData, TickArrayData, WhirlpoolData, @@ -18,6 +19,7 @@ import { ParsableFeeTier, ParsableMintInfo, ParsablePosition, + ParsablePositionBundle, ParsableTickArray, ParsableTokenInfo, ParsableWhirlpool, @@ -33,6 +35,7 @@ type CachedValue = | PositionData | TickArrayData | FeeTierData + | PositionBundleData | AccountInfo | MintInfo; @@ -181,6 +184,17 @@ export class AccountFetcher { return this.get(AddressUtil.toPubKey(address), ParsableWhirlpoolsConfig, refresh); } + /** + * Retrieve a cached position bundle account. Fetch from rpc on cache miss. + * + * @param address position bundle address + * @param refresh force cache refresh + * @returns position bundle account + */ + public async getPositionBundle(address: Address, refresh = false): Promise { + return this.get(AddressUtil.toPubKey(address), ParsablePositionBundle, refresh); + } + /** * Retrieve a list of cached whirlpool accounts. Fetch from rpc for cache misses. * @@ -284,6 +298,20 @@ export class AccountFetcher { return this.list(AddressUtil.toPubKeys(addresses), ParsableMintInfo, refresh); } + /** + * Retrieve a list of cached position bundle accounts. Fetch from rpc for cache misses. + * + * @param addresses position bundle addresses + * @param refresh force cache refresh + * @returns position bundle accounts + */ + public async listPositionBundles( + addresses: Address[], + refresh: boolean + ): Promise<(PositionBundleData | null)[]> { + return this.list(AddressUtil.toPubKeys(addresses), ParsablePositionBundle, refresh); + } + /** * Update the cached value of all entities currently in the cache. * Uses batched rpc request for network efficient fetch. diff --git a/sdk/src/network/public/parsing.ts b/sdk/src/network/public/parsing.ts index 32282ed..097ca68 100644 --- a/sdk/src/network/public/parsing.ts +++ b/sdk/src/network/public/parsing.ts @@ -7,6 +7,7 @@ import { TickArrayData, AccountName, FeeTierData, + PositionBundleData, } from "../../types/public"; import { BorshAccountsCoder, Idl } from "@project-serum/anchor"; import * as WhirlpoolIDL from "../../artifacts/whirlpool.json"; @@ -131,6 +132,27 @@ export class ParsableFeeTier { } } +/** + * @category Parsables + */ +@staticImplements>() +export class ParsablePositionBundle { + private constructor() {} + + public static parse(data: Buffer | undefined | null): PositionBundleData | null { + if (!data) { + return null; + } + + try { + return parseAnchorAccount(AccountName.PositionBundle, data); + } catch (e) { + console.error(`error while parsing PositionBundle: ${e}`); + return null; + } + } +} + /** * @category Parsables */ @@ -173,7 +195,7 @@ export class ParsableMintInfo { decimals: buffer.decimals, isInitialized: buffer.isInitialized !== 0, freezeAuthority: - buffer.freezeAuthority === 0 ? null : new PublicKey(buffer.freezeAuthority), + buffer.freezeAuthorityOption === 0 ? null : new PublicKey(buffer.freezeAuthority), }; return mintInfo; diff --git a/sdk/src/types/public/anchor-types.ts b/sdk/src/types/public/anchor-types.ts index d5ec5d9..99bbe1a 100644 --- a/sdk/src/types/public/anchor-types.ts +++ b/sdk/src/types/public/anchor-types.ts @@ -20,6 +20,7 @@ export enum AccountName { TickArray = "TickArray", Whirlpool = "Whirlpool", FeeTier = "FeeTier", + PositionBundle = "PositionBundle", } const IDL = WhirlpoolIDL as Idl; @@ -157,3 +158,11 @@ export type FeeTierData = { tickSpacing: number; defaultFeeRate: number; }; + +/** + * @category Solana Accounts + */ +export type PositionBundleData = { + positionBundleMint: PublicKey; + positionBitmap: number[]; +}; diff --git a/sdk/src/types/public/constants.ts b/sdk/src/types/public/constants.ts index 0453653..e964ad0 100644 --- a/sdk/src/types/public/constants.ts +++ b/sdk/src/types/public/constants.ts @@ -57,6 +57,12 @@ export const MIN_SQRT_PRICE = "4295048016"; */ export const TICK_ARRAY_SIZE = 88; +/** + * The number of bundled positions that a position-bundle account can hold. + * @category Constants + */ +export const POSITION_BUNDLE_SIZE = 256; + /** * @category Constants */ @@ -81,3 +87,11 @@ export const PROTOCOL_FEE_RATE_MUL_VALUE = new BN(10_000); * @category Constants */ export const FEE_RATE_MUL_VALUE = new BN(1_000_000); + +/** + * The public key that is allowed to update the metadata of Whirlpool NFTs. + * @category Constants + */ +export const WHIRLPOOL_NFT_UPDATE_AUTH = new PublicKey( + "3axbTs2z5GBy6usVbNVoqEgZMng3vZvMnAoX29BFfwhr" +); diff --git a/sdk/src/types/public/ix-types.ts b/sdk/src/types/public/ix-types.ts index 4b76e93..9b97742 100644 --- a/sdk/src/types/public/ix-types.ts +++ b/sdk/src/types/public/ix-types.ts @@ -27,6 +27,10 @@ export { SwapInput, SwapParams, UpdateFeesAndRewardsParams, + InitializePositionBundleParams, + DeletePositionBundleParams, + OpenBundledPositionParams, + CloseBundledPositionParams, } from "../../instructions/"; export { CollectAllParams, diff --git a/sdk/src/utils/public/index.ts b/sdk/src/utils/public/index.ts index aa264cf..085ae49 100644 --- a/sdk/src/utils/public/index.ts +++ b/sdk/src/utils/public/index.ts @@ -2,6 +2,7 @@ export * from "../graphs/public"; export * from "./ix-utils"; export * from "./pda-utils"; export * from "./pool-utils"; +export * from "./position-bundle-util"; export * from "./price-math"; export * from "./swap-utils"; export * from "./tick-utils"; diff --git a/sdk/src/utils/public/pda-utils.ts b/sdk/src/utils/public/pda-utils.ts index 4cef222..8ff109b 100644 --- a/sdk/src/utils/public/pda-utils.ts +++ b/sdk/src/utils/public/pda-utils.ts @@ -11,6 +11,8 @@ const PDA_METADATA_SEED = "metadata"; const PDA_TICK_ARRAY_SEED = "tick_array"; 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"; /** * @category Whirlpool Utils @@ -168,4 +170,61 @@ export class PDAUtil { programId ); } + + /** + * @category Program Derived Addresses + * @param programId + * @param positionBundleMintKey + * @param bundleIndex + * @returns + */ + public static getBundledPosition( + programId: PublicKey, + positionBundleMintKey: PublicKey, + bundleIndex: number + ) { + return AddressUtil.findProgramAddress( + [ + Buffer.from(PDA_BUNDLED_POSITION_SEED), + positionBundleMintKey.toBuffer(), + Buffer.from(bundleIndex.toString()), + ], + programId + ); + } + + /** + * @category Program Derived Addresses + * @param programId + * @param positionBundleMintKey + * @returns + */ + public static getPositionBundle( + programId: PublicKey, + positionBundleMintKey: PublicKey, + ) { + return AddressUtil.findProgramAddress( + [ + Buffer.from(PDA_POSITION_BUNDLE_SEED), + positionBundleMintKey.toBuffer(), + ], + programId + ); + } + + /** + * @category Program Derived Addresses + * @param positionBundleMintKey + * @returns + */ + public static getPositionBundleMetadata(positionBundleMintKey: PublicKey) { + return AddressUtil.findProgramAddress( + [ + Buffer.from(PDA_METADATA_SEED), + METADATA_PROGRAM_ADDRESS.toBuffer(), + positionBundleMintKey.toBuffer(), + ], + METADATA_PROGRAM_ADDRESS + ); + } } diff --git a/sdk/src/utils/public/position-bundle-util.ts b/sdk/src/utils/public/position-bundle-util.ts new file mode 100644 index 0000000..b85f2fc --- /dev/null +++ b/sdk/src/utils/public/position-bundle-util.ts @@ -0,0 +1,129 @@ +import invariant from "tiny-invariant"; +import { + PositionBundleData, + POSITION_BUNDLE_SIZE, +} from "../../types/public"; + + +/** + * A collection of utility functions when interacting with a PositionBundle. + * @category Whirlpool Utils + */ +export class PositionBundleUtil { + private constructor() {} + + /** + * Check if the bundle index is in the correct range. + * + * @param bundleIndex The bundle index to be checked + * @returns true if bundle index is in the correct range + */ + public static checkBundleIndexInBounds(bundleIndex: number): boolean { + return bundleIndex >= 0 && bundleIndex < POSITION_BUNDLE_SIZE; + } + + /** + * Check if the Bundled Position corresponding to the bundle index has been opened. + * + * @param positionBundle The position bundle to be checked + * @param bundleIndex The bundle index to be checked + * @returns true if Bundled Position has been opened + */ + public static isOccupied(positionBundle: PositionBundleData, bundleIndex: number): boolean { + invariant(PositionBundleUtil.checkBundleIndexInBounds(bundleIndex), "bundleIndex out of range"); + const array = PositionBundleUtil.convertBitmapToArray(positionBundle); + return array[bundleIndex]; + } + + /** + * Check if the Bundled Position corresponding to the bundle index has not been opened. + * + * @param positionBundle The position bundle to be checked + * @param bundleIndex The bundle index to be checked + * @returns true if Bundled Position has not been opened + */ + public static isUnoccupied(positionBundle: PositionBundleData, bundleIndex: number): boolean { + return !PositionBundleUtil.isOccupied(positionBundle, bundleIndex); + } + + /** + * Check if all bundle index is occupied. + * + * @param positionBundle The position bundle to be checked + * @returns true if all bundle index is occupied + */ + public static isFull(positionBundle: PositionBundleData): boolean { + const unoccupied = PositionBundleUtil.getUnoccupiedBundleIndexes(positionBundle); + return unoccupied.length === 0; + } + + /** + * Check if all bundle index is unoccupied. + * + * @param positionBundle The position bundle to be checked + * @returns true if all bundle index is unoccupied + */ + public static isEmpty(positionBundle: PositionBundleData): boolean { + const occupied = PositionBundleUtil.getOccupiedBundleIndexes(positionBundle); + return occupied.length === 0; + } + + /** + * Get all bundle indexes where the corresponding Bundled Position is open. + * + * @param positionBundle The position bundle to be checked + * @returns The array of bundle index where the corresponding Bundled Position is open + */ + public static getOccupiedBundleIndexes(positionBundle: PositionBundleData): number[] { + const result: number[] = []; + PositionBundleUtil.convertBitmapToArray(positionBundle).forEach((occupied, index) => { + if (occupied) { + result.push(index); + } + }) + return result; + } + + /** + * Get all bundle indexes where the corresponding Bundled Position is not open. + * + * @param positionBundle The position bundle to be checked + * @returns The array of bundle index where the corresponding Bundled Position is not open + */ + public static getUnoccupiedBundleIndexes(positionBundle: PositionBundleData): number[] { + const result: number[] = []; + PositionBundleUtil.convertBitmapToArray(positionBundle).forEach((occupied, index) => { + if (!occupied) { + result.push(index); + } + }) + return result; + } + + /** + * Get the first unoccupied bundle index in the position bundle. + * + * @param positionBundle The position bundle to be checked + * @returns The first unoccupied bundle index, null if the position bundle is full + */ + public static findUnoccupiedBundleIndex(positionBundle: PositionBundleData): number|null { + const unoccupied = PositionBundleUtil.getUnoccupiedBundleIndexes(positionBundle); + return unoccupied.length === 0 ? null : unoccupied[0]; + } + + /** + * Convert position bitmap to the array of boolean which represent if Bundled Position is open. + * + * @param positionBundle The position bundle whose bitmap will be converted + * @returns The array of boolean representing if Bundled Position is open + */ + public static convertBitmapToArray(positionBundle: PositionBundleData): boolean[] { + const result: boolean[] = []; + positionBundle.positionBitmap.map((bitmap) => { + for (let offset=0; offset<8; offset++) { + result.push((bitmap & (1 << offset)) !== 0); + } + }) + return result; + } +} diff --git a/sdk/tests/integration/close_bundled_position.test.ts b/sdk/tests/integration/close_bundled_position.test.ts new file mode 100644 index 0000000..9031f58 --- /dev/null +++ b/sdk/tests/integration/close_bundled_position.test.ts @@ -0,0 +1,642 @@ +import * as anchor from "@project-serum/anchor"; +import * as assert from "assert"; +import { + buildWhirlpoolClient, + increaseLiquidityQuoteByInputTokenWithParams, + InitPoolParams, + PositionBundleData, + POSITION_BUNDLE_SIZE, + toTx, + WhirlpoolContext, + WhirlpoolIx, +} from "../../src"; +import { + approveToken, + TickSpacing, + transfer, + ONE_SOL, + systemTransferTx, + createAssociatedTokenAccount, +} from "../utils"; +import { PDA, Percentage } from "@orca-so/common-sdk"; +import { initializePositionBundle, initTestPool, openBundledPosition, openPosition } from "../utils/init-utils"; +import { u64 } from "@solana/spl-token"; +import { mintTokensToTestAccount } from "../utils/test-builders"; + +describe("close_bundled_position", () => { + const provider = anchor.AnchorProvider.local(undefined, { + commitment: "confirmed", + preflightCommitment: "confirmed", + }); + + anchor.setProvider(anchor.AnchorProvider.env()); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const client = buildWhirlpoolClient(ctx); + const fetcher = ctx.fetcher; + + const tickLowerIndex = 0; + const tickUpperIndex = 128; + let poolInitInfo: InitPoolParams; + let whirlpoolPda: PDA; + const funderKeypair = anchor.web3.Keypair.generate(); + + before(async () => { + poolInitInfo = (await initTestPool(ctx, TickSpacing.Standard)).poolInitInfo; + whirlpoolPda = poolInitInfo.whirlpoolPda; + await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute(); + + const pool = await client.getPool(whirlpoolPda.publicKey); + await (await pool.initTickArrayForTicks([0]))?.buildAndExecute(); + }); + + function checkBitmapIsOpened(account: PositionBundleData, bundleIndex: number): boolean { + if (bundleIndex < 0 || bundleIndex >= POSITION_BUNDLE_SIZE) throw Error("bundleIndex is out of bounds"); + + const bitmapIndex = Math.floor(bundleIndex / 8); + const bitmapOffset = bundleIndex % 8; + return (account.positionBitmap[bitmapIndex] & (1 << bitmapOffset)) > 0; + } + + function checkBitmapIsClosed(account: PositionBundleData, bundleIndex: number): boolean { + if (bundleIndex < 0 || bundleIndex >= POSITION_BUNDLE_SIZE) throw Error("bundleIndex is out of bounds"); + + const bitmapIndex = Math.floor(bundleIndex / 8); + const bitmapOffset = bundleIndex % 8; + return (account.positionBitmap[bitmapIndex] & (1 << bitmapOffset)) === 0; + } + + function checkBitmap(account: PositionBundleData, openedBundleIndexes: number[]) { + for (let i=0; i { + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const bundleIndex = 0; + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + const { bundledPositionPda } = positionInitInfo.params; + + const preAccount = await fetcher.getPosition(bundledPositionPda.publicKey, true); + const prePositionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + checkBitmap(prePositionBundle!, [bundleIndex]); + assert.ok(preAccount !== null); + + const receiverKeypair = anchor.web3.Keypair.generate(); + await toTx( + ctx, + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: bundledPositionPda.publicKey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: receiverKeypair.publicKey, + }) + ).buildAndExecute(); + + const postAccount = await fetcher.getPosition(bundledPositionPda.publicKey, true); + const postPositionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + checkBitmap(postPositionBundle!, []); + assert.ok(postAccount === null); + + const receiverAccount = await provider.connection.getAccountInfo(receiverKeypair.publicKey); + const lamports = receiverAccount?.lamports; + assert.ok(lamports != undefined && lamports > 0); + }); + + it("should be failed: invalid bundle index", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const bundleIndex = 0; + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + const { bundledPositionPda } = positionInitInfo.params; + + const tx = await toTx( + ctx, + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: bundledPositionPda.publicKey, + bundleIndex: 1, // invalid + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: ctx.wallet.publicKey, + }) + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x7d6/ // ConstraintSeeds (seed constraint was violated) + ); + }); + + it("should be failed: user closes bundled position already closed", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const bundleIndex = 0; + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + const { bundledPositionPda } = positionInitInfo.params; + + const tx = toTx( + ctx, + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: bundledPositionPda.publicKey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: ctx.wallet.publicKey, + }) + ); + + // close... + await tx.buildAndExecute(); + + // re-close... + await assert.rejects( + tx.buildAndExecute(), + /0xbc4/ // AccountNotInitialized + ); + }); + + it("should be failed: bundled position is not empty", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const bundleIndex = 0; + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + const { bundledPositionPda } = positionInitInfo.params; + + // deposit + const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey, true); + const quote = increaseLiquidityQuoteByInputTokenWithParams({ + tokenMintA: poolInitInfo.tokenMintA, + tokenMintB: poolInitInfo.tokenMintB, + sqrtPrice: pool.getData().sqrtPrice, + slippageTolerance: Percentage.fromFraction(0, 100), + tickLowerIndex, + tickUpperIndex, + tickCurrentIndex: pool.getData().tickCurrentIndex, + inputTokenMint: poolInitInfo.tokenMintB, + inputTokenAmount: new u64(1_000_000), + }); + + await mintTokensToTestAccount( + provider, + poolInitInfo.tokenMintA, + quote.tokenMaxA.toNumber(), + poolInitInfo.tokenMintB, + quote.tokenMaxB.toNumber(), + ctx.wallet.publicKey + ); + + const position = await client.getPosition(bundledPositionPda.publicKey, true); + await (await position.increaseLiquidity(quote)).buildAndExecute(); + assert.ok((await position.refreshData()).liquidity.gtn(0)); + + // try to close... + const tx = toTx( + ctx, + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: bundledPositionPda.publicKey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: ctx.wallet.publicKey, + }) + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x1775/ // ClosePositionNotEmpty + ); + }); + + describe("invalid input account", () => { + it("should be failed: invalid bundled position", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const positionInitInfo0 = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + 0, + tickLowerIndex, + tickUpperIndex + ); + + const positionInitInfo1 = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + 1, + tickLowerIndex, + tickUpperIndex + ); + + const tx = toTx( + ctx, + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: positionInitInfo1.params.bundledPositionPda.publicKey, // invalid + bundleIndex: 0, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: ctx.wallet.publicKey, + }) + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x7d6/ // ConstraintSeeds (seed constraint was violated) + ); + }); + + it("should be failed: invalid position bundle", async () => { + const positionBundleInfo0 = await initializePositionBundle(ctx, ctx.wallet.publicKey); + const positionBundleInfo1 = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const bundleIndex = 0; + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo0.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + + const tx = toTx( + ctx, + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: positionInitInfo.params.bundledPositionPda.publicKey, + bundleIndex, + positionBundle: positionBundleInfo1.positionBundlePda.publicKey, // invalid + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo0.positionBundleTokenAccount, + receiver: ctx.wallet.publicKey, + }) + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x7d6/ // ConstraintSeeds (seed constraint was violated) + ); + }); + + it("should be failed: invalid ATA (amount is zero)", async () => { + const positionBundleInfo = await initializePositionBundle(ctx); + + const bundleIndex = 0; + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + + const ata = await createAssociatedTokenAccount( + provider, + positionBundleInfo.positionBundleMintKeypair.publicKey, + funderKeypair.publicKey, + ctx.wallet.publicKey, + ); + + const tx = toTx( + ctx, + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: positionInitInfo.params.bundledPositionPda.publicKey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: ata, // invalid + receiver: ctx.wallet.publicKey, + }) + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x7d3/ // ConstraintRaw (amount == 1) + ); + }); + + it("should be failed: invalid ATA (invalid mint)", async () => { + const positionBundleInfo0 = await initializePositionBundle(ctx, ctx.wallet.publicKey); + const positionBundleInfo1 = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const bundleIndex = 0; + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo0.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + + const tx = toTx( + ctx, + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: positionInitInfo.params.bundledPositionPda.publicKey, + bundleIndex, + positionBundle: positionBundleInfo0.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo1.positionBundleTokenAccount, // invalid + receiver: ctx.wallet.publicKey, + }) + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x7d3/ // ConstraintRaw (mint == position_bundle.position_bundle_mint) + ); + }); + + it("should be failed: invalid position bundle authority", async () => { + const positionBundleInfo = await initializePositionBundle(ctx); + + const bundleIndex = 0; + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + + const tx = toTx( + ctx, + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: positionInitInfo.params.bundledPositionPda.publicKey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: funderKeypair.publicKey, // invalid + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: ctx.wallet.publicKey, + }) + ); + tx.addSigner(funderKeypair); + + await assert.rejects( + tx.buildAndExecute(), + /0x1783/ // MissingOrInvalidDelegate + ); + }); + }); + + describe("authority delegation", () => { + it("successfully closes bundled position with delegated authority", async () => { + const positionBundleInfo = await initializePositionBundle(ctx); + + const bundleIndex = 0; + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + + const tx = toTx( + ctx, + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: positionInitInfo.params.bundledPositionPda.publicKey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: funderKeypair.publicKey, // should be delegated + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: ctx.wallet.publicKey, + }) + ); + tx.addSigner(funderKeypair); + + await assert.rejects( + tx.buildAndExecute(), + /0x1783/ // MissingOrInvalidDelegate + ); + + // delegate 1 token from ctx.wallet to funder + await approveToken( + provider, + positionBundleInfo.positionBundleTokenAccount, + funderKeypair.publicKey, + 1, + ); + + await tx.buildAndExecute(); + const positionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + checkBitmapIsClosed(positionBundle!, 0); + }); + + it("successfully closes bundled position even if delegation exists", async () => { + const positionBundleInfo = await initializePositionBundle(ctx); + + const bundleIndex = 0; + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + + const tx = toTx( + ctx, + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: positionInitInfo.params.bundledPositionPda.publicKey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: ctx.wallet.publicKey, + }) + ); + + // delegate 1 token from ctx.wallet to funder + await approveToken( + provider, + positionBundleInfo.positionBundleTokenAccount, + funderKeypair.publicKey, + 1, + ); + + // owner can close even if delegation exists + await tx.buildAndExecute(); + const positionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + checkBitmapIsClosed(positionBundle!, 0); + }); + + it("should be faild: delegated amount is zero", async () => { + const positionBundleInfo = await initializePositionBundle(ctx); + + const bundleIndex = 0; + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + + const tx = toTx( + ctx, + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: positionInitInfo.params.bundledPositionPda.publicKey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: funderKeypair.publicKey, // should be delegated + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: ctx.wallet.publicKey, + }) + ); + tx.addSigner(funderKeypair); + + await assert.rejects( + tx.buildAndExecute(), + /0x1783/ // MissingOrInvalidDelegate + ); + + // delegate ZERO token from ctx.wallet to funder + await approveToken( + provider, + positionBundleInfo.positionBundleTokenAccount, + funderKeypair.publicKey, + 0, + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x1784/ // InvalidPositionTokenAmount + ); + }); + }); + + describe("transfer position bundle", () => { + it("successfully closes bundled position after position bundle token transfer", async () => { + const positionBundleInfo = await initializePositionBundle(ctx); + + const bundleIndex = 0; + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + + const funderATA = await createAssociatedTokenAccount( + provider, + positionBundleInfo.positionBundleMintKeypair.publicKey, + funderKeypair.publicKey, + ctx.wallet.publicKey, + ); + + await transfer( + provider, + positionBundleInfo.positionBundleTokenAccount, + funderATA, + 1 + ); + + const tokenInfo = await fetcher.getTokenInfo(funderATA, true); + assert.ok(tokenInfo?.amount.eqn(1)); + + const tx = toTx( + ctx, + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: positionInitInfo.params.bundledPositionPda.publicKey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: funderKeypair.publicKey, // new owner + positionBundleTokenAccount: funderATA, + receiver: funderKeypair.publicKey + }) + ); + tx.addSigner(funderKeypair); + + await tx.buildAndExecute(); + const positionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + checkBitmapIsClosed(positionBundle!, 0); + }); + }); + + describe("non-bundled position", () => { + it("should be failed: try to close NON-bundled position", async () => { + const positionBundleInfo = await initializePositionBundle(ctx); + + const bundleIndex = 0; + + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + + // open NON-bundled position + const { params } = await openPosition(ctx, poolInitInfo.whirlpoolPda.publicKey, 0, 128); + + const tx = toTx( + ctx, + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: params.positionPda.publicKey, // NON-bundled position + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: ctx.wallet.publicKey, + }) + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x7d6/ // ConstraintSeeds (seed constraint was violated) + ); + }); + }); + +}); diff --git a/sdk/tests/integration/close_position.test.ts b/sdk/tests/integration/close_position.test.ts index fbd6672..e2c8eb0 100644 --- a/sdk/tests/integration/close_position.test.ts +++ b/sdk/tests/integration/close_position.test.ts @@ -12,7 +12,7 @@ import { ZERO_BN, } from "../utils"; import { WhirlpoolTestFixture } from "../utils/fixture"; -import { initTestPool, initTestPoolWithLiquidity, openPosition } from "../utils/init-utils"; +import { initializePositionBundle, initTestPool, initTestPoolWithLiquidity, openBundledPosition, openPosition } from "../utils/init-utils"; describe("close_position", () => { const provider = anchor.AnchorProvider.local(); @@ -421,7 +421,45 @@ describe("close_position", () => { positionTokenAccount: position.tokenAccount, }) ).buildAndExecute(), - /0x7dc/ // ConstraintAddress + // Seeds constraint added by adding PositionBundle, so ConstraintSeeds will be violated first + /0x7d6/ // ConstraintSeeds (seed constraint was violated) ); }); + + describe("bundled position", () => { + it("fails if position is BUNDLED position", async () => { + const fixture = await new WhirlpoolTestFixture(ctx).init({ + tickSpacing: TickSpacing.Standard, + positions: [], + }); + const { poolInitInfo } = fixture.getInfos(); + + // open bundled position + const positionBundleInfo = await initializePositionBundle(ctx); + const bundleIndex = 0; + const positionInitInfo = await openBundledPosition( + ctx, + poolInitInfo.whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + 0, + 128, + ); + + // try to close bundled position + await assert.rejects( + toTx( + ctx, + WhirlpoolIx.closePositionIx(ctx.program, { + positionAuthority: provider.wallet.publicKey, + receiver: provider.wallet.publicKey, + position: positionInitInfo.params.bundledPositionPda.publicKey, + positionMint: positionBundleInfo.positionBundleMintKeypair.publicKey, + positionTokenAccount: positionBundleInfo.positionBundleTokenAccount, + }) + ).buildAndExecute(), + /0x7d6/ // ConstraintSeeds (seed constraint was violated) + ); + }); + }); }); diff --git a/sdk/tests/integration/delete_position_bundle.test.ts b/sdk/tests/integration/delete_position_bundle.test.ts new file mode 100644 index 0000000..41d87dc --- /dev/null +++ b/sdk/tests/integration/delete_position_bundle.test.ts @@ -0,0 +1,561 @@ +import { PDA } from "@orca-so/common-sdk"; +import * as anchor from "@project-serum/anchor"; +import { ASSOCIATED_TOKEN_PROGRAM_ID, Token, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { Keypair } from "@solana/web3.js"; +import * as assert from "assert"; +import { InitPoolParams, METADATA_PROGRAM_ADDRESS, PositionBundleData, POSITION_BUNDLE_SIZE, toTx, WhirlpoolIx } from "../../src"; +import { WhirlpoolContext } from "../../src/context"; +import { + approveToken, + createAssociatedTokenAccount, + ONE_SOL, + systemTransferTx, + TickSpacing, + transfer, +} from "../utils"; +import { initializePositionBundle, initializePositionBundleWithMetadata, initTestPool, openBundledPosition } from "../utils/init-utils"; + +describe("delete_position_bundle", () => { + const provider = anchor.AnchorProvider.local(undefined, { + commitment: "confirmed", + preflightCommitment: "confirmed", + }); + + anchor.setProvider(anchor.AnchorProvider.env()); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + + const tickLowerIndex = 0; + const tickUpperIndex = 128; + let poolInitInfo: InitPoolParams; + let whirlpoolPda: PDA; + const funderKeypair = anchor.web3.Keypair.generate(); + + before(async () => { + poolInitInfo = (await initTestPool(ctx, TickSpacing.Standard)).poolInitInfo; + whirlpoolPda = poolInitInfo.whirlpoolPda; + await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute(); + }); + + function checkBitmapIsOpened(account: PositionBundleData, bundleIndex: number): boolean { + if (bundleIndex < 0 || bundleIndex >= POSITION_BUNDLE_SIZE) throw Error("bundleIndex is out of bounds"); + + const bitmapIndex = Math.floor(bundleIndex / 8); + const bitmapOffset = bundleIndex % 8; + return (account.positionBitmap[bitmapIndex] & (1 << bitmapOffset)) > 0; + } + + it("successfully closes an position bundle, with metadata", async () => { + // with local-validator, ctx.wallet may have large lamports and it overflows number data type... + const owner = funderKeypair; + + const positionBundleInfo = await initializePositionBundleWithMetadata( + ctx, + owner.publicKey, + owner + ); + + // PositionBundle account exists + const prePositionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + assert.ok(prePositionBundle !== null); + + // NFT supply should be 1 + const preSupplyResponse = await provider.connection.getTokenSupply(positionBundleInfo.positionBundleMintKeypair.publicKey); + assert.equal(preSupplyResponse.value.uiAmount, 1); + + // ATA account exists + assert.notEqual(await provider.connection.getAccountInfo(positionBundleInfo.positionBundleTokenAccount), undefined); + + // Metadata account exists + assert.notEqual(await provider.connection.getAccountInfo(positionBundleInfo.positionBundleMetadataPda.publicKey), undefined); + + const preBalance = await provider.connection.getBalance(owner.publicKey, "confirmed"); + + const rentPositionBundle = await provider.connection.getBalance(positionBundleInfo.positionBundlePda.publicKey, "confirmed"); + const rentTokenAccount = await provider.connection.getBalance(positionBundleInfo.positionBundleTokenAccount, "confirmed"); + + await toTx( + ctx, + WhirlpoolIx.deletePositionBundleIx(ctx.program, { + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + owner: owner.publicKey, + receiver: owner.publicKey + }) + ).addSigner(owner).buildAndExecute(); + + const postBalance = await provider.connection.getBalance(owner.publicKey, "confirmed"); + + // PositionBundle account should be closed + const postPositionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + assert.ok(postPositionBundle === null); + + // NFT should be burned and its supply should be 0 + const supplyResponse = await provider.connection.getTokenSupply(positionBundleInfo.positionBundleMintKeypair.publicKey); + assert.equal(supplyResponse.value.uiAmount, 0); + + // ATA account should be closed + assert.equal(await provider.connection.getAccountInfo(positionBundleInfo.positionBundleTokenAccount), undefined); + + // Metadata account should NOT be closed + assert.notEqual(await provider.connection.getAccountInfo(positionBundleInfo.positionBundleMetadataPda.publicKey), undefined); + + // check if rent are refunded + const diffBalance = postBalance - preBalance; + const rentTotal = rentPositionBundle + rentTokenAccount; + assert.equal(diffBalance, rentTotal); + }); + + it("successfully closes an position bundle, without metadata", async () => { + // with local-validator, ctx.wallet may have large lamports and it overflows number data type... + const owner = funderKeypair; + + const positionBundleInfo = await initializePositionBundle( + ctx, + owner.publicKey, + owner + ); + + // PositionBundle account exists + const prePositionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + assert.ok(prePositionBundle !== null); + + // NFT supply should be 1 + const preSupplyResponse = await provider.connection.getTokenSupply(positionBundleInfo.positionBundleMintKeypair.publicKey); + assert.equal(preSupplyResponse.value.uiAmount, 1); + + // ATA account exists + assert.notEqual(await provider.connection.getAccountInfo(positionBundleInfo.positionBundleTokenAccount), undefined); + + const preBalance = await provider.connection.getBalance(owner.publicKey, "confirmed"); + + const rentPositionBundle = await provider.connection.getBalance(positionBundleInfo.positionBundlePda.publicKey, "confirmed"); + const rentTokenAccount = await provider.connection.getBalance(positionBundleInfo.positionBundleTokenAccount, "confirmed"); + + await toTx( + ctx, + WhirlpoolIx.deletePositionBundleIx(ctx.program, { + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + owner: owner.publicKey, + receiver: owner.publicKey + }) + ).addSigner(owner).buildAndExecute(); + + const postBalance = await provider.connection.getBalance(owner.publicKey, "confirmed"); + + // PositionBundle account should be closed + const postPositionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + assert.ok(postPositionBundle === null); + + // NFT should be burned and its supply should be 0 + const supplyResponse = await provider.connection.getTokenSupply(positionBundleInfo.positionBundleMintKeypair.publicKey); + assert.equal(supplyResponse.value.uiAmount, 0); + + // ATA account should be closed + assert.equal(await provider.connection.getAccountInfo(positionBundleInfo.positionBundleTokenAccount), undefined); + + // check if rent are refunded + const diffBalance = postBalance - preBalance; + const rentTotal = rentPositionBundle + rentTokenAccount; + assert.equal(diffBalance, rentTotal); + }); + + it("successfully closes an position bundle, receiver != owner", async () => { + const receiver = funderKeypair; + + const positionBundleInfo = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + ); + + const preBalance = await provider.connection.getBalance(receiver.publicKey, "confirmed"); + + const rentPositionBundle = await provider.connection.getBalance(positionBundleInfo.positionBundlePda.publicKey, "confirmed"); + const rentTokenAccount = await provider.connection.getBalance(positionBundleInfo.positionBundleTokenAccount, "confirmed"); + + await toTx( + ctx, + WhirlpoolIx.deletePositionBundleIx(ctx.program, { + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + owner: ctx.wallet.publicKey, + receiver: receiver.publicKey + }) + ).buildAndExecute(); + + const postBalance = await provider.connection.getBalance(receiver.publicKey, "confirmed"); + + // check if rent are refunded to receiver + const diffBalance = postBalance - preBalance; + const rentTotal = rentPositionBundle + rentTokenAccount; + assert.equal(diffBalance, rentTotal); + }); + + it("should be failed: position bundle has opened bundled position (bundleIndex = 0)", async () => { + const positionBundleInfo = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + ); + + const bundleIndex = 0; + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + const { bundledPositionPda } = positionInitInfo.params; + + const position = await fetcher.getPosition(positionInitInfo.params.bundledPositionPda.publicKey, true); + assert.equal(position!.tickLowerIndex, tickLowerIndex); + assert.equal(position!.tickUpperIndex, tickUpperIndex); + + const positionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + checkBitmapIsOpened(positionBundle!, bundleIndex); + + const tx = toTx( + ctx, + WhirlpoolIx.deletePositionBundleIx(ctx.program, { + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + owner: ctx.wallet.publicKey, + receiver: ctx.wallet.publicKey, + }) + ); + + // should be failed + await assert.rejects( + tx.buildAndExecute(), + /0x179e/ // PositionBundleNotDeletable + ); + + // close bundled position + await toTx( + ctx, + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: bundledPositionPda.publicKey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: ctx.wallet.publicKey, + }) + ).buildAndExecute(); + + // should be ok + await tx.buildAndExecute(); + const deleted = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + assert.ok(deleted === null); + }); + + it("should be failed: position bundle has opened bundled position (bundleIndex = POSITION_BUNDLE_SIZE - 1)", async () => { + const positionBundleInfo = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + ); + + const bundleIndex = POSITION_BUNDLE_SIZE - 1; + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + const { bundledPositionPda } = positionInitInfo.params; + + const position = await fetcher.getPosition(positionInitInfo.params.bundledPositionPda.publicKey, true); + assert.equal(position!.tickLowerIndex, tickLowerIndex); + assert.equal(position!.tickUpperIndex, tickUpperIndex); + + const positionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + checkBitmapIsOpened(positionBundle!, bundleIndex); + + const tx = toTx( + ctx, + WhirlpoolIx.deletePositionBundleIx(ctx.program, { + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + owner: ctx.wallet.publicKey, + receiver: ctx.wallet.publicKey, + }) + ); + + // should be failed + await assert.rejects( + tx.buildAndExecute(), + /0x179e/ // PositionBundleNotDeletable + ); + + // close bundled position + await toTx( + ctx, + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: bundledPositionPda.publicKey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: ctx.wallet.publicKey, + }) + ).buildAndExecute(); + + // should be ok + await tx.buildAndExecute(); + const deleted = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + assert.ok(deleted === null); + }); + + it("should be failed: only owner can delete position bundle, delegated user cannot", async () => { + const positionBundleInfo = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + ); + + const delegate = Keypair.generate(); + await approveToken( + provider, + positionBundleInfo.positionBundleTokenAccount, + delegate.publicKey, + 1 + ); + + const tx = toTx( + ctx, + WhirlpoolIx.deletePositionBundleIx(ctx.program, { + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + owner: delegate.publicKey, // not owner + receiver: ctx.wallet.publicKey, + }) + ).addSigner(delegate); + + // should be failed + await assert.rejects( + tx.buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + + // ownership transfer to delegate + const delegateTokenAccount = await createAssociatedTokenAccount( + provider, + positionBundleInfo.positionBundleMintKeypair.publicKey, + delegate.publicKey, + ctx.wallet.publicKey + ); + await transfer( + provider, + positionBundleInfo.positionBundleTokenAccount, + delegateTokenAccount, + 1 + ); + + const txAfterTransfer = toTx( + ctx, + WhirlpoolIx.deletePositionBundleIx(ctx.program, { + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey, + positionBundleTokenAccount: delegateTokenAccount, + owner: delegate.publicKey, // now, delegate is owner + receiver: ctx.wallet.publicKey, + }) + ).addSigner(delegate); + + await txAfterTransfer.buildAndExecute(); + const deleted = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + assert.ok(deleted === null); + }); + + describe("invalid input account", () => { + it("should be failed: invalid position bundle", async () => { + const positionBundleInfo1 = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + ); + const positionBundleInfo2 = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + ); + + const tx = toTx( + ctx, + WhirlpoolIx.deletePositionBundleIx(ctx.program, { + positionBundle: positionBundleInfo2.positionBundlePda.publicKey, // invalid + positionBundleMint: positionBundleInfo1.positionBundleMintKeypair.publicKey, + positionBundleTokenAccount: positionBundleInfo1.positionBundleTokenAccount, + owner: ctx.wallet.publicKey, + receiver: ctx.wallet.publicKey, + }) + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("should be failed: invalid position bundle mint", async () => { + const positionBundleInfo1 = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + ); + const positionBundleInfo2 = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + ); + + const tx = toTx( + ctx, + WhirlpoolIx.deletePositionBundleIx(ctx.program, { + positionBundle: positionBundleInfo1.positionBundlePda.publicKey, + positionBundleMint: positionBundleInfo2.positionBundleMintKeypair.publicKey, // invalid + positionBundleTokenAccount: positionBundleInfo1.positionBundleTokenAccount, + owner: ctx.wallet.publicKey, + receiver: ctx.wallet.publicKey, + }) + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("should be failed: invalid ATA (amount is zero)", async () => { + const positionBundleInfo = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + ); + + // burn NFT + await toTx(ctx, { + instructions: [ + Token.createBurnInstruction( + TOKEN_PROGRAM_ID, + positionBundleInfo.positionBundleMintKeypair.publicKey, + positionBundleInfo.positionBundleTokenAccount, + ctx.wallet.publicKey, + [], + 1 + ) + ], + cleanupInstructions: [], + signers: [] + }).buildAndExecute(); + + const tokenAccount = await fetcher.getTokenInfo(positionBundleInfo.positionBundleTokenAccount); + assert.equal(tokenAccount!.amount.toString(), "0"); + + const tx = toTx( + ctx, + WhirlpoolIx.deletePositionBundleIx(ctx.program, { + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, // amount = 0 + owner: ctx.wallet.publicKey, + receiver: ctx.wallet.publicKey, + }) + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + + it("should be failed: invalid ATA (invalid mint)", async () => { + const positionBundleInfo1 = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + ); + const positionBundleInfo2 = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + ); + + const tx = toTx( + ctx, + WhirlpoolIx.deletePositionBundleIx(ctx.program, { + positionBundle: positionBundleInfo1.positionBundlePda.publicKey, + positionBundleMint: positionBundleInfo1.positionBundleMintKeypair.publicKey, + positionBundleTokenAccount: positionBundleInfo2.positionBundleTokenAccount, // invalid, + owner: ctx.wallet.publicKey, + receiver: ctx.wallet.publicKey, + }) + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + + it("should be failed: invalid ATA (invalid owner), invalid owner", async () => { + const positionBundleInfo = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + ); + + const otherWallet = Keypair.generate(); + const tx = toTx( + ctx, + WhirlpoolIx.deletePositionBundleIx(ctx.program, { + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, // ata.owner != owner + owner: otherWallet.publicKey, + receiver: ctx.wallet.publicKey, + }) + ).addSigner(otherWallet); + + await assert.rejects( + tx.buildAndExecute(), + /0x7d3/ // ConstraintRaw + ); + }); + + it("should be failed: invalid token program", async () => { + const positionBundleInfo = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + ); + + const ix = program.instruction.deletePositionBundle({ + accounts: { + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + positionBundleOwner: ctx.wallet.publicKey, + tokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, // invalid + receiver: ctx.wallet.publicKey, + } + }); + + const tx = toTx( + ctx, + { + instructions: [ix], + cleanupInstructions: [], + signers: [], + } + ); + + await assert.rejects( + tx.buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + }); + +}); diff --git a/sdk/tests/integration/initialize_pool.test.ts b/sdk/tests/integration/initialize_pool.test.ts index 20b4915..9ec5e27 100644 --- a/sdk/tests/integration/initialize_pool.test.ts +++ b/sdk/tests/integration/initialize_pool.test.ts @@ -1,4 +1,4 @@ -import { MathUtil } from "@orca-so/common-sdk"; +import { MathUtil, PDA } from "@orca-so/common-sdk"; import * as anchor from "@project-serum/anchor"; import * as assert from "assert"; import Decimal from "decimal.js"; @@ -162,7 +162,7 @@ describe("initialize_pool", () => { await assert.rejects( toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, modifiedPoolInitInfo)).buildAndExecute(), - /failed to complete|seeds|unauthorized/ + /custom program error: 0x7d6/ // ConstraintSeeds ); }); @@ -177,7 +177,7 @@ describe("initialize_pool", () => { await assert.rejects( toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, modifiedPoolInitInfo)).buildAndExecute(), - /failed to complete|seeds|unauthorized/ + /custom program error: 0x7d6/ // ConstraintSeeds ); }); @@ -258,4 +258,29 @@ describe("initialize_pool", () => { /custom program error: 0x177b/ // SqrtPriceOutOfBounds ); }); + + it("ignore passed bump", async () => { + const { poolInitInfo } = await buildTestPoolParams(ctx, 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: InitPoolParams = { + ...poolInitInfo, + whirlpoolPda: modifiedWhirlpoolPda, + }; + + await toTx(ctx, WhirlpoolIx.initializePoolIx(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); + }); + }); diff --git a/sdk/tests/integration/initialize_position_bundle.test.ts b/sdk/tests/integration/initialize_position_bundle.test.ts new file mode 100644 index 0000000..0fd1fe4 --- /dev/null +++ b/sdk/tests/integration/initialize_position_bundle.test.ts @@ -0,0 +1,267 @@ +import { deriveATA } from "@orca-so/common-sdk"; +import * as anchor from "@project-serum/anchor"; +import { AccountInfo, ASSOCIATED_TOKEN_PROGRAM_ID, MintInfo, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram } from "@solana/web3.js"; +import * as assert from "assert"; +import { + PDAUtil, + PositionBundleData, + POSITION_BUNDLE_SIZE, + toTx, + WhirlpoolContext, +} from "../../src"; +import { + createMintInstructions, + mintToByAuthority, +} from "../utils"; +import { initializePositionBundle } from "../utils/init-utils"; + +describe("initialize_position_bundle", () => { + const provider = anchor.AnchorProvider.local(undefined, { + commitment: "confirmed", + preflightCommitment: "confirmed", + }); + + anchor.setProvider(anchor.AnchorProvider.env()); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + + async function createInitializePositionBundleTx(ctx: WhirlpoolContext, overwrite: any, mintKeypair?: Keypair) { + const positionBundleMintKeypair = mintKeypair ?? Keypair.generate(); + const positionBundlePda = PDAUtil.getPositionBundle(ctx.program.programId, positionBundleMintKeypair.publicKey); + const positionBundleTokenAccount = await deriveATA(ctx.wallet.publicKey, positionBundleMintKeypair.publicKey); + + const defaultAccounts = { + positionBundle: positionBundlePda.publicKey, + positionBundleMint: positionBundleMintKeypair.publicKey, + positionBundleTokenAccount, + positionBundleOwner: ctx.wallet.publicKey, + funder: ctx.wallet.publicKey, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + tokenProgram: TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }; + + const ix = program.instruction.initializePositionBundle({ + accounts: { + ...defaultAccounts, + ...overwrite, + } + }); + + return toTx(ctx, { + instructions: [ix], + cleanupInstructions: [], + signers: [positionBundleMintKeypair], + }); + } + + async function checkPositionBundleMint(positionBundleMintPubkey: PublicKey) { + // verify position bundle Mint account + const positionBundleMint = (await ctx.fetcher.getMintInfo(positionBundleMintPubkey, true)) as MintInfo; + // should have NFT characteristics + assert.strictEqual(positionBundleMint.decimals, 0); + assert.ok(positionBundleMint.supply.eqn(1)); + // mint auth & freeze auth should be set to None + assert.ok(positionBundleMint.mintAuthority === null); + assert.ok(positionBundleMint.freezeAuthority === null); + } + + async function checkPositionBundleTokenAccount(positionBundleTokenAccountPubkey: PublicKey, owner: PublicKey, positionBundleMintPubkey: PublicKey) { + // verify position bundle Token account + const positionBundleTokenAccount = (await ctx.fetcher.getTokenInfo(positionBundleTokenAccountPubkey, true)) as AccountInfo; + assert.ok(positionBundleTokenAccount.amount.eqn(1)); + assert.ok(positionBundleTokenAccount.mint.equals(positionBundleMintPubkey)); + assert.ok(positionBundleTokenAccount.owner.equals(owner)); + } + + async function checkPositionBundle(positionBundlePubkey: PublicKey, positionBundleMintPubkey: PublicKey) { + // verify PositionBundle account + const positionBundle = (await ctx.fetcher.getPositionBundle(positionBundlePubkey, true)) as PositionBundleData; + assert.ok(positionBundle.positionBundleMint.equals(positionBundleMintPubkey)); + assert.strictEqual(positionBundle.positionBitmap.length * 8, POSITION_BUNDLE_SIZE); + for (const bitmap of positionBundle.positionBitmap) { + assert.strictEqual(bitmap, 0); + } + } + + 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; + } + + it("successfully initialize position bundle and verify initialized account contents", async () => { + const positionBundleInfo = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + // funder = ctx.wallet.publicKey + ); + + const { + positionBundleMintKeypair, + positionBundlePda, + positionBundleTokenAccount, + } = positionBundleInfo; + + await checkPositionBundleMint(positionBundleMintKeypair.publicKey); + await checkPositionBundleTokenAccount(positionBundleTokenAccount, ctx.wallet.publicKey, positionBundleMintKeypair.publicKey); + await checkPositionBundle(positionBundlePda.publicKey, positionBundleMintKeypair.publicKey); + }); + + it("successfully initialize when funder is different than account paying for transaction fee", async () => { + const preBalance = await ctx.connection.getBalance(ctx.wallet.publicKey); + + const otherWallet = await createOtherWallet(); + const positionBundleInfo = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + 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 { + positionBundleMintKeypair, + positionBundlePda, + positionBundleTokenAccount, + } = positionBundleInfo; + + await checkPositionBundleMint(positionBundleMintKeypair.publicKey); + await checkPositionBundleTokenAccount(positionBundleTokenAccount, ctx.wallet.publicKey, positionBundleMintKeypair.publicKey); + await checkPositionBundle(positionBundlePda.publicKey, positionBundleMintKeypair.publicKey); + }); + + it("PositionBundle account has reserved space", async () => { + const positionBundleAccountSizeIncludingReserve = 8 + 32 + 32 + 64; + + const positionBundleInfo = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + ); + + const account = await ctx.connection.getAccountInfo(positionBundleInfo.positionBundlePda.publicKey, "confirmed"); + assert.equal(account!.data.length, positionBundleAccountSizeIncludingReserve); + }); + + it("should be failed: cannot mint additional NFT by owner", async () => { + const positionBundleInfo = await initializePositionBundle( + ctx, + ctx.wallet.publicKey, + ); + + await assert.rejects( + mintToByAuthority( + provider, + positionBundleInfo.positionBundleMintKeypair.publicKey, + positionBundleInfo.positionBundleTokenAccount, + 1 + ), + /0x5/ // the total supply of this token is fixed + ); + }); + + it("should be failed: already used mint is passed as position bundle mint", async () => { + const positionBundleMintKeypair = Keypair.generate(); + + // create mint + const createMintIx = await createMintInstructions( + provider, + ctx.wallet.publicKey, + positionBundleMintKeypair.publicKey + ); + const createMintTx = toTx(ctx, { + instructions: createMintIx, + cleanupInstructions: [], + signers: [positionBundleMintKeypair] + }); + await createMintTx.buildAndExecute(); + + const tx = await createInitializePositionBundleTx(ctx, {}, positionBundleMintKeypair); + + await assert.rejects( + tx.buildAndExecute(), + (err) => { return JSON.stringify(err).includes("already in use") } + ); + }); + + describe("invalid input account", () => { + it("should be failed: invalid position bundle address", async () => { + const tx = await createInitializePositionBundleTx(ctx, { + // invalid parameter + positionBundle: PDAUtil.getPositionBundle(ctx.program.programId, Keypair.generate().publicKey).publicKey, + }); + + await assert.rejects( + tx.buildAndExecute(), + /0x7d6/ // ConstraintSeeds + ); + }); + + it("should be failed: invalid ATA address", async () => { + const tx = await createInitializePositionBundleTx(ctx, { + // invalid parameter + positionBundleTokenAccount: await deriveATA(ctx.wallet.publicKey, Keypair.generate().publicKey), + }); + + await assert.rejects( + tx.buildAndExecute(), + /An account required by the instruction is missing/ // Anchor cannot create derived ATA + ); + }); + + it("should be failed: invalid token program", async () => { + const tx = await createInitializePositionBundleTx(ctx, { + // invalid parameter + tokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }); + + await assert.rejects( + tx.buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("should be failed: invalid system program", async () => { + const tx = await createInitializePositionBundleTx(ctx, { + // invalid parameter + systemProgram: TOKEN_PROGRAM_ID, + }); + + await assert.rejects( + tx.buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("should be failed: invalid rent sysvar", async () => { + const tx = await createInitializePositionBundleTx(ctx, { + // invalid parameter + rent: anchor.web3.SYSVAR_CLOCK_PUBKEY, + }); + + await assert.rejects( + tx.buildAndExecute(), + /0xbc7/ // AccountSysvarMismatch + ); + }); + + it("should be failed: invalid associated token program", async () => { + const tx = await createInitializePositionBundleTx(ctx, { + // invalid parameter + associatedTokenProgram: TOKEN_PROGRAM_ID, + }); + + await assert.rejects( + tx.buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + }); +}); diff --git a/sdk/tests/integration/initialize_position_bundle_with_metadata.test.ts b/sdk/tests/integration/initialize_position_bundle_with_metadata.test.ts new file mode 100644 index 0000000..231e397 --- /dev/null +++ b/sdk/tests/integration/initialize_position_bundle_with_metadata.test.ts @@ -0,0 +1,336 @@ +import { Metadata } from "@metaplex-foundation/mpl-token-metadata"; +import { deriveATA, PDA } from "@orca-so/common-sdk"; +import * as anchor from "@project-serum/anchor"; +import { AccountInfo, ASSOCIATED_TOKEN_PROGRAM_ID, MintInfo, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram } from "@solana/web3.js"; +import * as assert from "assert"; +import { + METADATA_PROGRAM_ADDRESS, + PDAUtil, + PositionBundleData, + POSITION_BUNDLE_SIZE, + toTx, + WhirlpoolContext, + WHIRLPOOL_NFT_UPDATE_AUTH, +} from "../../src"; +import { + createMintInstructions, + mintToByAuthority, +} from "../utils"; +import { initializePositionBundleWithMetadata } from "../utils/init-utils"; + +describe("initialize_position_bundle_with_metadata", () => { + const provider = anchor.AnchorProvider.local(undefined, { + commitment: "confirmed", + preflightCommitment: "confirmed", + }); + + anchor.setProvider(anchor.AnchorProvider.env()); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + + async function createInitializePositionBundleWithMetadataTx(ctx: WhirlpoolContext, overwrite: any, mintKeypair?: Keypair) { + const positionBundleMintKeypair = mintKeypair ?? Keypair.generate(); + const positionBundlePda = PDAUtil.getPositionBundle(ctx.program.programId, positionBundleMintKeypair.publicKey); + const positionBundleMetadataPda = PDAUtil.getPositionBundleMetadata(positionBundleMintKeypair.publicKey); + const positionBundleTokenAccount = await deriveATA(ctx.wallet.publicKey, positionBundleMintKeypair.publicKey); + + const defaultAccounts = { + positionBundle: positionBundlePda.publicKey, + positionBundleMint: positionBundleMintKeypair.publicKey, + positionBundleMetadata: positionBundleMetadataPda.publicKey, + positionBundleTokenAccount, + positionBundleOwner: ctx.wallet.publicKey, + funder: ctx.wallet.publicKey, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + tokenProgram: TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + metadataProgram: METADATA_PROGRAM_ADDRESS, + metadataUpdateAuth: WHIRLPOOL_NFT_UPDATE_AUTH, + }; + + const ix = program.instruction.initializePositionBundleWithMetadata({ + accounts: { + ...defaultAccounts, + ...overwrite, + } + }); + + return toTx(ctx, { + instructions: [ix], + cleanupInstructions: [], + signers: [positionBundleMintKeypair], + }); + } + + async function checkPositionBundleMint(positionBundleMintPubkey: PublicKey) { + // verify position bundle Mint account + const positionBundleMint = (await ctx.fetcher.getMintInfo(positionBundleMintPubkey, true)) as MintInfo; + // should have NFT characteristics + assert.strictEqual(positionBundleMint.decimals, 0); + assert.ok(positionBundleMint.supply.eqn(1)); + // mint auth & freeze auth should be set to None + assert.ok(positionBundleMint.mintAuthority === null); + assert.ok(positionBundleMint.freezeAuthority === null); + } + + async function checkPositionBundleTokenAccount(positionBundleTokenAccountPubkey: PublicKey, owner: PublicKey, positionBundleMintPubkey: PublicKey) { + // verify position bundle Token account + const positionBundleTokenAccount = (await ctx.fetcher.getTokenInfo(positionBundleTokenAccountPubkey, true)) as AccountInfo; + assert.ok(positionBundleTokenAccount.amount.eqn(1)); + assert.ok(positionBundleTokenAccount.mint.equals(positionBundleMintPubkey)); + assert.ok(positionBundleTokenAccount.owner.equals(owner)); + } + + async function checkPositionBundle(positionBundlePubkey: PublicKey, positionBundleMintPubkey: PublicKey) { + // verify PositionBundle account + const positionBundle = (await ctx.fetcher.getPositionBundle(positionBundlePubkey, true)) as PositionBundleData; + assert.ok(positionBundle.positionBundleMint.equals(positionBundleMintPubkey)); + assert.strictEqual(positionBundle.positionBitmap.length * 8, POSITION_BUNDLE_SIZE); + for (const bitmap of positionBundle.positionBitmap) { + assert.strictEqual(bitmap, 0); + } + } + + async function checkPositionBundleMetadata(metadataPda: PDA, positionMint: PublicKey) { + const WPB_METADATA_NAME_PREFIX = "Orca Position Bundle"; + const WPB_METADATA_SYMBOL = "OPB"; + const WPB_METADATA_URI = "https://arweave.net/A_Wo8dx2_3lSUwMIi7bdT_sqxi8soghRNAWXXiqXpgE"; + + const mintAddress = positionMint.toBase58(); + const nftName = WPB_METADATA_NAME_PREFIX + + " " + + mintAddress.slice(0, 4) + + "..." + + mintAddress.slice(-4); + + assert.ok(metadataPda != null); + const metadata = await Metadata.load(provider.connection, metadataPda.publicKey); + assert.ok(metadata.data.mint === positionMint.toString()); + assert.ok(metadata.data.updateAuthority === WHIRLPOOL_NFT_UPDATE_AUTH.toBase58()); + assert.ok(metadata.data.isMutable); + assert.strictEqual(metadata.data.data.name, nftName); + assert.strictEqual(metadata.data.data.symbol, WPB_METADATA_SYMBOL); + assert.strictEqual(metadata.data.data.uri, WPB_METADATA_URI); + } + + 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; + } + + it("successfully initialize position bundle and verify initialized account contents", async () => { + const positionBundleInfo = await initializePositionBundleWithMetadata( + ctx, + ctx.wallet.publicKey, + // funder = ctx.wallet.publicKey + ); + + const { + positionBundleMintKeypair, + positionBundlePda, + positionBundleMetadataPda, + positionBundleTokenAccount, + } = positionBundleInfo; + + await checkPositionBundleMint(positionBundleMintKeypair.publicKey); + await checkPositionBundleTokenAccount(positionBundleTokenAccount, ctx.wallet.publicKey, positionBundleMintKeypair.publicKey); + await checkPositionBundle(positionBundlePda.publicKey, positionBundleMintKeypair.publicKey); + await checkPositionBundleMetadata(positionBundleMetadataPda, positionBundleMintKeypair.publicKey); + }); + + it("successfully initialize when funder is different than account paying for transaction fee", async () => { + const preBalance = await ctx.connection.getBalance(ctx.wallet.publicKey); + + const otherWallet = await createOtherWallet(); + const positionBundleInfo = await initializePositionBundleWithMetadata( + ctx, + ctx.wallet.publicKey, + 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 { + positionBundleMintKeypair, + positionBundlePda, + positionBundleMetadataPda, + positionBundleTokenAccount, + } = positionBundleInfo; + + await checkPositionBundleMint(positionBundleMintKeypair.publicKey); + await checkPositionBundleTokenAccount(positionBundleTokenAccount, ctx.wallet.publicKey, positionBundleMintKeypair.publicKey); + await checkPositionBundle(positionBundlePda.publicKey, positionBundleMintKeypair.publicKey); + await checkPositionBundleMetadata(positionBundleMetadataPda, positionBundleMintKeypair.publicKey); + }); + + it("PositionBundle account has reserved space", async () => { + const positionBundleAccountSizeIncludingReserve = 8 + 32 + 32 + 64; + + const positionBundleInfo = await initializePositionBundleWithMetadata( + ctx, + ctx.wallet.publicKey, + ); + + const account = await ctx.connection.getAccountInfo(positionBundleInfo.positionBundlePda.publicKey, "confirmed"); + assert.equal(account!.data.length, positionBundleAccountSizeIncludingReserve); + }); + + it("should be failed: cannot mint additional NFT by owner", async () => { + const positionBundleInfo = await initializePositionBundleWithMetadata( + ctx, + ctx.wallet.publicKey, + ); + + await assert.rejects( + mintToByAuthority( + provider, + positionBundleInfo.positionBundleMintKeypair.publicKey, + positionBundleInfo.positionBundleTokenAccount, + 1 + ), + /0x5/ // the total supply of this token is fixed + ); + }); + + it("should be failed: already used mint is passed as position bundle mint", async () => { + const positionBundleMintKeypair = Keypair.generate(); + + // create mint + const createMintIx = await createMintInstructions( + provider, + ctx.wallet.publicKey, + positionBundleMintKeypair.publicKey + ); + const createMintTx = toTx(ctx, { + instructions: createMintIx, + cleanupInstructions: [], + signers: [positionBundleMintKeypair] + }); + await createMintTx.buildAndExecute(); + + const tx = await createInitializePositionBundleWithMetadataTx(ctx, {}, positionBundleMintKeypair); + + await assert.rejects( + tx.buildAndExecute(), + (err) => { return JSON.stringify(err).includes("already in use") } + ); + }); + + describe("invalid input account", () => { + it("should be failed: invalid position bundle address", async () => { + const tx = await createInitializePositionBundleWithMetadataTx(ctx, { + // invalid parameter + positionBundle: PDAUtil.getPositionBundle(ctx.program.programId, Keypair.generate().publicKey).publicKey, + }); + + await assert.rejects( + tx.buildAndExecute(), + /0x7d6/ // ConstraintSeeds + ); + }); + + it("should be failed: invalid metadata address", async () => { + const tx = await createInitializePositionBundleWithMetadataTx(ctx, { + // invalid parameter + positionBundleMetadata: PDAUtil.getPositionBundleMetadata(Keypair.generate().publicKey).publicKey, + }); + + await assert.rejects( + tx.buildAndExecute(), + /0x5/ // InvalidMetadataKey: cannot create Metadata + ); + }); + + it("should be failed: invalid ATA address", async () => { + const tx = await createInitializePositionBundleWithMetadataTx(ctx, { + // invalid parameter + positionBundleTokenAccount: await deriveATA(ctx.wallet.publicKey, Keypair.generate().publicKey), + }); + + await assert.rejects( + tx.buildAndExecute(), + /An account required by the instruction is missing/ // Anchor cannot create derived ATA + ); + }); + + it("should be failed: invalid update auth", async () => { + const tx = await createInitializePositionBundleWithMetadataTx(ctx, { + // invalid parameter + metadataUpdateAuth: Keypair.generate().publicKey, + }); + + await assert.rejects( + tx.buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + + it("should be failed: invalid token program", async () => { + const tx = await createInitializePositionBundleWithMetadataTx(ctx, { + // invalid parameter + tokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }); + + await assert.rejects( + tx.buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("should be failed: invalid system program", async () => { + const tx = await createInitializePositionBundleWithMetadataTx(ctx, { + // invalid parameter + systemProgram: TOKEN_PROGRAM_ID, + }); + + await assert.rejects( + tx.buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("should be failed: invalid rent sysvar", async () => { + const tx = await createInitializePositionBundleWithMetadataTx(ctx, { + // invalid parameter + rent: anchor.web3.SYSVAR_CLOCK_PUBKEY, + }); + + await assert.rejects( + tx.buildAndExecute(), + /0xbc7/ // AccountSysvarMismatch + ); + }); + + it("should be failed: invalid associated token program", async () => { + const tx = await createInitializePositionBundleWithMetadataTx(ctx, { + // invalid parameter + associatedTokenProgram: TOKEN_PROGRAM_ID, + }); + + await assert.rejects( + tx.buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("should be failed: invalid metadata program", async () => { + const tx = await createInitializePositionBundleWithMetadataTx(ctx, { + // invalid parameter + metadataProgram: TOKEN_PROGRAM_ID, + }); + + await assert.rejects( + tx.buildAndExecute(), + /0x7dc/ // ConstraintAddress + ); + }); + }); +}); diff --git a/sdk/tests/integration/multi-ix/bundled_position_management.test.ts b/sdk/tests/integration/multi-ix/bundled_position_management.test.ts new file mode 100644 index 0000000..ea5e53b --- /dev/null +++ b/sdk/tests/integration/multi-ix/bundled_position_management.test.ts @@ -0,0 +1,1234 @@ +import * as anchor from "@project-serum/anchor"; +import * as assert from "assert"; +import { toTx, WhirlpoolIx, Whirlpool, WhirlpoolClient, buildWhirlpoolClient, PDAUtil, collectFeesQuote, NUM_REWARDS, ORCA_WHIRLPOOL_PROGRAM_ID, PoolUtil, PriceMath, POSITION_BUNDLE_SIZE, PositionBundleData } from "../../../src"; +import { WhirlpoolContext } from "../../../src/context"; +import { createTokenAccount, TickSpacing, ZERO_BN } from "../../utils"; +import { initializePositionBundle, openBundledPosition } from "../../utils/init-utils"; +import { u64 } from "@solana/spl-token"; +import { WhirlpoolTestFixture } from "../../utils/fixture"; +import { deriveATA, MathUtil, TransactionBuilder, ZERO } from "@orca-so/common-sdk"; +import Decimal from "decimal.js"; +import { Keypair, SystemProgram } from "@solana/web3.js"; +import { BN } from "bn.js"; + + +interface SharedTestContext { + provider: anchor.AnchorProvider; + program: Whirlpool; + whirlpoolCtx: WhirlpoolContext; + whirlpoolClient: WhirlpoolClient; +} + +describe("bundled position management tests", () => { + const provider = anchor.AnchorProvider.local(undefined, { + commitment: "confirmed", + preflightCommitment: "confirmed", + }); + + let testCtx: SharedTestContext; + const tickLowerIndex = 29440; + const tickUpperIndex = 33536; + const tickSpacing = TickSpacing.Standard; + const vaultStartBalance = 1_000_000; + const liquidityAmount = new u64(10_000_000); + const sleep = (second: number) => new Promise(resolve => setTimeout(resolve, second * 1000)) + + before(() => { + anchor.setProvider(provider); + const program = anchor.workspace.Whirlpool; + const whirlpoolCtx = WhirlpoolContext.fromWorkspace(provider, program); + const whirlpoolClient = buildWhirlpoolClient(whirlpoolCtx); + + testCtx = { + provider, + program, + whirlpoolCtx, + whirlpoolClient, + }; + }); + + function checkBitmapIsOpened(account: PositionBundleData, bundleIndex: number): boolean { + if (bundleIndex < 0 || bundleIndex >= POSITION_BUNDLE_SIZE) throw Error("bundleIndex is out of bounds"); + + const bitmapIndex = Math.floor(bundleIndex / 8); + const bitmapOffset = bundleIndex % 8; + return (account.positionBitmap[bitmapIndex] & (1 << bitmapOffset)) > 0; + } + + function checkBitmapIsClosed(account: PositionBundleData, bundleIndex: number): boolean { + if (bundleIndex < 0 || bundleIndex >= POSITION_BUNDLE_SIZE) throw Error("bundleIndex is out of bounds"); + + const bitmapIndex = Math.floor(bundleIndex / 8); + const bitmapOffset = bundleIndex % 8; + return (account.positionBitmap[bitmapIndex] & (1 << bitmapOffset)) === 0; + } + + function checkBitmap(account: PositionBundleData, openedBundleIndexes: number[]) { + for (let i=0; i { + // create test pool + const ctx = testCtx.whirlpoolCtx; + const fixture = await new WhirlpoolTestFixture(ctx).init({ + tickSpacing, + positions: [], + rewards: [], + }); + const { poolInitInfo, rewards } = fixture.getInfos(); + + // initialize position bundle + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + const positionBundlePubkey = positionBundleInfo.positionBundlePda.publicKey; + const whirlpoolPubkey = poolInitInfo.whirlpoolPda.publicKey; + + const batchSize = 12; + const openedBundleIndexes: number[] = []; + + // open all + for (let startBundleIndex=0; startBundleIndex { + // create test pool + const ctx = testCtx.whirlpoolCtx; + const fixture = await new WhirlpoolTestFixture(ctx).init({ + tickSpacing, + positions: [ + { liquidityAmount, tickLowerIndex, tickUpperIndex }, // non bundled position (to create TickArrays) + ], + rewards: [ + { + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new u64(vaultStartBalance), + }, + { + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new u64(vaultStartBalance), + }, + { + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new u64(vaultStartBalance), + }, + ], + }); + const { poolInitInfo, rewards } = fixture.getInfos(); + + // initialize position bundle + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + // open bundled position + const bundleIndex = 0; + const positionInitInfo = await openBundledPosition( + ctx, + poolInitInfo.whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + const { bundledPositionPda } = positionInitInfo.params; + + const bundledPositionPubkey = bundledPositionPda.publicKey; + const tickArrayLower = PDAUtil.getTickArrayFromTickIndex(positionInitInfo.params.tickLowerIndex, poolInitInfo.tickSpacing, poolInitInfo.whirlpoolPda.publicKey, ctx.program.programId).publicKey; + const tickArrayUpper = PDAUtil.getTickArrayFromTickIndex(positionInitInfo.params.tickUpperIndex, poolInitInfo.tickSpacing, poolInitInfo.whirlpoolPda.publicKey, ctx.program.programId).publicKey; + const whirlpoolPubkey = poolInitInfo.whirlpoolPda.publicKey; + const tokenOwnerAccountA = await deriveATA(ctx.wallet.publicKey, poolInitInfo.tokenMintA); + const tokenOwnerAccountB = await deriveATA(ctx.wallet.publicKey, poolInitInfo.tokenMintB); + + const modifyLiquidityParams = { + liquidityAmount, + position: bundledPositionPubkey, + positionAuthority: ctx.wallet.publicKey, + positionTokenAccount: positionBundleInfo.positionBundleTokenAccount, + tickArrayLower, + tickArrayUpper, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + whirlpool: whirlpoolPubkey, + } + + // increaseLiquidity + const depositAmounts = PoolUtil.getTokenAmountsFromLiquidity( + liquidityAmount, + (await ctx.fetcher.getPool(whirlpoolPubkey, true))!.sqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex), + true + ); + + const preIncrease = await ctx.fetcher.getPosition(bundledPositionPubkey, true); + assert.ok(preIncrease!.liquidity.isZero()); + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityIx(ctx.program, { + ...modifyLiquidityParams, + tokenMaxA: depositAmounts.tokenA, + tokenMaxB: depositAmounts.tokenB, + }) + ).buildAndExecute(); + const postIncrease = await ctx.fetcher.getPosition(bundledPositionPubkey, true); + assert.ok(postIncrease!.liquidity.eq(liquidityAmount)); + + await sleep(2); // accrueRewards + await accrueFees(fixture); + await stopRewardsEmission(fixture); + + // updateFeesAndRewards + const preUpdate = await ctx.fetcher.getPosition(bundledPositionPubkey, true); + assert.ok(preUpdate!.feeOwedA.isZero()); + assert.ok(preUpdate!.feeOwedB.isZero()); + assert.ok(preUpdate!.rewardInfos.every((r) => r.amountOwed.isZero())); + await toTx( + ctx, + WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, { + position: bundledPositionPubkey, + tickArrayLower, + tickArrayUpper, + whirlpool: whirlpoolPubkey, + }) + ).buildAndExecute(); + const postUpdate = await ctx.fetcher.getPosition(bundledPositionPubkey, true); + assert.ok(postUpdate!.feeOwedA.gtn(0)); + assert.ok(postUpdate!.feeOwedB.gtn(0)); + assert.ok(postUpdate!.rewardInfos.every((r) => r.amountOwed.gtn(0))); + + // collectFees + await toTx( + ctx, + WhirlpoolIx.collectFeesIx(ctx.program, { + position: bundledPositionPubkey, + positionAuthority: ctx.wallet.publicKey, + positionTokenAccount: positionBundleInfo.positionBundleTokenAccount, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + whirlpool: whirlpoolPubkey, + }) + ).buildAndExecute(); + const postCollectFees = await ctx.fetcher.getPosition(bundledPositionPubkey, true); + assert.ok(postCollectFees!.feeOwedA.isZero()); + assert.ok(postCollectFees!.feeOwedB.isZero()); + + // collectReward + for (let i=0; i { + const openCloseIterationNum = 5; + + // create test pool + const ctx = testCtx.whirlpoolCtx; + const fixture = await new WhirlpoolTestFixture(ctx).init({ + tickSpacing, + positions: [ + { liquidityAmount, tickLowerIndex, tickUpperIndex }, // non bundled position (to create TickArrays) + ], + rewards: [ + { + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new u64(vaultStartBalance), + }, + { + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new u64(vaultStartBalance), + }, + { + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new u64(vaultStartBalance), + }, + ], + }); + const { poolInitInfo, rewards } = fixture.getInfos(); + + // increase feeGrowth + await accrueFees(fixture); + + // initialize position bundle + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + const bundleIndex = Math.floor(Math.random() * POSITION_BUNDLE_SIZE); + + for (let iter=0; iter r.growthInsideCheckpoint.isZero())); + + const modifyLiquidityParams = { + liquidityAmount, + position: bundledPositionPubkey, + positionAuthority: ctx.wallet.publicKey, + positionTokenAccount: positionBundleInfo.positionBundleTokenAccount, + tickArrayLower, + tickArrayUpper, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + whirlpool: whirlpoolPubkey, + } + + // increaseLiquidity + const depositAmounts = PoolUtil.getTokenAmountsFromLiquidity( + liquidityAmount, + (await ctx.fetcher.getPool(whirlpoolPubkey, true))!.sqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex), + true + ); + + const preIncrease = await ctx.fetcher.getPosition(bundledPositionPubkey, true); + assert.ok(preIncrease!.liquidity.isZero()); + await toTx( + ctx, + WhirlpoolIx.increaseLiquidityIx(ctx.program, { + ...modifyLiquidityParams, + tokenMaxA: depositAmounts.tokenA, + tokenMaxB: depositAmounts.tokenB, + }) + ).buildAndExecute(); + const postIncrease = await ctx.fetcher.getPosition(bundledPositionPubkey, true); + assert.ok(postIncrease!.liquidity.eq(liquidityAmount)); + + // non-zero check + assert.ok(postIncrease!.feeGrowthCheckpointA.gtn(0)); + assert.ok(postIncrease!.feeGrowthCheckpointB.gtn(0)); + assert.ok(postIncrease!.rewardInfos.every((r) => r.growthInsideCheckpoint.gtn(0))); + + await sleep(2); // accrueRewards + await accrueFees(fixture); + + // decreaseLiquidity + const withdrawAmounts = PoolUtil.getTokenAmountsFromLiquidity( + liquidityAmount, + (await ctx.fetcher.getPool(whirlpoolPubkey, true))!.sqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex), + false + ); + + const preDecrease = await ctx.fetcher.getPosition(bundledPositionPubkey, true); + assert.ok(preDecrease!.liquidity.eq(liquidityAmount)); + await toTx( + ctx, + WhirlpoolIx.decreaseLiquidityIx(ctx.program, { + ...modifyLiquidityParams, + tokenMinA: withdrawAmounts.tokenA, + tokenMinB: withdrawAmounts.tokenB, + }) + ).buildAndExecute(); + const postDecrease = await ctx.fetcher.getPosition(bundledPositionPubkey, true); + assert.ok(postDecrease!.liquidity.isZero()); + + // collectFees + await toTx( + ctx, + WhirlpoolIx.collectFeesIx(ctx.program, { + position: bundledPositionPubkey, + positionAuthority: ctx.wallet.publicKey, + positionTokenAccount: positionBundleInfo.positionBundleTokenAccount, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + whirlpool: whirlpoolPubkey, + }) + ).buildAndExecute(); + const postCollectFees = await ctx.fetcher.getPosition(bundledPositionPubkey, true); + assert.ok(postCollectFees!.feeOwedA.isZero()); + assert.ok(postCollectFees!.feeOwedB.isZero()); + + // collectReward + for (let i=0; i { + it("successfully openBundledPosition+increaseLiquidity / decreaseLiquidity+closeBundledPosition in single Tx", async () => { + // create test pool + const ctx = testCtx.whirlpoolCtx; + const fixture = await new WhirlpoolTestFixture(ctx).init({ + tickSpacing, + positions: [ + { liquidityAmount, tickLowerIndex, tickUpperIndex }, // non bundled position (to create TickArrays) + ], + rewards: [], + }); + const { poolInitInfo, rewards } = fixture.getInfos(); + + // initialize position bundle + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + const bundleIndex = Math.floor(Math.random() * POSITION_BUNDLE_SIZE); + + const bundledPositionPda = PDAUtil.getBundledPosition(ctx.program.programId, positionBundleInfo.positionBundleMintKeypair.publicKey, bundleIndex); + const bundledPositionPubkey = bundledPositionPda.publicKey; + const tickArrayLower = PDAUtil.getTickArrayFromTickIndex(tickLowerIndex, poolInitInfo.tickSpacing, poolInitInfo.whirlpoolPda.publicKey, ctx.program.programId).publicKey; + const tickArrayUpper = PDAUtil.getTickArrayFromTickIndex(tickUpperIndex, poolInitInfo.tickSpacing, poolInitInfo.whirlpoolPda.publicKey, ctx.program.programId).publicKey; + const whirlpoolPubkey = poolInitInfo.whirlpoolPda.publicKey; + const tokenOwnerAccountA = await deriveATA(ctx.wallet.publicKey, poolInitInfo.tokenMintA); + const tokenOwnerAccountB = await deriveATA(ctx.wallet.publicKey, poolInitInfo.tokenMintB); + + const modifyLiquidityParams = { + liquidityAmount, + position: bundledPositionPubkey, + positionAuthority: ctx.wallet.publicKey, + positionTokenAccount: positionBundleInfo.positionBundleTokenAccount, + tickArrayLower, + tickArrayUpper, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + whirlpool: whirlpoolPubkey, + } + + const depositAmounts = PoolUtil.getTokenAmountsFromLiquidity( + liquidityAmount, + (await ctx.fetcher.getPool(whirlpoolPubkey, true))!.sqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex), + true + ); + + // openBundledPosition + increaseLiquidity + const openIncreaseBuilder = new TransactionBuilder(ctx.connection, ctx.wallet); + openIncreaseBuilder + .addInstruction(WhirlpoolIx.openBundledPositionIx(ctx.program, { + bundledPositionPda, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + tickLowerIndex, + tickUpperIndex, + whirlpool: whirlpoolPubkey, + funder: ctx.wallet.publicKey + })) + .addInstruction(WhirlpoolIx.increaseLiquidityIx(ctx.program, { + ...modifyLiquidityParams, + tokenMaxA: depositAmounts.tokenA, + tokenMaxB: depositAmounts.tokenB, + })); + await openIncreaseBuilder.buildAndExecute(); + const postIncrease = await ctx.fetcher.getPosition(bundledPositionPubkey, true); + assert.ok(postIncrease!.liquidity.eq(liquidityAmount)); + + + const withdrawAmounts = PoolUtil.getTokenAmountsFromLiquidity( + liquidityAmount, + (await ctx.fetcher.getPool(whirlpoolPubkey, true))!.sqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex), + false + ); + + const decreaseCloseBuilder = new TransactionBuilder(ctx.connection, ctx.wallet); + decreaseCloseBuilder + .addInstruction(WhirlpoolIx.decreaseLiquidityIx(ctx.program, { + ...modifyLiquidityParams, + tokenMinA: withdrawAmounts.tokenA, + tokenMinB: withdrawAmounts.tokenB, + })) + .addInstruction(WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: bundledPositionPubkey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: ctx.wallet.publicKey, + })); + await decreaseCloseBuilder.buildAndExecute(); + const postClose = await ctx.fetcher.getPosition(bundledPositionPubkey, true); + assert.ok(postClose === null); + }); + + it("successfully open bundled position & close bundled position in single Tx", async () => { + // create test pool + const ctx = testCtx.whirlpoolCtx; + const fixture = await new WhirlpoolTestFixture(ctx).init({ + tickSpacing, + positions: [ + { liquidityAmount, tickLowerIndex, tickUpperIndex }, // non bundled position (to create TickArrays) + ], + rewards: [ + { + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new u64(vaultStartBalance), + }, + { + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new u64(vaultStartBalance), + }, + { + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new u64(vaultStartBalance), + }, + ], + }); + const { poolInitInfo, rewards } = fixture.getInfos(); + + // initialize position bundle + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + const bundleIndex = Math.floor(Math.random() * POSITION_BUNDLE_SIZE); + + const bundledPositionPda = PDAUtil.getBundledPosition(ctx.program.programId, positionBundleInfo.positionBundleMintKeypair.publicKey, bundleIndex); + const bundledPositionPubkey = bundledPositionPda.publicKey; + const tickArrayLower = PDAUtil.getTickArrayFromTickIndex(tickLowerIndex, poolInitInfo.tickSpacing, poolInitInfo.whirlpoolPda.publicKey, ctx.program.programId).publicKey; + const tickArrayUpper = PDAUtil.getTickArrayFromTickIndex(tickUpperIndex, poolInitInfo.tickSpacing, poolInitInfo.whirlpoolPda.publicKey, ctx.program.programId).publicKey; + const whirlpoolPubkey = poolInitInfo.whirlpoolPda.publicKey; + const tokenOwnerAccountA = await deriveATA(ctx.wallet.publicKey, poolInitInfo.tokenMintA); + const tokenOwnerAccountB = await deriveATA(ctx.wallet.publicKey, poolInitInfo.tokenMintB); + + const modifyLiquidityParams = { + liquidityAmount, + position: bundledPositionPubkey, + positionAuthority: ctx.wallet.publicKey, + positionTokenAccount: positionBundleInfo.positionBundleTokenAccount, + tickArrayLower, + tickArrayUpper, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + whirlpool: whirlpoolPubkey, + } + + const depositAmounts = PoolUtil.getTokenAmountsFromLiquidity( + liquidityAmount, + (await ctx.fetcher.getPool(whirlpoolPubkey, true))!.sqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex), + true + ); + + const receiver = Keypair.generate(); + const builder = new TransactionBuilder(ctx.connection, ctx.wallet); + builder + .addInstruction(WhirlpoolIx.openBundledPositionIx(ctx.program, { + bundledPositionPda, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + tickLowerIndex, + tickUpperIndex, + whirlpool: whirlpoolPubkey, + funder: ctx.wallet.publicKey + })) + .addInstruction(WhirlpoolIx.increaseLiquidityIx(ctx.program, { + ...modifyLiquidityParams, + tokenMaxA: depositAmounts.tokenA, + tokenMaxB: depositAmounts.tokenB, + })) + .addInstruction(WhirlpoolIx.decreaseLiquidityIx(ctx.program, { + ...modifyLiquidityParams, + tokenMinA: new u64(0), + tokenMinB: new u64(0), + })) + .addInstruction(WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: bundledPositionPubkey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: receiver.publicKey, + })); + + await builder.buildAndExecute(); + const receiverBalance = await ctx.connection.getBalance(receiver.publicKey, "confirmed"); + assert.ok(receiverBalance > 0); + }); + + it("successfully close & re-open bundled position with the same bundle index in single Tx", async () => { + // create test pool + const ctx = testCtx.whirlpoolCtx; + const fixture = await new WhirlpoolTestFixture(ctx).init({ + tickSpacing, + positions: [], + rewards: [], + }); + const { poolInitInfo, rewards } = fixture.getInfos(); + + // initialize position bundle + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + const bundleIndex = Math.floor(Math.random() * POSITION_BUNDLE_SIZE); + + const bundledPositionPda = PDAUtil.getBundledPosition(ctx.program.programId, positionBundleInfo.positionBundleMintKeypair.publicKey, bundleIndex); + const bundledPositionPubkey = bundledPositionPda.publicKey; + const whirlpoolPubkey = poolInitInfo.whirlpoolPda.publicKey; + + const builder = new TransactionBuilder(ctx.connection, ctx.wallet); + builder + // open + .addInstruction(WhirlpoolIx.openBundledPositionIx(ctx.program, { + bundledPositionPda, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + tickLowerIndex, + tickUpperIndex, + whirlpool: whirlpoolPubkey, + funder: ctx.wallet.publicKey + })) + // close + .addInstruction(WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: bundledPositionPubkey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: whirlpoolPubkey, + })) + // reopen bundled position with same bundleIndex in single Tx + .addInstruction(WhirlpoolIx.openBundledPositionIx(ctx.program, { + bundledPositionPda, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + tickLowerIndex: tickLowerIndex + tickSpacing, + tickUpperIndex: tickUpperIndex + tickSpacing, + whirlpool: whirlpoolPubkey, + funder: ctx.wallet.publicKey + })); + + // Account closing reassigns to system program and reallocates + // https://github.com/coral-xyz/anchor/pull/2169 + // in Anchor v0.26.0, close & open in same Tx will success. + await builder.buildAndExecute(); + const postReopen = await ctx.fetcher.getPosition(bundledPositionPubkey, true); + assert.ok(postReopen!.liquidity.isZero()); + assert.ok(postReopen!.tickLowerIndex === tickLowerIndex + tickSpacing); + assert.ok(postReopen!.tickUpperIndex === tickUpperIndex + tickSpacing); + }); + + it("successfully open bundled position & swap & close bundled position in single Tx", async () => { + // create test pool + const ctx = testCtx.whirlpoolCtx; + const fixture = await new WhirlpoolTestFixture(ctx).init({ + tickSpacing, + positions: [ + { liquidityAmount, tickLowerIndex, tickUpperIndex }, // non bundled position (to create TickArrays) + ], + rewards: [ + { + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new u64(vaultStartBalance), + }, + { + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new u64(vaultStartBalance), + }, + { + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new u64(vaultStartBalance), + }, + ], + }); + const { poolInitInfo, rewards } = fixture.getInfos(); + + // initialize position bundle + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + const bundleIndex = Math.floor(Math.random() * POSITION_BUNDLE_SIZE); + + const bundledPositionPda = PDAUtil.getBundledPosition(ctx.program.programId, positionBundleInfo.positionBundleMintKeypair.publicKey, bundleIndex); + const bundledPositionPubkey = bundledPositionPda.publicKey; + const tickArrayLower = PDAUtil.getTickArrayFromTickIndex(tickLowerIndex, poolInitInfo.tickSpacing, poolInitInfo.whirlpoolPda.publicKey, ctx.program.programId).publicKey; + const tickArrayUpper = PDAUtil.getTickArrayFromTickIndex(tickUpperIndex, poolInitInfo.tickSpacing, poolInitInfo.whirlpoolPda.publicKey, ctx.program.programId).publicKey; + const whirlpoolPubkey = poolInitInfo.whirlpoolPda.publicKey; + const tokenOwnerAccountA = await deriveATA(ctx.wallet.publicKey, poolInitInfo.tokenMintA); + const tokenOwnerAccountB = await deriveATA(ctx.wallet.publicKey, poolInitInfo.tokenMintB); + + const tickArrayPda = PDAUtil.getTickArray(ctx.program.programId, whirlpoolPubkey, 22528); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPubkey); + + const modifyLiquidityParams = { + liquidityAmount, + position: bundledPositionPubkey, + positionAuthority: ctx.wallet.publicKey, + positionTokenAccount: positionBundleInfo.positionBundleTokenAccount, + tickArrayLower, + tickArrayUpper, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + whirlpool: whirlpoolPubkey, + } + + const depositAmounts = PoolUtil.getTokenAmountsFromLiquidity( + liquidityAmount, + (await ctx.fetcher.getPool(whirlpoolPubkey, true))!.sqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex), + true + ); + + const swapInput = new u64(200_000); + const poolLiquidity = new BN(liquidityAmount.muln(2).toString()); + const estimatedFee = new BN(swapInput.toString()) + .muln(3).divn(1000) // feeRate 0.3% + .muln(97).divn(100) // minus protocolFee 3% + .shln(64).div(poolLiquidity) // to X64 growth + .mul(liquidityAmount) + .shrn(64) + .toNumber(); + + const receiver = Keypair.generate(); + const receiverAtaA = await createTokenAccount(provider, poolInitInfo.tokenMintA, receiver.publicKey); + const receiverAtaB = await createTokenAccount(provider, poolInitInfo.tokenMintB, receiver.publicKey); + + const builder = new TransactionBuilder(ctx.connection, ctx.wallet); + builder + .addInstruction(WhirlpoolIx.openBundledPositionIx(ctx.program, { + bundledPositionPda, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + tickLowerIndex, + tickUpperIndex, + whirlpool: whirlpoolPubkey, + funder: ctx.wallet.publicKey + })) + .addInstruction(WhirlpoolIx.increaseLiquidityIx(ctx.program, { + ...modifyLiquidityParams, + tokenMaxA: depositAmounts.tokenA, + tokenMaxB: depositAmounts.tokenB, + })) + .addInstruction(WhirlpoolIx.swapIx(ctx.program, { + amount: swapInput, + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(4)), + amountSpecifiedIsInput: true, + aToB: true, + whirlpool: whirlpoolPubkey, + tokenAuthority: ctx.wallet.publicKey, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + })) + .addInstruction(WhirlpoolIx.swapIx(ctx.program, { + amount: swapInput, + otherAmountThreshold: ZERO_BN, + sqrtPriceLimit: MathUtil.toX64(new Decimal(5)), + amountSpecifiedIsInput: true, + aToB: false, + whirlpool: whirlpoolPubkey, + tokenAuthority: ctx.wallet.publicKey, + tokenOwnerAccountA, + tokenOwnerAccountB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + tickArray0: tickArrayPda.publicKey, + tickArray1: tickArrayPda.publicKey, + tickArray2: tickArrayPda.publicKey, + oracle: oraclePda.publicKey, + })) + .addInstruction(WhirlpoolIx.decreaseLiquidityIx(ctx.program, { + ...modifyLiquidityParams, + tokenMinA: new u64(0), + tokenMinB: new u64(0), + })) + .addInstruction(WhirlpoolIx.collectFeesIx(ctx.program, { + position: bundledPositionPubkey, + positionAuthority: ctx.wallet.publicKey, + positionTokenAccount: positionBundleInfo.positionBundleTokenAccount, + tokenOwnerAccountA: receiverAtaA, + tokenOwnerAccountB: receiverAtaB, + tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey, + tokenVaultB: poolInitInfo.tokenVaultBKeypair.publicKey, + whirlpool: whirlpoolPubkey, + })) + .addInstruction(WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: bundledPositionPubkey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: receiver.publicKey, + })); + + await builder.buildAndExecute(); + assert.ok((await ctx.fetcher.getTokenInfo(receiverAtaA, true))!.amount.eqn(estimatedFee)); + assert.ok((await ctx.fetcher.getTokenInfo(receiverAtaB, true))!.amount.eqn(estimatedFee)); + }); + }); + + describe("Ensuring that the account is closed", () => { + it("The discriminator of the deleted position bundle is marked as closed", async () => { + const ctx = testCtx.whirlpoolCtx; + + // initialize position bundle + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const preClose = await ctx.connection.getAccountInfo(positionBundleInfo.positionBundlePda.publicKey, "confirmed"); + assert.ok(preClose !== null); + const rentOfPositionBundle = preClose.lamports; + assert.ok(rentOfPositionBundle > 0); + + const builder = new TransactionBuilder(ctx.connection, ctx.wallet); + builder + // close + .addInstruction(WhirlpoolIx.deletePositionBundleIx(ctx.program, { + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + owner: ctx.wallet.publicKey, + receiver: ctx.wallet.publicKey, + })) + // fund rent + .addInstruction({ + instructions:[ + SystemProgram.transfer({ + fromPubkey: ctx.wallet.publicKey, + toPubkey: positionBundleInfo.positionBundlePda.publicKey, + lamports: rentOfPositionBundle, + }) + ], + cleanupInstructions: [], + signers: [], + }); + + await builder.buildAndExecute(); + + // Account closing reassigns to system program and reallocates + // https://github.com/coral-xyz/anchor/pull/2169 + const postClose = await ctx.connection.getAccountInfo(positionBundleInfo.positionBundlePda.publicKey, "confirmed"); + assert.ok(postClose !== null); + assert.ok(postClose.owner.equals(SystemProgram.programId)); + assert.ok(postClose.data.length === 0); + }); + + it("The owner of closed account should be system program", async () => { + // create test pool + const ctx = testCtx.whirlpoolCtx; + const fixture = await new WhirlpoolTestFixture(ctx).init({ + tickSpacing, + positions: [], + rewards: [], + }); + const { poolInitInfo, rewards } = fixture.getInfos(); + + // initialize position bundle + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + const bundleIndex = Math.floor(Math.random() * POSITION_BUNDLE_SIZE); + + const bundledPositionPda = PDAUtil.getBundledPosition(ctx.program.programId, positionBundleInfo.positionBundleMintKeypair.publicKey, bundleIndex); + const bundledPositionPubkey = bundledPositionPda.publicKey; + const whirlpoolPubkey = poolInitInfo.whirlpoolPda.publicKey; + + // open + await toTx( + ctx, + WhirlpoolIx.openBundledPositionIx(ctx.program, { + bundledPositionPda, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + tickLowerIndex, + tickUpperIndex, + whirlpool: whirlpoolPubkey, + funder: ctx.wallet.publicKey + }) + ).buildAndExecute(); + + const preClose = await ctx.connection.getAccountInfo(bundledPositionPubkey, "confirmed"); + assert.ok(preClose !== null); + const rentOfBundledPosition = preClose.lamports; + assert.ok(rentOfBundledPosition > 0); + + const builder = new TransactionBuilder(ctx.connection, ctx.wallet); + builder + // close + .addInstruction(WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: bundledPositionPubkey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: whirlpoolPubkey, + })) + // fund rent + .addInstruction({ + instructions:[ + SystemProgram.transfer({ + fromPubkey: ctx.wallet.publicKey, + toPubkey: bundledPositionPubkey, + lamports: rentOfBundledPosition, + }) + ], + cleanupInstructions: [], + signers: [], + }); + + await builder.buildAndExecute(); + + // Account closing reassigns to system program and reallocates + // https://github.com/coral-xyz/anchor/pull/2169 + const postClose = await ctx.connection.getAccountInfo(bundledPositionPubkey, "confirmed"); + assert.ok(postClose !== null); + assert.ok(postClose.owner.equals(SystemProgram.programId)); + assert.ok(postClose.data.length === 0); + }); + + it("should be failed: close bundled position and then updateFeesAndRewards in single Tx", async () => { + // create test pool + const ctx = testCtx.whirlpoolCtx; + const fixture = await new WhirlpoolTestFixture(ctx).init({ + tickSpacing, + positions: [], + rewards: [], + }); + const { poolInitInfo, rewards } = fixture.getInfos(); + + // initialize position bundle + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + const bundleIndex = Math.floor(Math.random() * POSITION_BUNDLE_SIZE); + + const bundledPositionPda = PDAUtil.getBundledPosition(ctx.program.programId, positionBundleInfo.positionBundleMintKeypair.publicKey, bundleIndex); + const bundledPositionPubkey = bundledPositionPda.publicKey; + const whirlpoolPubkey = poolInitInfo.whirlpoolPda.publicKey; + const tickArrayLower = PDAUtil.getTickArrayFromTickIndex(tickLowerIndex, poolInitInfo.tickSpacing, poolInitInfo.whirlpoolPda.publicKey, ctx.program.programId).publicKey; + const tickArrayUpper = PDAUtil.getTickArrayFromTickIndex(tickUpperIndex, poolInitInfo.tickSpacing, poolInitInfo.whirlpoolPda.publicKey, ctx.program.programId).publicKey; + + const builder = new TransactionBuilder(ctx.connection, ctx.wallet); + builder + // open + .addInstruction(WhirlpoolIx.openBundledPositionIx(ctx.program, { + bundledPositionPda, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + tickLowerIndex, + tickUpperIndex, + whirlpool: whirlpoolPubkey, + funder: ctx.wallet.publicKey + })) + // close + .addInstruction(WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundledPosition: bundledPositionPubkey, + bundleIndex, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + receiver: whirlpoolPubkey, + })) + // try to use closed bundled position + .addInstruction(WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, { + position: bundledPositionPubkey, + tickArrayLower, + tickArrayUpper, + whirlpool: whirlpoolPubkey, + })); + + await assert.rejects( + builder.buildAndExecute(), + /0xbc4/ // AccountNotInitialized + ); + }); + }); + +}); diff --git a/sdk/tests/integration/open_bundled_position.test.ts b/sdk/tests/integration/open_bundled_position.test.ts new file mode 100644 index 0000000..be99f0e --- /dev/null +++ b/sdk/tests/integration/open_bundled_position.test.ts @@ -0,0 +1,613 @@ +import { PDA } from "@orca-so/common-sdk"; +import * as anchor from "@project-serum/anchor"; +import { ASSOCIATED_TOKEN_PROGRAM_ID, Token, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { PublicKey, SystemProgram } from "@solana/web3.js"; +import * as assert from "assert"; +import { + InitPoolParams, + MAX_TICK_INDEX, + MIN_TICK_INDEX, + PDAUtil, + PositionBundleData, + PositionData, + POSITION_BUNDLE_SIZE, + toTx, + WhirlpoolContext, + WhirlpoolIx, +} from "../../src"; +import { + approveToken, + createAssociatedTokenAccount, + ONE_SOL, + systemTransferTx, + TickSpacing, + transfer, + ZERO_BN, +} from "../utils"; +import { initializePositionBundle, initTestPool, openBundledPosition } from "../utils/init-utils"; + +describe("open_bundled_position", () => { + const provider = anchor.AnchorProvider.local(undefined, { + commitment: "confirmed", + preflightCommitment: "confirmed", + }); + + anchor.setProvider(anchor.AnchorProvider.env()); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const fetcher = ctx.fetcher; + + const tickLowerIndex = 0; + const tickUpperIndex = 128; + let poolInitInfo: InitPoolParams; + let whirlpoolPda: PDA; + const funderKeypair = anchor.web3.Keypair.generate(); + + before(async () => { + poolInitInfo = (await initTestPool(ctx, TickSpacing.Standard)).poolInitInfo; + whirlpoolPda = poolInitInfo.whirlpoolPda; + await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute(); + }); + + async function createOpenBundledPositionTx( + ctx: WhirlpoolContext, + positionBundleMint: PublicKey, + bundleIndex: number, + overwrite: any, + ) { + const bundledPositionPda = PDAUtil.getBundledPosition(ctx.program.programId, positionBundleMint, bundleIndex); + const positionBundle = PDAUtil.getPositionBundle(ctx.program.programId, positionBundleMint).publicKey; + + const positionBundleTokenAccount = await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + positionBundleMint, + ctx.wallet.publicKey + ); + + const defaultAccounts = { + bundledPosition: bundledPositionPda.publicKey, + positionBundle, + positionBundleTokenAccount, + positionBundleAuthority: ctx.wallet.publicKey, + whirlpool: whirlpoolPda.publicKey, + funder: ctx.wallet.publicKey, + systemProgram: SystemProgram.programId, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }; + + const ix = program.instruction.openBundledPosition(bundleIndex, tickLowerIndex, tickUpperIndex, { + accounts: { + ...defaultAccounts, + ...overwrite, + } + }); + + return toTx(ctx, { + instructions: [ix], + cleanupInstructions: [], + signers: [], + }); + } + + function checkPositionAccountContents(position: PositionData, mint: PublicKey) { + assert.strictEqual(position.tickLowerIndex, tickLowerIndex); + assert.strictEqual(position.tickUpperIndex, tickUpperIndex); + assert.ok(position.whirlpool.equals(poolInitInfo.whirlpoolPda.publicKey)); + assert.ok(position.positionMint.equals(mint)); + assert.ok(position.liquidity.eq(ZERO_BN)); + assert.ok(position.feeGrowthCheckpointA.eq(ZERO_BN)); + assert.ok(position.feeGrowthCheckpointB.eq(ZERO_BN)); + assert.ok(position.feeOwedA.eq(ZERO_BN)); + assert.ok(position.feeOwedB.eq(ZERO_BN)); + assert.ok(position.rewardInfos[0].amountOwed.eq(ZERO_BN)); + assert.ok(position.rewardInfos[1].amountOwed.eq(ZERO_BN)); + assert.ok(position.rewardInfos[2].amountOwed.eq(ZERO_BN)); + assert.ok(position.rewardInfos[0].growthInsideCheckpoint.eq(ZERO_BN)); + assert.ok(position.rewardInfos[1].growthInsideCheckpoint.eq(ZERO_BN)); + assert.ok(position.rewardInfos[2].growthInsideCheckpoint.eq(ZERO_BN)); + } + + function checkBitmapIsOpened(account: PositionBundleData, bundleIndex: number): boolean { + if (bundleIndex < 0 || bundleIndex >= POSITION_BUNDLE_SIZE) throw Error("bundleIndex is out of bounds"); + + const bitmapIndex = Math.floor(bundleIndex / 8); + const bitmapOffset = bundleIndex % 8; + return (account.positionBitmap[bitmapIndex] & (1 << bitmapOffset)) > 0; + } + + function checkBitmapIsClosed(account: PositionBundleData, bundleIndex: number): boolean { + if (bundleIndex < 0 || bundleIndex >= POSITION_BUNDLE_SIZE) throw Error("bundleIndex is out of bounds"); + + const bitmapIndex = Math.floor(bundleIndex / 8); + const bitmapOffset = bundleIndex % 8; + return (account.positionBitmap[bitmapIndex] & (1 << bitmapOffset)) === 0; + } + + function checkBitmap(account: PositionBundleData, openedBundleIndexes: number[]) { + for (let i=0; i { + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const bundleIndex = 0; + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + const { bundledPositionPda } = positionInitInfo.params; + + const position = (await fetcher.getPosition(bundledPositionPda.publicKey)) as PositionData; + checkPositionAccountContents(position, positionBundleInfo.positionBundleMintKeypair.publicKey); + + const positionBundle = (await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true)) as PositionBundleData; + checkBitmap(positionBundle, [bundleIndex]); + }); + + it("successfully opens bundled position when funder is different than account paying for transaction fee", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const preBalance = await ctx.connection.getBalance(ctx.wallet.publicKey); + + const bundleIndex = POSITION_BUNDLE_SIZE - 1; + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex, + ctx.wallet.publicKey, + funderKeypair, + ); + const { bundledPositionPda } = positionInitInfo.params; + + 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 any rent + + const position = (await fetcher.getPosition(bundledPositionPda.publicKey)) as PositionData; + checkPositionAccountContents(position, positionBundleInfo.positionBundleMintKeypair.publicKey); + + const positionBundle = (await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true)) as PositionBundleData; + checkBitmap(positionBundle, [bundleIndex]); + }); + + it("successfully opens multiple bundled position and verify bitmap", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const bundleIndexes = [1, 7, 8, 64, 127, 128, 254, 255]; + for (const bundleIndex of bundleIndexes) { + const positionInitInfo = await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + const { bundledPositionPda } = positionInitInfo.params; + + const position = (await fetcher.getPosition(bundledPositionPda.publicKey)) as PositionData; + checkPositionAccountContents(position, positionBundleInfo.positionBundleMintKeypair.publicKey); + } + + const positionBundle = (await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true)) as PositionBundleData; + checkBitmap(positionBundle, bundleIndexes); + }); + + describe("invalid bundle index", () => { + it("should be failed: invalid bundle index (< 0)", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const bundleIndex = -1; + await assert.rejects( + openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex, + ), + /It must be >= 0 and <= 65535/ // rejected by client + ); + }); + + it("should be failed: invalid bundle index (POSITION_BUNDLE_SIZE)", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const bundleIndex = POSITION_BUNDLE_SIZE; + await assert.rejects( + openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex, + ), + /0x179b/ // InvalidBundleIndex + ); + }); + + + it("should be failed: invalid bundle index (u16 max)", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const bundleIndex = 2**16 - 1; + await assert.rejects( + openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex, + ), + /0x179b/ // InvalidBundleIndex + ); + }); + }); + + describe("invalid tick index", () => { + async function assertTicksFail(lowerTick: number, upperTick: number) { + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + const bundleIndex = 0; + await assert.rejects( + openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + lowerTick, + upperTick, + provider.wallet.publicKey, + funderKeypair + ), + /0x177a/ // InvalidTickIndex + ); + } + + it("should be failed: user pass in an out of bound tick index for upper-index", async () => { + await assertTicksFail(0, MAX_TICK_INDEX + 1); + }); + + it("should be failed: user pass in a lower tick index that is higher than the upper-index", async () => { + await assertTicksFail(-22534, -22534 - 1); + }); + + it("should be failed: user pass in a lower tick index that equals the upper-index", async () => { + await assertTicksFail(22365, 22365); + }); + + it("should be failed: user pass in an out of bound tick index for lower-index", async () => { + await assertTicksFail(MIN_TICK_INDEX - 1, 0); + }); + + it("should be failed: user pass in a non-initializable tick index for upper-index", async () => { + await assertTicksFail(0, 1); + }); + + it("should be failed: user pass in a non-initializable tick index for lower-index", async () => { + await assertTicksFail(1, 2); + }); + }); + + it("should be fail: user opens bundled position with bundle index whose state is opened", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const bundleIndex = 0; + await openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ); + + const positionBundle = (await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true)) as PositionBundleData; + assert.ok(checkBitmapIsOpened(positionBundle, bundleIndex)); + + await assert.rejects( + openBundledPosition( + ctx, + whirlpoolPda.publicKey, + positionBundleInfo.positionBundleMintKeypair.publicKey, + bundleIndex, + tickLowerIndex, + tickUpperIndex + ), + (err) => { return JSON.stringify(err).includes("already in use") } + ); + }); + + describe("invalid input account", () => { + it("should be failed: invalid bundled position", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const tx = await createOpenBundledPositionTx( + ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, { + // invalid parameter + bundledPosition: PDAUtil.getBundledPosition( + ctx.program.programId, + positionBundleInfo.positionBundleMintKeypair.publicKey, + 1 // another bundle index + ).publicKey + } + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x7d6/ // ConstraintSeeds + ); + }); + + it("should be failed: invalid position bundle", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + const otherPositionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const tx = await createOpenBundledPositionTx( + ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, { + // invalid parameter + positionBundle: otherPositionBundleInfo.positionBundlePda.publicKey, + } + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x7d6/ // ConstraintSeeds + ); + }); + + it("should be failed: invalid ATA (amount is zero)", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, funderKeypair.publicKey, funderKeypair); + + const ata = await createAssociatedTokenAccount( + provider, + positionBundleInfo.positionBundleMintKeypair.publicKey, + ctx.wallet.publicKey, + ctx.wallet.publicKey + ); + + const tx = await createOpenBundledPositionTx( + ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, { + // invalid parameter + positionBundleTokenAccount: ata, + } + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x7d3/ // ConstraintRaw (amount == 1) + ); + }); + + it("should be failed: invalid ATA (invalid mint)", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, funderKeypair.publicKey, funderKeypair); + const otherPositionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey); + + const tx = await createOpenBundledPositionTx( + ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, { + // invalid parameter + positionBundleTokenAccount: otherPositionBundleInfo.positionBundleTokenAccount, + } + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x7d3/ // ConstraintRaw (mint == position_bundle.position_bundle_mint) + ); + }); + + it("should be failed: invalid position bundle authority", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, funderKeypair.publicKey, funderKeypair); + + const tx = await createOpenBundledPositionTx( + ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, { + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + // invalid parameter + positionBundleAuthority: ctx.wallet.publicKey, + } + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x1783/ // MissingOrInvalidDelegate + ); + }); + + it("should be failed: invalid whirlpool", async () => { + const positionBundleInfo = await initializePositionBundle(ctx); + + const tx = await createOpenBundledPositionTx( + ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, { + // invalid parameter + whirlpool: positionBundleInfo.positionBundlePda.publicKey, + } + ); + + await assert.rejects( + tx.buildAndExecute(), + /0xbba/ // AccountDiscriminatorMismatch + ); + }); + + + it("should be failed: invalid system program", async () => { + const positionBundleInfo = await initializePositionBundle(ctx); + + const tx = await createOpenBundledPositionTx( + ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, { + // invalid parameter + systemProgram: TOKEN_PROGRAM_ID, + } + ); + + await assert.rejects( + tx.buildAndExecute(), + /0xbc0/ // InvalidProgramId + ); + }); + + it("should be failed: invalid rent", async () => { + const positionBundleInfo = await initializePositionBundle(ctx); + + const tx = await createOpenBundledPositionTx( + ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, { + // invalid parameter + rent: anchor.web3.SYSVAR_CLOCK_PUBKEY, + } + ); + + await assert.rejects( + tx.buildAndExecute(), + /0xbc7/ // AccountSysvarMismatch + ); + }); + }); + + describe("authority delegation", () => { + it("successfully opens bundled position with delegated authority", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, funderKeypair.publicKey, funderKeypair); + + const tx = await createOpenBundledPositionTx( + ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, { + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + positionBundleAuthority: ctx.wallet.publicKey, + } + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x1783/ // MissingOrInvalidDelegate + ); + + // delegate 1 token from funder to ctx.wallet + await approveToken( + provider, + positionBundleInfo.positionBundleTokenAccount, + ctx.wallet.publicKey, + 1, + funderKeypair + ); + + await tx.buildAndExecute(); + const positionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + checkBitmapIsOpened(positionBundle!, 0); + }); + + it("successfully opens bundled position even if delegation exists", async () => { + const positionBundleInfo = await initializePositionBundle(ctx); + + const tx = await createOpenBundledPositionTx( + ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, { + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + positionBundleAuthority: ctx.wallet.publicKey, + } + ); + + // delegate 1 token from ctx.wallet to funder + await approveToken( + provider, + positionBundleInfo.positionBundleTokenAccount, + funderKeypair.publicKey, + 1, + ); + + // owner can open even if delegation exists + await tx.buildAndExecute(); + const positionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + checkBitmapIsOpened(positionBundle!, 0); + }); + + + it("should be failed: delegated amount is zero", async () => { + const positionBundleInfo = await initializePositionBundle(ctx, funderKeypair.publicKey, funderKeypair); + + const tx = await createOpenBundledPositionTx( + ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, { + positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, + positionBundleAuthority: ctx.wallet.publicKey, + } + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x1783/ // MissingOrInvalidDelegate + ); + + // delegate ZERO token from funder to ctx.wallet + await approveToken( + provider, + positionBundleInfo.positionBundleTokenAccount, + ctx.wallet.publicKey, + 0, + funderKeypair + ); + + await assert.rejects( + tx.buildAndExecute(), + /0x1784/ // InvalidPositionTokenAmount + ); + }); + }); + + describe("transfer position bundle", () => { + it("successfully opens bundled position after position bundle token transfer", async () => { + const positionBundleInfo = await initializePositionBundle(ctx); + + const funderATA = await createAssociatedTokenAccount( + provider, + positionBundleInfo.positionBundleMintKeypair.publicKey, + funderKeypair.publicKey, + ctx.wallet.publicKey, + ); + + await transfer( + provider, + positionBundleInfo.positionBundleTokenAccount, + funderATA, + 1 + ); + + const tokenInfo = await fetcher.getTokenInfo(funderATA, true); + assert.ok(tokenInfo?.amount.eqn(1)); + + const tx = toTx( + ctx, + WhirlpoolIx.openBundledPositionIx(ctx.program, { + bundledPositionPda: PDAUtil.getBundledPosition(ctx.program.programId, positionBundleInfo.positionBundleMintKeypair.publicKey, 0), + bundleIndex: 0, + funder: funderKeypair.publicKey, + positionBundle: positionBundleInfo.positionBundlePda.publicKey, + positionBundleAuthority: funderKeypair.publicKey, + positionBundleTokenAccount: funderATA, + tickLowerIndex, + tickUpperIndex, + whirlpool: whirlpoolPda.publicKey, + }) + ); + tx.addSigner(funderKeypair); + + await tx.buildAndExecute(); + const positionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true); + checkBitmapIsOpened(positionBundle!, 0); + }); + }); + +}); diff --git a/sdk/tests/integration/set_fee_rate.test.ts b/sdk/tests/integration/set_fee_rate.test.ts index 501f171..5be6dfe 100644 --- a/sdk/tests/integration/set_fee_rate.test.ts +++ b/sdk/tests/integration/set_fee_rate.test.ts @@ -114,7 +114,9 @@ describe("set_fee_rate", () => { }, signers: [configKeypairs.feeAuthorityKeypair], }), - /A has_one constraint was violated/ // ConstraintHasOne + // message have been changed + // https://github.com/coral-xyz/anchor/pull/2101/files#diff-e564d6832afe5358ef129e96970ba1e5180b5e74aba761831e1923c06d7b839fR412 + /A has[_ ]one constraint was violated/ // ConstraintHasOne ); }); diff --git a/sdk/tests/integration/set_protocol_fee_rate.test.ts b/sdk/tests/integration/set_protocol_fee_rate.test.ts index 1b8667d..4a94037 100644 --- a/sdk/tests/integration/set_protocol_fee_rate.test.ts +++ b/sdk/tests/integration/set_protocol_fee_rate.test.ts @@ -111,7 +111,9 @@ describe("set_protocol_fee_rate", () => { }, signers: [configKeypairs.feeAuthorityKeypair], }), - /A has_one constraint was violated/ // ConstraintHasOne + // message have been changed + // https://github.com/coral-xyz/anchor/pull/2101/files#diff-e564d6832afe5358ef129e96970ba1e5180b5e74aba761831e1923c06d7b839fR412 + /A has[_ ]one constraint was violated/ // ConstraintHasOne ); }); diff --git a/sdk/tests/sdk/whirlpools/utils/position-bundle-util.test.ts b/sdk/tests/sdk/whirlpools/utils/position-bundle-util.test.ts new file mode 100644 index 0000000..04e02a8 --- /dev/null +++ b/sdk/tests/sdk/whirlpools/utils/position-bundle-util.test.ts @@ -0,0 +1,149 @@ +import * as assert from "assert"; +import { PositionBundleUtil, POSITION_BUNDLE_SIZE } from "../../../../src"; +import { buildPositionBundleData } from "../../../utils/testDataTypes"; + +describe("PositionBundleUtil tests", () => { + const occupiedEmpty: number[] = []; + const occupiedPartial: number[] = [0, 1, 5, 49, 128, 193, 255]; + const occupiedFull: number[] = new Array(POSITION_BUNDLE_SIZE).fill(0).map((a, i) => i); + + describe("checkBundleIndexInBounds", () => { + it("valid bundle indexes", async () => { + for (let bundleIndex=0; bundleIndex { + assert.ok(!PositionBundleUtil.checkBundleIndexInBounds(-1)); + }); + + it("greater than or equal to POSITION_BUNDLE_SIZE", async () => { + assert.ok(!PositionBundleUtil.checkBundleIndexInBounds(POSITION_BUNDLE_SIZE)); + assert.ok(!PositionBundleUtil.checkBundleIndexInBounds(POSITION_BUNDLE_SIZE+1)); + }); + }); + + it("isOccupied / isUnoccupied", async () => { + const positionBundle = buildPositionBundleData(occupiedPartial); + + for (let bundleIndex=0; bundleIndex { + it("empty", async () => { + const positionBundle = buildPositionBundleData(occupiedEmpty); + assert.ok(PositionBundleUtil.isEmpty(positionBundle)); + assert.ok(!PositionBundleUtil.isFull(positionBundle)); + }); + + it("some bundle indexes are occupied", async () => { + const positionBundle = buildPositionBundleData(occupiedPartial); + assert.ok(!PositionBundleUtil.isEmpty(positionBundle)); + assert.ok(!PositionBundleUtil.isFull(positionBundle)); + }); + + it("full", async () => { + const positionBundle = buildPositionBundleData(occupiedFull); + assert.ok(!PositionBundleUtil.isEmpty(positionBundle)); + assert.ok(PositionBundleUtil.isFull(positionBundle)); + }) + }) + + describe("getOccupiedBundleIndexes", () => { + it("empty", async () => { + const positionBundle = buildPositionBundleData(occupiedEmpty); + const result = PositionBundleUtil.getOccupiedBundleIndexes(positionBundle); + assert.equal(result.length, 0); + }); + + it("some bundle indexes are occupied", async () => { + const positionBundle = buildPositionBundleData(occupiedPartial); + const result = PositionBundleUtil.getOccupiedBundleIndexes(positionBundle); + assert.equal(result.length, occupiedPartial.length); + assert.ok(occupiedPartial.every(index => result.includes(index))); + }); + + it("full", async () => { + const positionBundle = buildPositionBundleData(occupiedFull); + const result = PositionBundleUtil.getOccupiedBundleIndexes(positionBundle); + assert.equal(result.length, POSITION_BUNDLE_SIZE); + assert.ok(occupiedFull.every(index => result.includes(index))); + }) + }); + + describe("getUnoccupiedBundleIndexes", () => { + it("empty", async () => { + const positionBundle = buildPositionBundleData(occupiedEmpty); + const result = PositionBundleUtil.getUnoccupiedBundleIndexes(positionBundle); + assert.equal(result.length, POSITION_BUNDLE_SIZE); + assert.ok(occupiedFull.every(index => result.includes(index))); + }); + + it("some bundle indexes are occupied", async () => { + const positionBundle = buildPositionBundleData(occupiedPartial); + const result = PositionBundleUtil.getUnoccupiedBundleIndexes(positionBundle); + assert.equal(result.length, POSITION_BUNDLE_SIZE - occupiedPartial.length); + assert.ok(occupiedPartial.every(index => !result.includes(index))); + }); + + it("full", async () => { + const positionBundle = buildPositionBundleData(occupiedFull); + const result = PositionBundleUtil.getUnoccupiedBundleIndexes(positionBundle); + assert.equal(result.length, 0); + }) + }); + + + describe("findUnoccupiedBundleIndex", () => { + it("empty", async () => { + const positionBundle = buildPositionBundleData(occupiedEmpty); + const result = PositionBundleUtil.findUnoccupiedBundleIndex(positionBundle); + assert.equal(result, 0); + }); + + it("some bundle indexes are occupied", async () => { + const positionBundle = buildPositionBundleData(occupiedPartial); + const result = PositionBundleUtil.findUnoccupiedBundleIndex(positionBundle); + assert.equal(result, 2); + }); + + it("full", async () => { + const positionBundle = buildPositionBundleData(occupiedFull); + const result = PositionBundleUtil.findUnoccupiedBundleIndex(positionBundle); + assert.ok(result === null); + }) + }); + + describe("convertBitmapToArray", () => { + it("empty", async () => { + const positionBundle = buildPositionBundleData(occupiedEmpty); + const result = PositionBundleUtil.convertBitmapToArray(positionBundle); + assert.equal(result.length, POSITION_BUNDLE_SIZE); + assert.ok(result.every((occupied) => !occupied)); + }); + + it("some bundle indexes are occupied", async () => { + const positionBundle = buildPositionBundleData(occupiedPartial); + const result = PositionBundleUtil.convertBitmapToArray(positionBundle); + assert.equal(result.length, POSITION_BUNDLE_SIZE); + assert.ok(result.every((occupied, i) => occupied === occupiedPartial.includes(i))); + }); + + it("full", async () => { + const positionBundle = buildPositionBundleData(occupiedFull); + const result = PositionBundleUtil.convertBitmapToArray(positionBundle); + assert.equal(result.length, POSITION_BUNDLE_SIZE); + assert.ok(result.every((occupied) => occupied)); + }) + }); +}); diff --git a/sdk/tests/sdk/whirlpools/whirlpool-impl.test.ts b/sdk/tests/sdk/whirlpools/whirlpool-impl.test.ts index b0e45c3..f03207a 100644 --- a/sdk/tests/sdk/whirlpools/whirlpool-impl.test.ts +++ b/sdk/tests/sdk/whirlpools/whirlpool-impl.test.ts @@ -436,13 +436,6 @@ describe("whirlpool-impl", () => { tickUpper: position.getUpperTickData(), }); - const rewardsQuote = collectRewardsQuote({ - whirlpool: poolData, - position: positionData, - tickLower: position.getLowerTickData(), - tickUpper: position.getUpperTickData(), - }); - let ataTx: TransactionBuilder | undefined; let closeTx: TransactionBuilder; if (txs.length === 1) { @@ -455,7 +448,19 @@ describe("whirlpool-impl", () => { } await ataTx?.buildAndExecute(); - await closeTx.addSigner(otherWallet).buildAndExecute(); + const signature = await closeTx.addSigner(otherWallet).buildAndExecute(); + + // To calculate the rewards that have accumulated up to the timing of the close, + // the block time at transaction execution is used. + const tx = await ctx.provider.connection.getTransaction(signature); + const closeTimestampInSeconds = new anchor.BN(tx!.blockTime!.toString()); + const rewardsQuote = collectRewardsQuote({ + whirlpool: poolData, + position: positionData, + tickLower: position.getLowerTickData(), + tickUpper: position.getUpperTickData(), + timeStampInSeconds: closeTimestampInSeconds, + }); assert.equal( await getTokenBalance(ctx.provider, dWalletTokenAAccount), @@ -591,13 +596,6 @@ describe("whirlpool-impl", () => { tickUpper: position.getUpperTickData(), }); - const rewardsQuote = collectRewardsQuote({ - whirlpool: poolData, - position: positionData, - tickLower: position.getLowerTickData(), - tickUpper: position.getUpperTickData(), - }); - const dWalletTokenBAccount = await deriveATA(otherWallet.publicKey, poolData.tokenMintB); const rewardAccount0 = await deriveATA(otherWallet.publicKey, poolData.rewardInfos[0].mint); const rewardAccount1 = await deriveATA(otherWallet.publicKey, poolData.rewardInfos[1].mint); @@ -626,7 +624,19 @@ describe("whirlpool-impl", () => { const positionAccountBalance = await ctx.connection.getBalance(positionWithFees.publicKey); await ataTx?.buildAndExecute(); - await closeTx.addSigner(otherWallet).buildAndExecute(); + const signature = await closeTx.addSigner(otherWallet).buildAndExecute(); + + // To calculate the rewards that have accumulated up to the timing of the close, + // the block time at transaction execution is used. + const tx = await ctx.provider.connection.getTransaction(signature); + const closeTimestampInSeconds = new anchor.BN(tx!.blockTime!.toString()); + const rewardsQuote = collectRewardsQuote({ + whirlpool: poolData, + position: positionData, + tickLower: position.getLowerTickData(), + tickUpper: position.getUpperTickData(), + timeStampInSeconds: closeTimestampInSeconds, + }); const otherWalletBalanceAfter = await ctx.connection.getBalance(otherWallet.publicKey); diff --git a/sdk/tests/utils/init-utils.ts b/sdk/tests/utils/init-utils.ts index f3acb8a..e94c930 100644 --- a/sdk/tests/utils/init-utils.ts +++ b/sdk/tests/utils/init-utils.ts @@ -1,4 +1,4 @@ -import { AddressUtil, MathUtil, PDA } from "@orca-so/common-sdk"; +import { deriveATA, AddressUtil, MathUtil, PDA } from "@orca-so/common-sdk"; import * as anchor from "@project-serum/anchor"; import { NATIVE_MINT, u64 } from "@solana/spl-token"; import { Keypair, PublicKey } from "@solana/web3.js"; @@ -32,6 +32,7 @@ import { generateDefaultInitFeeTierParams, generateDefaultInitPoolParams, generateDefaultInitTickArrayParams, + generateDefaultOpenBundledPositionParams, generateDefaultOpenPositionParams, TestConfigParams, TestWhirlpoolsConfigKeypairs, @@ -883,3 +884,102 @@ export async function initTestPoolWithLiquidity( feeTierParams, }; } + +export async function initializePositionBundleWithMetadata( + ctx: WhirlpoolContext, + owner: PublicKey = ctx.provider.wallet.publicKey, + funder?: Keypair +) { + const positionBundleMintKeypair = Keypair.generate(); + const positionBundlePda = PDAUtil.getPositionBundle(ctx.program.programId, positionBundleMintKeypair.publicKey); + const positionBundleMetadataPda = PDAUtil.getPositionBundleMetadata(positionBundleMintKeypair.publicKey); + const positionBundleTokenAccount = await deriveATA(owner, positionBundleMintKeypair.publicKey); + + const tx = toTx(ctx, WhirlpoolIx.initializePositionBundleWithMetadataIx( + ctx.program, + { + positionBundleMintKeypair, + positionBundlePda, + positionBundleMetadataPda, + owner, + positionBundleTokenAccount, + funder: !!funder ? funder.publicKey : owner, + }, + )); + if (funder) { + tx.addSigner(funder); + } + + const txId = await tx.buildAndExecute(); + + return { + txId, + positionBundleMintKeypair, + positionBundlePda, + positionBundleMetadataPda, + positionBundleTokenAccount, + }; +} + +export async function initializePositionBundle( + ctx: WhirlpoolContext, + owner: PublicKey = ctx.provider.wallet.publicKey, + funder?: Keypair +) { + const positionBundleMintKeypair = Keypair.generate(); + const positionBundlePda = PDAUtil.getPositionBundle(ctx.program.programId, positionBundleMintKeypair.publicKey); + const positionBundleTokenAccount = await deriveATA(owner, positionBundleMintKeypair.publicKey); + + const tx = toTx(ctx, WhirlpoolIx.initializePositionBundleIx( + ctx.program, + { + positionBundleMintKeypair, + positionBundlePda, + owner, + positionBundleTokenAccount, + funder: !!funder ? funder.publicKey : owner, + }, + )); + if (funder) { + tx.addSigner(funder); + } + + const txId = await tx.buildAndExecute(); + + return { + txId, + positionBundleMintKeypair, + positionBundlePda, + positionBundleTokenAccount, + }; +} + +export async function openBundledPosition( + ctx: WhirlpoolContext, + whirlpool: PublicKey, + positionBundleMint: PublicKey, + bundleIndex: number, + tickLowerIndex: number, + tickUpperIndex: number, + owner: PublicKey = ctx.provider.wallet.publicKey, + funder?: Keypair +) { + const { params } = await generateDefaultOpenBundledPositionParams( + ctx, + whirlpool, + positionBundleMint, + bundleIndex, + tickLowerIndex, + tickUpperIndex, + owner, + funder?.publicKey || owner + ); + + const tx = toTx(ctx, WhirlpoolIx.openBundledPositionIx(ctx.program, params)); + if (funder) { + tx.addSigner(funder); + } + + const txId = await tx.buildAndExecute(); + return { txId, params }; +} diff --git a/sdk/tests/utils/test-builders.ts b/sdk/tests/utils/test-builders.ts index 0a95868..118c928 100644 --- a/sdk/tests/utils/test-builders.ts +++ b/sdk/tests/utils/test-builders.ts @@ -16,6 +16,7 @@ import { InitPoolParams, InitTickArrayParams, OpenPositionParams, + OpenBundledPositionParams, PDAUtil, PoolUtil, PriceMath, @@ -254,3 +255,39 @@ export async function initPosition( positionAddress: PDAUtil.getPosition(ctx.program.programId, positionMint), }; } + +export async function generateDefaultOpenBundledPositionParams( + context: WhirlpoolContext, + whirlpool: PublicKey, + positionBundleMint: PublicKey, + bundleIndex: number, + tickLowerIndex: number, + tickUpperIndex: number, + owner: PublicKey, + funder?: PublicKey +): Promise<{ params: Required }> { + const bundledPositionPda = PDAUtil.getBundledPosition(context.program.programId, positionBundleMint, bundleIndex); + const positionBundle = PDAUtil.getPositionBundle(context.program.programId, positionBundleMint).publicKey; + + const positionBundleTokenAccount = await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + positionBundleMint, + owner + ); + + const params: Required = { + bundleIndex, + bundledPositionPda, + positionBundle, + positionBundleAuthority: owner, + funder: funder || owner, + positionBundleTokenAccount, + whirlpool: whirlpool, + tickLowerIndex, + tickUpperIndex, + }; + return { + params, + }; +} diff --git a/sdk/tests/utils/testDataTypes.ts b/sdk/tests/utils/testDataTypes.ts index 9a2b9ca..020f8f7 100644 --- a/sdk/tests/utils/testDataTypes.ts +++ b/sdk/tests/utils/testDataTypes.ts @@ -2,9 +2,12 @@ import { ZERO } from "@orca-so/common-sdk"; import { web3 } from "@project-serum/anchor"; import { PublicKey, Keypair } from "@solana/web3.js"; import { BN } from "bn.js"; +import invariant from "tiny-invariant"; import { AccountFetcher, PDAUtil, + PositionBundleData, + POSITION_BUNDLE_SIZE, PriceMath, TickArray, TickArrayData, @@ -98,3 +101,16 @@ export async function getTickArrays( }; }); } + +export const buildPositionBundleData = (occupiedBundleIndexes: number[]): PositionBundleData => { + invariant(POSITION_BUNDLE_SIZE % 8 == 0, "POSITION_BUNDLE_SIZE should be multiple of 8"); + + const positionBundleMint = Keypair.generate().publicKey; + const positionBitmap: number[] = new Array(POSITION_BUNDLE_SIZE / 8).fill(0); + occupiedBundleIndexes.forEach((bundleIndex) => { + const index = Math.floor(bundleIndex / 8); + const offset = bundleIndex % 8; + positionBitmap[index] = positionBitmap[index] | (1 << offset); + }); + return { positionBundleMint, positionBitmap }; +};