From 9e528b0b33011332feb460174bd1e82665f3d511 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 6 Aug 2026 15:46:18 +0900 Subject: [PATCH 1/4] initial AI generated output --- Cargo.lock | 1439 +++++++++++++++++++++++++++++++++++- Cargo.toml | 3 + test-cli/Cargo.toml | 3 + test-cli/src/cmd/settle.rs | 268 +++++-- test-cli/src/main.rs | 1 + test-cli/src/orca.rs | 316 ++++++++ 6 files changed, 1945 insertions(+), 85 deletions(-) create mode 100644 test-cli/src/orca.rs diff --git a/Cargo.lock b/Cargo.lock index f96263a..2b939e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,6 +230,58 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +[[package]] +name = "anza-quinn" +version = "0.11.9-rustsec20260037" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91bfa08f6e7e4187354ff4f793b81cc08218a6a95cc48f5de7616d44452bf6e0" +dependencies = [ + "anza-quinn-proto", + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "anza-quinn-proto" +version = "0.11.13-rustsec20260037" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00a4d8cf8d72ee56e0ee20d3b4eef4785ce1b05299c4982c6de7f251c458efe" +dependencies = [ + "bytes", + "fastbloom", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + [[package]] name = "ark-bn254" version = "0.4.0" @@ -489,6 +541,45 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "async-compression" version = "0.4.42" @@ -501,6 +592,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -536,6 +638,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.22.1" @@ -577,6 +685,9 @@ name = "bitflags" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] [[package]] name = "blake3" @@ -721,6 +832,18 @@ name = "bytes" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +dependencies = [ + "serde", +] + +[[package]] +name = "caps" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1ddba47aba30b6a889298ad0109c3b8dcb0e8fc993b459daa7067d46f865e0" +dependencies = [ + "libc", +] [[package]] name = "cc" @@ -734,6 +857,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -832,6 +961,16 @@ dependencies = [ "unreachable", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "compression-codecs" version = "0.4.38" @@ -874,13 +1013,31 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cow-test-cli" version = "0.1.0" dependencies = [ "anyhow", "clap", + "orca_whirlpools", "settlement-client", + "solana-client", "solana-commitment-config", "solana-hash 3.1.0", "solana-instruction", @@ -892,6 +1049,7 @@ dependencies = [ "spl-associated-token-account-interface", "spl-token", "spl-token-interface", + "tokio", ] [[package]] @@ -921,6 +1079,40 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + [[package]] name = "crunchy" version = "0.2.4" @@ -1040,6 +1232,25 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + [[package]] name = "der" version = "0.7.10" @@ -1050,6 +1261,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint 0.4.6", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "derivation-path" version = "0.2.0" @@ -1130,6 +1361,29 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cbae11b3de8fce2a456e8ea3dada226b35fe791f0dc1d360c0941f0bb681f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "eager" version = "0.1.0" @@ -1286,6 +1540,44 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ethnum" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "rand 0.9.4", + "siphasher 1.0.3", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -1440,6 +1732,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + [[package]] name = "futures-util" version = "0.3.32" @@ -1468,6 +1766,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -1517,6 +1825,26 @@ dependencies = [ "r-efi 6.0.0", ] +[[package]] +name = "governor" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" +dependencies = [ + "cfg-if", + "dashmap", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "rand 0.8.6", + "smallvec", + "spinning_top", +] + [[package]] name = "group" version = "0.13.0" @@ -1546,6 +1874,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -1567,12 +1901,24 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex-literal" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" +[[package]] +name = "histogram" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cb882ccb290b8646e554b157ab0b71e64e8d5bef775cd66b6531e52d302669" + [[package]] name = "hmac" version = "0.12.1" @@ -1582,6 +1928,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.4.2" @@ -1599,7 +1956,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.4.2", ] [[package]] @@ -1610,7 +1967,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", + "http 1.4.2", "http-body", "pin-project-lite", ] @@ -1640,7 +1997,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "http", + "http 1.4.2", "http-body", "httparse", "itoa", @@ -1656,14 +2013,14 @@ version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http", + "http 1.4.2", "hyper", "hyper-util", "rustls", "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 1.0.8", ] [[package]] @@ -1676,7 +2033,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", + "http 1.4.2", "http-body", "hyper", "ipnet", @@ -1885,31 +2242,75 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "jobserver" -version = "0.1.34" +name = "jni" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ - "getrandom 0.3.4", - "libc", + "cesu8", + "cfg-if", + "combine 4.6.7", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", ] [[package]] -name = "js-sys" -version = "0.3.103" +name = "jni-sys" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", + "jni-sys 0.4.1", ] [[package]] -name = "jsonrpc-core" -version = "18.0.0" +name = "jni-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpc-core" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" dependencies = [ "futures", "futures-executor", @@ -1955,6 +2356,12 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libsecp256k1" version = "0.6.0" @@ -2171,6 +2578,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2192,6 +2605,41 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "num" version = "0.2.1" @@ -2237,6 +2685,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-derive" version = "0.4.2" @@ -2289,6 +2743,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_enum" version = "0.7.6" @@ -2311,6 +2775,15 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2329,6 +2802,97 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "orca_whirlpools" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6471e77f1decfe9309b492f11482a218f5c19170d9e2323346cab4d65888e9c1" +dependencies = [ + "bincode", + "bs58", + "orca_whirlpools_client", + "orca_whirlpools_core", + "serde", + "serde_json", + "solana-account", + "solana-account-decoder", + "solana-client", + "solana-commitment-config", + "solana-epoch-info", + "solana-instruction", + "solana-keypair", + "solana-message", + "solana-program-error", + "solana-program-pack", + "solana-pubkey 3.0.0", + "solana-rent 3.1.0", + "solana-sha256-hasher", + "solana-signature", + "solana-system-interface 2.0.0", + "solana-sysvar-id", + "solana-transaction", + "spl-associated-token-account-interface", + "spl-memo-interface", + "spl-token-2022-interface", + "spl-token-interface", +] + +[[package]] +name = "orca_whirlpools_client" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08496c5b41482cfdd9ccbde3d25f3d063c5a2eab0dec913059307ede01c068f8" +dependencies = [ + "borsh", + "num-derive", + "num-traits", + "orca_whirlpools_core", + "solana-account", + "solana-account-decoder", + "solana-account-info", + "solana-client", + "solana-cpi", + "solana-instruction", + "solana-program", + "solana-program-error", + "solana-pubkey 3.0.0", + "thiserror 2.0.18", +] + +[[package]] +name = "orca_whirlpools_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fa5a2318d63ef4c7241234b3aa53690bf76b7bd797c90b90ec38f494d847cc6" +dependencies = [ + "ethnum", + "libm", + "orca_whirlpools_macros", +] + +[[package]] +name = "orca_whirlpools_macros" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b093a9e60ca608c1dff40fcd841a2d5680b96f877080b6279044c159ebd50b78" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2373,6 +2937,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2472,6 +3045,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2538,6 +3117,21 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi 0.11.1+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -2729,6 +3323,35 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -2778,7 +3401,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http", + "http 1.4.2", "http-body", "http-body-util", "hyper", @@ -2804,7 +3427,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 1.0.8", ] [[package]] @@ -2815,7 +3438,7 @@ checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" dependencies = [ "anyhow", "async-trait", - "http", + "http 1.4.2", "reqwest", "serde", "thiserror 1.0.69", @@ -2867,6 +3490,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "1.1.4" @@ -2894,6 +3526,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.14.1" @@ -2904,6 +3548,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.13" @@ -2939,6 +3610,24 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2959,6 +3648,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -3110,6 +3822,17 @@ dependencies = [ "spl-token-interface", ] +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.9.9" @@ -3188,6 +3911,12 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "slab" version = "0.4.12" @@ -3493,6 +4222,75 @@ dependencies = [ "solana-vote-program", ] +[[package]] +name = "solana-client" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1021bbda77368c57f3d8b850c5d9b19077906d21d1c516b1ccfbb8fe7737553f" +dependencies = [ + "anza-quinn", + "async-trait", + "bincode", + "dashmap", + "futures", + "futures-util", + "indexmap", + "indicatif", + "log", + "rayon", + "solana-account", + "solana-client-traits", + "solana-commitment-config", + "solana-connection-cache", + "solana-epoch-info", + "solana-hash 3.1.0", + "solana-instruction", + "solana-keypair", + "solana-measure", + "solana-message", + "solana-net-utils", + "solana-pubkey 3.0.0", + "solana-pubsub-client", + "solana-quic-client", + "solana-quic-definitions", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-rpc-client-nonce-utils", + "solana-signature", + "solana-signer", + "solana-streamer", + "solana-time-utils", + "solana-tpu-client", + "solana-transaction", + "solana-transaction-error", + "solana-transaction-status-client-types", + "solana-udp-client", + "thiserror 2.0.18", + "tokio", + "tokio-util", +] + +[[package]] +name = "solana-client-traits" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08618ed587e128105510c54ae3e456b9a06d674d8640db75afe66dad65cb4e02" +dependencies = [ + "solana-account", + "solana-commitment-config", + "solana-epoch-info", + "solana-hash 3.1.0", + "solana-instruction", + "solana-keypair", + "solana-message", + "solana-pubkey 3.0.0", + "solana-signature", + "solana-signer", + "solana-system-interface 2.0.0", + "solana-transaction", + "solana-transaction-error", +] + [[package]] name = "solana-clock" version = "3.1.1" @@ -3507,6 +4305,15 @@ dependencies = [ "solana-sysvar-id", ] +[[package]] +name = "solana-cluster-type" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a494cf8eda7d98d9f0144b288bb409c88308d2e86f15cc1045aa77b83304718" +dependencies = [ + "solana-hash 4.4.0", +] + [[package]] name = "solana-commitment-config" version = "3.1.1" @@ -3585,6 +4392,29 @@ dependencies = [ "solana-system-interface 2.0.0", ] +[[package]] +name = "solana-connection-cache" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a1f37271395e9e5371ef18d93197d5d53a18acd5651b66a5aa2ac68bd6dd54" +dependencies = [ + "async-trait", + "bincode", + "crossbeam-channel", + "futures-util", + "indexmap", + "log", + "rand 0.8.6", + "rayon", + "solana-keypair", + "solana-measure", + "solana-metrics", + "solana-time-utils", + "solana-transaction-error", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "solana-cpi" version = "3.1.0" @@ -3673,7 +4503,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ee8beac9bff4db9225e57d532d169b0be5e447f1e6601a2f50f27a01bf5518f" dependencies = [ - "siphasher", + "siphasher 0.3.11", "solana-address 2.6.1", "solana-hash 4.4.0", ] @@ -3980,6 +4810,12 @@ dependencies = [ "solana-transaction-context", ] +[[package]] +name = "solana-measure" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b0eef68497ca818f50da3de449c64dc406d28170e395fd2ec40b64b6798afec" + [[package]] name = "solana-message" version = "3.1.0" @@ -4000,6 +4836,22 @@ dependencies = [ "solana-transaction-error", ] +[[package]] +name = "solana-metrics" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba33d34f2297e307bc124c3108da4b5b536a63022d705c0367a8512caebc9c4f" +dependencies = [ + "crossbeam-channel", + "gethostname", + "log", + "reqwest", + "solana-cluster-type", + "solana-sha256-hasher", + "solana-time-utils", + "thiserror 2.0.18", +] + [[package]] name = "solana-msg" version = "3.1.0" @@ -4015,6 +4867,29 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae8dd4c280dca9d046139eb5b7a5ac9ad10403fbd64964c7d7571214950d758f" +[[package]] +name = "solana-net-utils" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2a83f0f11be6d2c0104871136ae22250a5dafd013fe137678312b73a7e35044" +dependencies = [ + "anyhow", + "bincode", + "bytes", + "cfg-if", + "dashmap", + "itertools 0.12.1", + "log", + "nix", + "rand 0.8.6", + "serde", + "socket2", + "solana-serde", + "solana-svm-type-overrides", + "tokio", + "url", +] + [[package]] name = "solana-nonce" version = "3.2.0" @@ -4072,7 +4947,45 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edf2f25743c95229ac0fdc32f8f5893ef738dbf332c669e9861d33ddb0f469d" dependencies = [ + "bincode", "bitflags", + "cfg_eval", + "serde", + "serde_derive", + "serde_with", +] + +[[package]] +name = "solana-perf" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "794506987f9168331a84836eb66e762ecd5fd18920f0c5bcdf349eefff553e59" +dependencies = [ + "ahash", + "bincode", + "bv", + "bytes", + "caps", + "curve25519-dalek", + "dlopen2", + "fnv", + "libc", + "log", + "nix", + "rand 0.8.6", + "rayon", + "serde", + "solana-hash 3.1.0", + "solana-message", + "solana-metrics", + "solana-packet", + "solana-pubkey 3.0.0", + "solana-rayon-threadlimit", + "solana-sdk-ids", + "solana-short-vec", + "solana-signature", + "solana-time-utils", + "solana-transaction-context", ] [[package]] @@ -4262,9 +5175,84 @@ dependencies = [ name = "solana-pubkey" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7db719574990de7e8b0f55a8593ac92a5ccb42c8ce67b3e4bf05b139d5d9ee71" +checksum = "7db719574990de7e8b0f55a8593ac92a5ccb42c8ce67b3e4bf05b139d5d9ee71" +dependencies = [ + "solana-address 2.6.1", +] + +[[package]] +name = "solana-pubsub-client" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3eb58936a5c35140f7eb0fc0a30685a32523001b24c4e3e18e70ca05c8c680b" +dependencies = [ + "crossbeam-channel", + "futures-util", + "http 0.2.12", + "log", + "semver", + "serde", + "serde_json", + "solana-account-decoder-client-types", + "solana-clock", + "solana-pubkey 3.0.0", + "solana-rpc-client-types", + "solana-signature", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tungstenite", + "url", +] + +[[package]] +name = "solana-quic-client" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab47dde0f9b63bfee69fb7f4c7525b87b6685de2e538e8d0ab614625df3d093" +dependencies = [ + "anza-quinn", + "anza-quinn-proto", + "async-lock", + "async-trait", + "futures", + "itertools 0.12.1", + "log", + "rustls", + "solana-connection-cache", + "solana-keypair", + "solana-measure", + "solana-metrics", + "solana-net-utils", + "solana-pubkey 3.0.0", + "solana-quic-definitions", + "solana-rpc-client-api", + "solana-signer", + "solana-streamer", + "solana-tls-utils", + "solana-transaction-error", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "solana-quic-definitions" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15319accf7d3afd845817aeffa6edd8cc185f135cefbc6b985df29cfd8c09609" +dependencies = [ + "solana-keypair", +] + +[[package]] +name = "solana-rayon-threadlimit" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06a84c1c10b97998364e06e1254e2e2364c338ce0cf1bdcfc996ab39e85c618" dependencies = [ - "solana-address 2.6.1", + "log", + "num_cpus", ] [[package]] @@ -4360,6 +5348,23 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "solana-rpc-client-nonce-utils" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4804524b94b49e721c6febc0b4c8f745cd76fc05dfbd972a1e6257e23502b59" +dependencies = [ + "solana-account", + "solana-commitment-config", + "solana-hash 3.1.0", + "solana-message", + "solana-nonce", + "solana-pubkey 3.0.0", + "solana-rpc-client", + "solana-sdk-ids", + "thiserror 2.0.18", +] + [[package]] name = "solana-rpc-client-types" version = "3.1.14" @@ -4400,7 +5405,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15b079e08471a9dbfe1e48b2c7439c85aa2a055cbd54eddd8bd257b0a7dbb29" dependencies = [ "byteorder", - "combine", + "combine 3.8.1", "hash32", "libc", "log", @@ -4644,6 +5649,54 @@ dependencies = [ "solana-sysvar-id", ] +[[package]] +name = "solana-streamer" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1506f6dc927e86fd673bee67fafa27c942527874bac7d4aea63869dcbf22615" +dependencies = [ + "anza-quinn", + "anza-quinn-proto", + "arc-swap", + "bytes", + "crossbeam-channel", + "dashmap", + "futures", + "futures-util", + "governor", + "histogram", + "indexmap", + "itertools 0.12.1", + "libc", + "log", + "nix", + "num_cpus", + "pem", + "percentage", + "rand 0.8.6", + "rustls", + "smallvec", + "socket2", + "solana-keypair", + "solana-measure", + "solana-metrics", + "solana-net-utils", + "solana-packet", + "solana-perf", + "solana-pubkey 3.0.0", + "solana-quic-definitions", + "solana-signature", + "solana-signer", + "solana-time-utils", + "solana-tls-utils", + "solana-transaction-error", + "solana-transaction-metrics-tracker", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "x509-parser", +] + [[package]] name = "solana-svm-callback" version = "3.1.14" @@ -4817,6 +5870,53 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ced92c60aa76ec4780a9d93f3bd64dfa916e1b998eacc6f1c110f3f444f02c9" +[[package]] +name = "solana-tls-utils" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e195a1cd2dbb54fb234abb1fd5c92021152669f0d8853075a52b67bc9e7095" +dependencies = [ + "rustls", + "solana-keypair", + "solana-pubkey 3.0.0", + "solana-signer", + "x509-parser", +] + +[[package]] +name = "solana-tpu-client" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71b3e8d4db00daee1efb357c2e810ce07fd5f0d9167c722434b77c4f9aa668b1" +dependencies = [ + "async-trait", + "bincode", + "futures-util", + "indexmap", + "indicatif", + "log", + "rayon", + "solana-client-traits", + "solana-clock", + "solana-commitment-config", + "solana-connection-cache", + "solana-epoch-schedule", + "solana-measure", + "solana-message", + "solana-net-utils", + "solana-pubkey 3.0.0", + "solana-pubsub-client", + "solana-quic-definitions", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-signature", + "solana-signer", + "solana-transaction", + "solana-transaction-error", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "solana-transaction" version = "3.1.0" @@ -4868,6 +5968,22 @@ dependencies = [ "solana-sanitize", ] +[[package]] +name = "solana-transaction-metrics-tracker" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcfdbabf25f0b1335c4042924011f1be7d93b668d3315b12bbb33781ad388adc" +dependencies = [ + "base64 0.22.1", + "bincode", + "log", + "rand 0.8.6", + "solana-packet", + "solana-perf", + "solana-short-vec", + "solana-signature", +] + [[package]] name = "solana-transaction-status-client-types" version = "3.1.14" @@ -4892,6 +6008,22 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "solana-udp-client" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e01a3aa2c2f0c1cd9e493e59ef9c7f0281cdf56845900927d3e3a021222bbab" +dependencies = [ + "async-trait", + "solana-connection-cache", + "solana-keypair", + "solana-net-utils", + "solana-streamer", + "solana-transaction-error", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "solana-version" version = "3.1.14" @@ -5080,6 +6212,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + [[package]] name = "spki" version = "0.7.3" @@ -5146,6 +6287,16 @@ dependencies = [ "solana-pubkey 3.0.0", ] +[[package]] +name = "spl-memo-interface" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3745d384b0afee980d43d62b66c27bdcbbd03507732b8d3626d3413cb72084f2" +dependencies = [ + "solana-instruction", + "solana-pubkey 4.2.0", +] + [[package]] name = "spl-pod" version = "0.7.3" @@ -5377,6 +6528,18 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -5448,6 +6611,36 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -5475,9 +6668,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -5511,6 +6704,33 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -5520,6 +6740,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "pin-project-lite", "tokio", ] @@ -5580,7 +6801,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", + "http 1.4.2", "http-body", "http-body-util", "pin-project-lite", @@ -5610,6 +6831,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-core", ] @@ -5629,6 +6851,26 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.2", + "httparse", + "log", + "rand 0.9.4", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.18", + "utf-8", + "webpki-roots 0.26.11", +] + [[package]] name = "typenum" version = "1.20.1" @@ -5653,6 +6895,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unit-prefix" version = "0.5.2" @@ -5706,6 +6954,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -5739,6 +6993,16 @@ dependencies = [ "libc", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -5844,6 +7108,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.8", +] + [[package]] name = "webpki-roots" version = "1.0.8" @@ -5869,6 +7151,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -5906,6 +7197,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -5933,6 +7233,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -5966,6 +7281,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5978,6 +7299,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5990,6 +7317,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6014,6 +7347,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -6026,6 +7365,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -6038,6 +7383,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6050,6 +7401,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -6083,6 +7440,24 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "x509-parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8" +dependencies = [ + "asn1-rs", + "base64 0.13.1", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "yoke" version = "0.8.3" @@ -6103,7 +7478,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.118", - "synstructure", + "synstructure 0.13.2", ] [[package]] @@ -6144,7 +7519,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.118", - "synstructure", + "synstructure 0.13.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2e4f19c..c67e09d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ hex-literal = "1" litesvm = "0.12.0" litesvm-token = "0.12.0" num_enum = "0.7" +orca_whirlpools = "8" pinocchio = "0.11.1" pinocchio-system = "0.6" pinocchio-token = "0.6" @@ -31,6 +32,7 @@ settlement-interface = { path = "interface" } solana-account-view = "2" solana-address = "2" solana-address-lookup-table-interface = "3" +solana-client = "3" solana-hash = "3" solana-instruction = "3" solana-instructions-sysvar = "3" @@ -43,6 +45,7 @@ solana-system-interface = "3" spl-associated-token-account-interface = { version = "2" } spl-token = "9" spl-token-interface = "2" +tokio = { version = "1", features = ["rt-multi-thread"] } [workspace.metadata.cli] # Used to help solana-verify identify the correct build image. diff --git a/test-cli/Cargo.toml b/test-cli/Cargo.toml index 230cbf6..e9f5f34 100644 --- a/test-cli/Cargo.toml +++ b/test-cli/Cargo.toml @@ -10,7 +10,9 @@ path = "src/main.rs" [dependencies] anyhow.workspace = true clap.workspace = true +orca_whirlpools.workspace = true settlement-client.workspace = true +solana-client.workspace = true solana-commitment-config.workspace = true solana-hash.workspace = true solana-instruction.workspace = true @@ -22,6 +24,7 @@ solana-system-interface = { workspace = true, features = ["bincode"] } spl-associated-token-account-interface.workspace = true spl-token.workspace = true spl-token-interface.workspace = true +tokio.workspace = true [lints] workspace = true diff --git a/test-cli/src/cmd/settle.rs b/test-cli/src/cmd/settle.rs index 0c27148..5775e1f 100644 --- a/test-cli/src/cmd/settle.rs +++ b/test-cli/src/cmd/settle.rs @@ -17,8 +17,10 @@ use solana_sdk::{ signature::{Signature, Signer}, transaction::Transaction, }; +use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id; use std::collections::{HashMap, HashSet}; +use crate::orca::{self, OrcaClient}; use crate::token::{resolve_from_token_account, ResolvedToken}; use super::Context; @@ -90,9 +92,24 @@ pub fn run(ctx: Context, args: SettleArgs) -> anyhow::Result<()> { let intents = resolve_intents(&ctx, &args)?; let mut all_ixs: Vec = vec![]; - prepare_setup_ixs(&ctx, &intents, &mut all_ixs)?; + let (sell_amount_pulled, buy_amount_pushed) = prepare_setup_ixs(&ctx, &intents, &mut all_ixs)?; + + let (surplus, deficits) = compute_imbalances(&sell_amount_pulled, &buy_amount_pushed); + + // Mints the orders can't fully CoW against each other need an Orca swap, funded by + // the mints left over from the CoW matching, to make up the difference. + let mut swap_ixs: Vec = vec![]; + let mut payer_pulls: HashMap = HashMap::new(); + if !deficits.is_empty() { + let orca = OrcaClient::new(&ctx.rpc.url(), ctx.rpc.commitment())?; + let plan = orca::plan_swaps(&orca, &ctx, &surplus, &deficits)?; + all_ixs.extend(plan.setup_ixs); + swap_ixs = plan.swap_ixs; + payer_pulls = plan.payer_pulls; + } - let pulls = compute_pulls(&ctx, &intents); + let mut sinks = compute_sinks(&ctx, &sell_amount_pulled, &payer_pulls); + let pulls = compute_pulls(&intents, &mut sinks); let initialized_intents: Vec<_> = intents .iter() @@ -103,10 +120,12 @@ pub fn run(ctx: Context, args: SettleArgs) -> anyhow::Result<()> { }) .collect(); - let (begin_ix_index, finalize_ix_index) = u16::try_from(all_ixs.len()) + let begin_ix_index = + u16::try_from(all_ixs.len()).context("too many instructions: begin index overflow")?; + let finalize_ix_index = u16::try_from(swap_ixs.len()) .ok() - .and_then(|begin| Some((begin, begin.checked_add(1)?))) - .context("too many instructions: begin/finalize index overflow")?; + .and_then(|n| begin_ix_index.checked_add(1)?.checked_add(n)) + .context("too many instructions: finalize index overflow")?; let begin_ix = BeginSettle { program_id: ctx.program_id, @@ -132,6 +151,7 @@ pub fn run(ctx: Context, args: SettleArgs) -> anyhow::Result<()> { }; all_ixs.push(begin_ix.into()); + all_ixs.extend(swap_ixs); all_ixs.push(finalize_ix.into()); let blockhash = ctx.rpc.get_latest_blockhash().context("fetch blockhash")?; @@ -261,13 +281,13 @@ fn tally_and_register_buffer( Ok(()) } -/// Create any missing buffer PDAs before the settle tx, and check that the -/// settlement is self-funding (see [`ensure_cow_balance`]). +/// Create any missing buffer PDAs before the settle tx, and tally up the +/// total sell/buy amount per mint across all orders (see [`compute_imbalances`]). fn prepare_setup_ixs( ctx: &Context, intents: &[ResolvedIntent], all_ixs: &mut Vec, -) -> anyhow::Result<()> { +) -> anyhow::Result<(HashMap, HashMap)> { let mut sell_amount_pulled: HashMap = HashMap::new(); let mut buy_amount_pushed: HashMap = HashMap::new(); let mut mint_buffers_to_create: HashSet = HashSet::new(); @@ -291,8 +311,6 @@ fn prepare_setup_ixs( )?; } - ensure_cow_balance(&sell_amount_pulled, &buy_amount_pushed)?; - if !mint_buffers_to_create.is_empty() { all_ixs.push( CreateBuffers { @@ -304,44 +322,129 @@ fn prepare_setup_ixs( ); } - Ok(()) + Ok((sell_amount_pulled, buy_amount_pushed)) } -/// Check that the orders can settle against each other: for every mint, the -/// amount the users sell into its buffer must cover the amount the settlement -/// pushes back out of it. Otherwise there is nothing to CoW the orders with and -/// the settlement would have to be funded from elsewhere. +/// Compare each mint's total sold against its total bought (both tallied across +/// every order) to find: +/// - `surplus`: mints sold for more than they're bought back — the extra can +/// fund an Orca swap into a mint that's short. +/// - `deficits`: mints bought for more than they're sold — CoW matching alone +/// can't cover them, so an Orca swap has to make up the difference. /// /// This deliberately ignores whatever the buffers already hold, so a settlement -/// that would only work by spending pre-existing buffer balances is rejected -/// here rather than silently draining them. -fn ensure_cow_balance( +/// that would only work by spending pre-existing buffer balances still shows up +/// as a deficit here rather than silently draining them. +fn compute_imbalances( sell_amount_pulled: &HashMap, buy_amount_pushed: &HashMap, -) -> anyhow::Result<()> { - for (mint, &pushed) in buy_amount_pushed { - let pulled = sell_amount_pulled.get(mint).copied().unwrap_or_default(); - anyhow::ensure!( - pulled >= pushed, - "orders can't be CoW'd: mint {mint} needs {pushed} pushed out of its buffer \ - but only {pulled} is sold into it", - ); +) -> (HashMap, Vec<(Pubkey, u64)>) { + let surplus = sell_amount_pulled + .iter() + .filter_map(|(&mint, &pulled)| { + let pushed = buy_amount_pushed.get(&mint).copied().unwrap_or(0); + (pulled > pushed).then(|| (mint, pulled.saturating_sub(pushed))) + }) + .collect(); + + let deficits = buy_amount_pushed + .iter() + .filter_map(|(&mint, &pushed)| { + let pulled = sell_amount_pulled.get(&mint).copied().unwrap_or(0); + (pushed > pulled).then(|| (mint, pushed.saturating_sub(pulled))) + }) + .collect(); + + (surplus, deficits) +} + +/// Compute each sold mint's pull destinations. Whatever `payer_pulls` (built by +/// [`orca::plan_swaps`]) reserves from a mint goes to the payer's own wallet to +/// fund the swaps that cover another mint's deficit; the rest goes straight +/// into the mint's buffer, same as a pure CoW settlement. +fn compute_sinks( + ctx: &Context, + sell_amount_pulled: &HashMap, + payer_pulls: &HashMap, +) -> HashMap> { + sell_amount_pulled + .iter() + .map(|(&mint, &sold)| { + let to_payer = payer_pulls.get(&mint).copied().unwrap_or(0).min(sold); + let to_buffer = sold.saturating_sub(to_payer); + + let mut mint_sinks = Vec::with_capacity(2); + if to_buffer > 0 { + let (buffer_pda, _) = find_buffer_pda(&ctx.program_id, &mint); + mint_sinks.push(Pull { + destination: buffer_pda, + amount: to_buffer, + }); + } + if to_payer > 0 { + let payer_ata = get_associated_token_address_with_program_id( + &ctx.payer.pubkey(), + &mint, + &spl_token_interface::id(), + ); + mint_sinks.push(Pull { + destination: payer_ata, + amount: to_payer, + }); + } + + (mint, mint_sinks) + }) + .collect() +} + +/// Carve `amount` out of `mint_sinks` (a single mint's entry from +/// [`compute_sinks`]'s map), depleting it from the back as we go. Splits +/// across multiple `Pull`s when a single sink entry doesn't cover the whole +/// amount (e.g. an order that draws from both the buffer and the payer's +/// swap-funding wallet). +fn carve_pulls(amount: u64, mint_sinks: &mut Vec) -> Vec { + let mut pulls = Vec::with_capacity(1); + let mut remaining = amount; + + while remaining > 0 { + let Some(sink) = mint_sinks.last().cloned() else { + break; + }; + + if sink.amount <= remaining { + mint_sinks.pop(); + remaining = remaining.saturating_sub(sink.amount); + pulls.push(sink); + } else { + mint_sinks + .last_mut() + .expect("checked non-empty above") + .amount = sink.amount.saturating_sub(remaining); + pulls.push(Pull { + destination: sink.destination, + amount: remaining, + }); + remaining = 0; + } } - Ok(()) + pulls } -/// Compute the pull destinations for a settlement: every order's full sell -/// amount goes into the buffer PDA of its sell mint. -fn compute_pulls(ctx: &Context, intents: &[ResolvedIntent]) -> Vec<[Pull; 1]> { +/// Carve each order's required pull amount out of the shared per-mint sink +/// pool computed by [`compute_sinks`], depleting `sinks` as we go. +fn compute_pulls( + intents: &[ResolvedIntent], + sinks: &mut HashMap>, +) -> Vec> { intents .iter() .map(|intent| { - let (buffer_pda, _) = find_buffer_pda(&ctx.program_id, &intent.sell.mint); - [Pull { - destination: buffer_pda, - amount: intent.data.sell_amount, - }] + sinks + .get_mut(&intent.sell.mint) + .map(|mint_sinks| carve_pulls(intent.data.sell_amount, mint_sinks)) + .unwrap_or_default() }) .collect() } @@ -449,31 +552,90 @@ mod tests { } #[test] - fn cow_balance_accepts_covered_and_exact_mints() { - let a = Pubkey::new_unique(); - let b = Pubkey::new_unique(); - // `a` is oversold (the surplus stays in its buffer), `b` matches exactly. - let sold = HashMap::from([(a, 100), (b, 50)]); - let bought = HashMap::from([(a, 90), (b, 50)]); - assert!(ensure_cow_balance(&sold, &bought).is_ok()); + fn imbalances_ignore_mints_that_net_out_exactly() { + let mint = Pubkey::new_unique(); + let sold = HashMap::from([(mint, 50)]); + let bought = HashMap::from([(mint, 50)]); + let (surplus, deficits) = compute_imbalances(&sold, &bought); + assert!(surplus.is_empty()); + assert!(deficits.is_empty()); + } + + #[test] + fn imbalances_report_surplus_and_deficit_mints() { + let oversold = Pubkey::new_unique(); + let overbought = Pubkey::new_unique(); + let sold = HashMap::from([(oversold, 100)]); + let bought = HashMap::from([(oversold, 90), (overbought, 11)]); + + let (surplus, deficits) = compute_imbalances(&sold, &bought); + assert_eq!(surplus, HashMap::from([(oversold, 10)])); + assert_eq!(deficits, vec![(overbought, 11)]); } #[test] - fn cow_balance_rejects_undersold_mint() { + fn sinks_split_a_mint_between_its_buffer_and_the_payer_wallet() { + let ctx = Context { + payer: solana_sdk::signature::Keypair::new(), + program_id: Pubkey::new_unique(), + rpc: RpcClient::new("http://localhost:8899".to_string()), + }; let mint = Pubkey::new_unique(); - let sold = HashMap::from([(mint, 10)]); - let bought = HashMap::from([(mint, 11)]); - let err = ensure_cow_balance(&sold, &bought) - .expect_err("pushing more than is sold can't be CoW'd") - .to_string(); - assert!(err.contains("can't be CoW'd"), "unexpected error: {err}"); + let sold = HashMap::from([(mint, 100)]); + let payer_pulls = HashMap::from([(mint, 30)]); + + let sinks = compute_sinks(&ctx, &sold, &payer_pulls); + let mint_sinks = &sinks[&mint]; + let total: u64 = mint_sinks.iter().map(|p| p.amount).sum(); + assert_eq!(total, 100); + assert!(mint_sinks.iter().any(|p| p.amount == 30)); + assert!(mint_sinks.iter().any(|p| p.amount == 70)); + } + + #[test] + fn carve_pulls_splits_across_sinks_when_one_alone_is_short() { + let buffer = Pubkey::new_unique(); + let payer_ata = Pubkey::new_unique(); + let mut mint_sinks = vec![ + Pull { + destination: buffer, + amount: 70, + }, + Pull { + destination: payer_ata, + amount: 30, + }, + ]; + + let pulls = carve_pulls(100, &mut mint_sinks); + let total: u64 = pulls.iter().map(|p| p.amount).sum(); + assert_eq!(total, 100); + assert!(mint_sinks.is_empty()); } #[test] - fn cow_balance_rejects_mint_that_is_only_bought() { - // A mint nobody sells has nothing in its buffer to push from. - let bought = HashMap::from([(Pubkey::new_unique(), 1)]); - assert!(ensure_cow_balance(&HashMap::new(), &bought).is_err()); + fn carve_pulls_takes_only_what_it_needs_from_the_last_sink() { + let buffer = Pubkey::new_unique(); + let mut mint_sinks = vec![Pull { + destination: buffer, + amount: 70, + }]; + + let pulls = carve_pulls(40, &mut mint_sinks); + assert_eq!( + pulls, + vec![Pull { + destination: buffer, + amount: 40 + }] + ); + assert_eq!( + mint_sinks, + vec![Pull { + destination: buffer, + amount: 30 + }] + ); } #[test] diff --git a/test-cli/src/main.rs b/test-cli/src/main.rs index 87a2a7f..2732d44 100644 --- a/test-cli/src/main.rs +++ b/test-cli/src/main.rs @@ -3,6 +3,7 @@ use settlement_client::settlement_interface::Pubkey; mod cmd; mod instructions; +mod orca; mod token; fn default_keypair_path() -> String { diff --git a/test-cli/src/orca.rs b/test-cli/src/orca.rs new file mode 100644 index 0000000..e4666dd --- /dev/null +++ b/test-cli/src/orca.rs @@ -0,0 +1,316 @@ +//! Orca Whirlpool swap planning: turns per-mint surplus/deficit tallies from a +//! settlement into the swap instructions (and payer-wallet routing) needed to +//! cover the deficits. +//! +//! The Whirlpools SDK is async-only; [`OrcaClient`] bridges into it with a +//! small Tokio runtime scoped to this module so the rest of the CLI can stay +//! synchronous. + +use anyhow::Context as _; +use orca_whirlpools::{ + fetch_splash_pool, fetch_whirlpools_by_token_pair, set_native_mint_wrapping_strategy, + swap_instructions, NativeMintWrappingStrategy, PoolInfo, SwapConfig, SwapQuote, SwapType, + WhirlpoolDeployment, +}; +use settlement_client::settlement_interface::{pda::buffer::find_buffer_pda, Pubkey}; +use solana_client::nonblocking::rpc_client::RpcClient as AsyncRpcClient; +use solana_commitment_config::CommitmentConfig; +use solana_instruction::Instruction; +use solana_sdk::signature::Signer; +use spl_associated_token_account_interface::{ + address::get_associated_token_address_with_program_id, + instruction::create_associated_token_account_idempotent, +}; +use std::collections::{HashMap, HashSet}; + +use crate::cmd::Context; + +/// Bridges this otherwise-synchronous CLI into the (async-only) Orca Whirlpools SDK. +pub struct OrcaClient { + rpc: AsyncRpcClient, + runtime: tokio::runtime::Runtime, +} + +impl OrcaClient { + pub fn new(rpc_url: &str, commitment: CommitmentConfig) -> anyhow::Result { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .context("failed to start the Tokio runtime needed to drive the Orca SDK")?; + + // The solver funds swaps through its own regular ATAs, same as how the rest of + // this CLI treats WSOL, instead of Orca's default ephemeral-keypair wrapping — + // so a swap never needs an extra transaction signer beyond the payer. + set_native_mint_wrapping_strategy(NativeMintWrappingStrategy::Ata).map_err(|e| { + anyhow::anyhow!("failed to configure Orca's SOL-wrapping strategy: {e}") + })?; + + Ok(Self { + rpc: AsyncRpcClient::new_with_commitment(rpc_url.to_string(), commitment), + runtime, + }) + } +} + +/// Orca swap instructions needed to cover every deficit, plus the payer-wallet +/// routing [`crate::cmd::settle`] needs to fold into its pull plan. +pub struct SwapPlan { + /// Idempotent payer-ATA creation for every surplus mint a swap draws from. + /// Must run before `BeginSettle`, since its `Pull` destinations have to + /// already exist on-chain. + pub setup_ixs: Vec, + /// The swaps themselves, plus the transfers moving their output into the + /// deficit mints' buffer PDAs. Must run between `BeginSettle` and + /// `FinalizeSettle`. + pub swap_ixs: Vec, + /// How much of each surplus mint needs to be pulled to the payer's own + /// wallet (instead of its buffer) to fund the swaps above. + pub payer_pulls: HashMap, +} + +/// Find Orca swaps that cover every mint in `deficits` (mints the settlement +/// is short of) by drawing down `surplus` (mints the settlement has extra of), +/// greedily pairing them off. Errors if a deficit can't be fully covered by +/// the available surplus and Orca liquidity. +pub fn plan_swaps( + orca: &OrcaClient, + ctx: &Context, + surplus: &HashMap, + deficits: &[(Pubkey, u64)], +) -> anyhow::Result { + let mut surplus_remaining = surplus.clone(); + let mut setup_ixs = Vec::new(); + let mut swap_ixs = Vec::new(); + let mut payer_pulls: HashMap = HashMap::new(); + let mut payer_atas_created: HashSet = HashSet::new(); + + for &(deficit_mint, deficit_amount) in deficits { + let mut remaining = deficit_amount; + + for (&surplus_mint, avail) in surplus_remaining.iter_mut() { + if remaining == 0 { + break; + } + if *avail == 0 { + continue; + } + + let Some(fill) = orca.runtime.block_on(plan_pair( + orca, + ctx.payer.pubkey(), + surplus_mint, + deficit_mint, + remaining, + *avail, + ))? + else { + // No initialized Orca pool for this pair; try the next surplus mint. + continue; + }; + + if payer_atas_created.insert(surplus_mint) { + setup_ixs.push(create_associated_token_account_idempotent( + &ctx.payer.pubkey(), + &ctx.payer.pubkey(), + &surplus_mint, + &spl_token_interface::id(), + )); + } + + swap_ixs.extend(fill.instructions); + + let (deficit_buffer, _) = find_buffer_pda(&ctx.program_id, &deficit_mint); + let payer_output_ata = get_associated_token_address_with_program_id( + &ctx.payer.pubkey(), + &deficit_mint, + &spl_token_interface::id(), + ); + swap_ixs.push( + spl_token_interface::instruction::transfer( + &spl_token_interface::id(), + &payer_output_ata, + &deficit_buffer, + &ctx.payer.pubkey(), + &[], + fill.output_covered, + ) + .context("failed to build transfer of swap proceeds into the buffer")?, + ); + + payer_pulls + .entry(surplus_mint) + .and_modify(|v| *v = v.saturating_add(fill.input_used)) + .or_insert(fill.input_used); + *avail = avail.saturating_sub(fill.input_used); + remaining = remaining.saturating_sub(fill.output_covered); + } + + anyhow::ensure!( + remaining == 0, + "no Orca liquidity/surplus available to cover the remaining {remaining} of mint \ + {deficit_mint}'s deficit", + ); + } + + Ok(SwapPlan { + setup_ixs, + swap_ixs, + payer_pulls, + }) +} + +/// The result of covering (some or all of) one deficit from one surplus mint. +struct PairFill { + instructions: Vec, + /// How much of `surplus_mint` this swap consumes. + input_used: u64, + /// How much of `deficit_mint` this swap's output covers. + output_covered: u64, +} + +/// Plan a single surplus/deficit pair: try to buy the full remaining deficit +/// with an exact-output swap; if the surplus mint can't cover that swap's +/// worst-case input, fall back to spending all of `surplus_avail` via an +/// exact-input swap instead (a partial fill, leaving the rest of the deficit +/// for another surplus mint). Returns `None` if there's no initialized Orca +/// pool for this pair. +async fn plan_pair( + orca: &OrcaClient, + signer: Pubkey, + surplus_mint: Pubkey, + deficit_mint: Pubkey, + deficit_remaining: u64, + surplus_avail: u64, +) -> anyhow::Result> { + let Some(pool) = find_pool(orca, surplus_mint, deficit_mint).await? else { + return Ok(None); + }; + + // Both of the swap's own token accounts need to survive it intact: the output ATA + // still has to hold the proceeds for the transfer into the deficit buffer that + // follows, and the input ATA shouldn't get swept out from under a later swap that + // also draws on this surplus mint. `NativeMintWrappingStrategy::Ata` (set in + // `OrcaClient::new`) auto-closes a native-mint ATA it had to create for the swap, + // which would silently destroy either of those — see `strip_close_account`. + let protected = [ + get_associated_token_address_with_program_id( + &signer, + &surplus_mint, + &spl_token_interface::id(), + ), + get_associated_token_address_with_program_id( + &signer, + &deficit_mint, + &spl_token_interface::id(), + ), + ]; + + let config = SwapConfig { + slippage_tolerance_bps: None, + signer: Some(signer), + whirlpool_deployment: Some(WhirlpoolDeployment::devnet()), + }; + + let exact_out = swap_instructions( + &orca.rpc, + pool, + deficit_remaining, + deficit_mint, + SwapType::ExactOut, + config.clone(), + ) + .await + .map_err(|e| anyhow::anyhow!("failed to quote {surplus_mint} -> {deficit_mint} swap: {e}"))?; + + let SwapQuote::ExactOut(quote) = exact_out.quote else { + anyhow::bail!("Orca returned the wrong quote type for an exact-output swap"); + }; + anyhow::ensure!( + exact_out.additional_signers.is_empty(), + "swap unexpectedly requires extra signers beyond the payer", + ); + + if quote.token_max_in <= surplus_avail { + return Ok(Some(PairFill { + instructions: strip_close_account(exact_out.instructions, &protected), + input_used: quote.token_max_in, + output_covered: deficit_remaining, + })); + } + + // The full deficit would need more than the surplus mint has on offer; spend all of + // it via an exact-input swap instead, and leave the rest of the deficit uncovered. + let exact_in = swap_instructions( + &orca.rpc, + pool, + surplus_avail, + surplus_mint, + SwapType::ExactIn, + config, + ) + .await + .map_err(|e| anyhow::anyhow!("failed to quote {surplus_mint} -> {deficit_mint} swap: {e}"))?; + + let SwapQuote::ExactIn(quote) = exact_in.quote else { + anyhow::bail!("Orca returned the wrong quote type for an exact-input swap"); + }; + anyhow::ensure!( + exact_in.additional_signers.is_empty(), + "swap unexpectedly requires extra signers beyond the payer", + ); + + Ok(Some(PairFill { + instructions: strip_close_account(exact_in.instructions, &protected), + input_used: surplus_avail, + // `token_min_out` is what the swap instruction is guaranteed to deliver even at + // the edge of its slippage tolerance; using the estimate instead could leave the + // buffer short of what FinalizeSettle expects it to push out. + output_covered: quote.token_min_out, + })) +} + +/// Drop any SPL Token `CloseAccount` instruction targeting one of `protected` accounts. +/// `CloseAccount`'s instruction data is always the single discriminant byte `9` with no +/// extra payload, and its first account is always the account being closed. +fn strip_close_account(instructions: Vec, protected: &[Pubkey]) -> Vec { + instructions + .into_iter() + .filter(|ix| { + let is_close_account = ix.program_id == spl_token_interface::id() && ix.data == [9]; + let targets_protected = ix + .accounts + .first() + .is_some_and(|a| protected.contains(&a.pubkey)); + !(is_close_account && targets_protected) + }) + .collect() +} + +/// Find the Orca Whirlpool with the most liquidity for `mint_a`/`mint_b` on devnet, +/// across every fee tier plus the splash pool. Returns `None` if none are initialized. +async fn find_pool( + orca: &OrcaClient, + mint_a: Pubkey, + mint_b: Pubkey, +) -> anyhow::Result> { + let deployment = WhirlpoolDeployment::devnet(); + + let mut pools = fetch_whirlpools_by_token_pair(&orca.rpc, mint_a, mint_b, Some(deployment)) + .await + .map_err(|e| anyhow::anyhow!("failed to look up Orca pools for {mint_a}/{mint_b}: {e}"))?; + + // The splash pool uses a fee tier outside the ones enumerated above; a failure here + // (e.g. it was never created for this pair) just means one fewer candidate to check. + if let Ok(splash) = fetch_splash_pool(&orca.rpc, mint_a, mint_b, Some(deployment)).await { + pools.push(splash); + } + + Ok(pools + .into_iter() + .filter_map(|pool| match pool { + PoolInfo::Initialized(pool) => Some(pool), + PoolInfo::Uninitialized(_) => None, + }) + .max_by_key(|pool| pool.data.liquidity) + .map(|pool| pool.address)) +} From dd834e07ad5a17052cfe22c98c4987363c4167dd Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 7 Aug 2026 17:02:39 +0900 Subject: [PATCH 2/4] fixes from testing/review --- test-cli/src/cmd/settle.rs | 134 +++++++++------- test-cli/src/liquidity/mod.rs | 20 +++ test-cli/src/{ => liquidity}/orca.rs | 222 ++++++++++++++++----------- test-cli/src/main.rs | 3 +- test-cli/src/network.rs | 7 + test-cli/src/token.rs | 4 +- 6 files changed, 240 insertions(+), 150 deletions(-) create mode 100644 test-cli/src/liquidity/mod.rs rename test-cli/src/{ => liquidity}/orca.rs (55%) create mode 100644 test-cli/src/network.rs diff --git a/test-cli/src/cmd/settle.rs b/test-cli/src/cmd/settle.rs index 5775e1f..ecbe37e 100644 --- a/test-cli/src/cmd/settle.rs +++ b/test-cli/src/cmd/settle.rs @@ -20,7 +20,7 @@ use solana_sdk::{ use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id; use std::collections::{HashMap, HashSet}; -use crate::orca::{self, OrcaClient}; +use crate::liquidity::orca::{self, OrcaClient}; use crate::token::{resolve_from_token_account, ResolvedToken}; use super::Context; @@ -94,22 +94,25 @@ pub fn run(ctx: Context, args: SettleArgs) -> anyhow::Result<()> { let mut all_ixs: Vec = vec![]; let (sell_amount_pulled, buy_amount_pushed) = prepare_setup_ixs(&ctx, &intents, &mut all_ixs)?; - let (surplus, deficits) = compute_imbalances(&sell_amount_pulled, &buy_amount_pushed); + let surplus = union_with_positive_remainder(&sell_amount_pulled, &buy_amount_pushed); + let deficits = union_with_positive_remainder(&buy_amount_pushed, &sell_amount_pulled); // Mints the orders can't fully CoW against each other need an Orca swap, funded by // the mints left over from the CoW matching, to make up the difference. let mut swap_ixs: Vec = vec![]; - let mut payer_pulls: HashMap = HashMap::new(); + let mut teardown_ixs: Vec = vec![]; + let mut swap_sinks: HashMap = HashMap::new(); if !deficits.is_empty() { let orca = OrcaClient::new(&ctx.rpc.url(), ctx.rpc.commitment())?; let plan = orca::plan_swaps(&orca, &ctx, &surplus, &deficits)?; all_ixs.extend(plan.setup_ixs); swap_ixs = plan.swap_ixs; - payer_pulls = plan.payer_pulls; + teardown_ixs = plan.teardown_ixs; + swap_sinks = plan.sinks; } - let mut sinks = compute_sinks(&ctx, &sell_amount_pulled, &payer_pulls); - let pulls = compute_pulls(&intents, &mut sinks); + let mut sinks = compute_sinks(&ctx, &sell_amount_pulled, &swap_sinks)?; + let pulls = compute_pulls(&intents, &mut sinks)?; let initialized_intents: Vec<_> = intents .iter() @@ -153,6 +156,7 @@ pub fn run(ctx: Context, args: SettleArgs) -> anyhow::Result<()> { all_ixs.push(begin_ix.into()); all_ixs.extend(swap_ixs); all_ixs.push(finalize_ix.into()); + all_ixs.extend(teardown_ixs); let blockhash = ctx.rpc.get_latest_blockhash().context("fetch blockhash")?; let tx = Transaction::new_signed_with_payer( @@ -282,7 +286,8 @@ fn tally_and_register_buffer( } /// Create any missing buffer PDAs before the settle tx, and tally up the -/// total sell/buy amount per mint across all orders (see [`compute_imbalances`]). +/// total sell/buy amount per mint across all orders, which +/// [`union_with_positive_remainder`] then nets against each other. fn prepare_setup_ixs( ctx: &Context, intents: &[ResolvedIntent], @@ -325,37 +330,23 @@ fn prepare_setup_ixs( Ok((sell_amount_pulled, buy_amount_pushed)) } -/// Compare each mint's total sold against its total bought (both tallied across -/// every order) to find: -/// - `surplus`: mints sold for more than they're bought back — the extra can -/// fund an Orca swap into a mint that's short. -/// - `deficits`: mints bought for more than they're sold — CoW matching alone -/// can't cover them, so an Orca swap has to make up the difference. -/// -/// This deliberately ignores whatever the buffers already hold, so a settlement -/// that would only work by spending pre-existing buffer balances still shows up -/// as a deficit here rather than silently draining them. -fn compute_imbalances( - sell_amount_pulled: &HashMap, - buy_amount_pushed: &HashMap, -) -> (HashMap, Vec<(Pubkey, u64)>) { - let surplus = sell_amount_pulled +/// Computes a new set that subtracts key-matching elements of b_set from a_set +fn union_with_positive_remainder( + a_set: &HashMap, + b_set: &HashMap, +) -> HashMap { + a_set .iter() .filter_map(|(&mint, &pulled)| { - let pushed = buy_amount_pushed.get(&mint).copied().unwrap_or(0); - (pulled > pushed).then(|| (mint, pulled.saturating_sub(pushed))) - }) - .collect(); - - let deficits = buy_amount_pushed - .iter() - .filter_map(|(&mint, &pushed)| { - let pulled = sell_amount_pulled.get(&mint).copied().unwrap_or(0); - (pushed > pulled).then(|| (mint, pushed.saturating_sub(pulled))) + let pushed = b_set.get(&mint).copied().unwrap_or(0); + (pulled > pushed).then(|| { + let extra = pulled + .checked_sub(pushed) + .expect("gated by prior condition"); + (mint, extra) + }) }) - .collect(); - - (surplus, deficits) + .collect() } /// Compute each sold mint's pull destinations. Whatever `payer_pulls` (built by @@ -365,13 +356,15 @@ fn compute_imbalances( fn compute_sinks( ctx: &Context, sell_amount_pulled: &HashMap, - payer_pulls: &HashMap, -) -> HashMap> { + swap_sinks: &HashMap, +) -> anyhow::Result>> { sell_amount_pulled .iter() .map(|(&mint, &sold)| { - let to_payer = payer_pulls.get(&mint).copied().unwrap_or(0).min(sold); - let to_buffer = sold.saturating_sub(to_payer); + let to_payer = swap_sinks.get(&mint).copied().unwrap_or(0).min(sold); + let to_buffer = sold.checked_sub(to_payer).with_context(|| { + format!("swaps reserved more of mint {mint} than the orders sell") + })?; let mut mint_sinks = Vec::with_capacity(2); if to_buffer > 0 { @@ -393,7 +386,7 @@ fn compute_sinks( }); } - (mint, mint_sinks) + Ok((mint, mint_sinks)) }) .collect() } @@ -403,7 +396,7 @@ fn compute_sinks( /// across multiple `Pull`s when a single sink entry doesn't cover the whole /// amount (e.g. an order that draws from both the buffer and the payer's /// swap-funding wallet). -fn carve_pulls(amount: u64, mint_sinks: &mut Vec) -> Vec { +fn carve_pulls(amount: u64, mint_sinks: &mut Vec) -> anyhow::Result> { let mut pulls = Vec::with_capacity(1); let mut remaining = amount; @@ -412,15 +405,22 @@ fn carve_pulls(amount: u64, mint_sinks: &mut Vec) -> Vec { break; }; + // Both subtractions run in the direction this branch just established, so an + // underflow would mean the comparison and the arithmetic disagree. if sink.amount <= remaining { mint_sinks.pop(); - remaining = remaining.saturating_sub(sink.amount); + remaining = remaining + .checked_sub(sink.amount) + .context("sink is larger than the amount it was chosen to fit inside")?; pulls.push(sink); } else { mint_sinks .last_mut() .expect("checked non-empty above") - .amount = sink.amount.saturating_sub(remaining); + .amount = sink + .amount + .checked_sub(remaining) + .context("sink is smaller than the amount carved out of it")?; pulls.push(Pull { destination: sink.destination, amount: remaining, @@ -429,7 +429,7 @@ fn carve_pulls(amount: u64, mint_sinks: &mut Vec) -> Vec { } } - pulls + Ok(pulls) } /// Carve each order's required pull amount out of the shared per-mint sink @@ -437,14 +437,14 @@ fn carve_pulls(amount: u64, mint_sinks: &mut Vec) -> Vec { fn compute_pulls( intents: &[ResolvedIntent], sinks: &mut HashMap>, -) -> Vec> { +) -> anyhow::Result>> { intents .iter() .map(|intent| { sinks .get_mut(&intent.sell.mint) .map(|mint_sinks| carve_pulls(intent.data.sell_amount, mint_sinks)) - .unwrap_or_default() + .unwrap_or_else(|| Ok(Vec::new())) }) .collect() } @@ -552,25 +552,45 @@ mod tests { } #[test] - fn imbalances_ignore_mints_that_net_out_exactly() { + fn remainder_drops_mints_that_net_out_exactly() { let mint = Pubkey::new_unique(); let sold = HashMap::from([(mint, 50)]); let bought = HashMap::from([(mint, 50)]); - let (surplus, deficits) = compute_imbalances(&sold, &bought); - assert!(surplus.is_empty()); - assert!(deficits.is_empty()); + assert!(union_with_positive_remainder(&sold, &bought).is_empty()); + assert!(union_with_positive_remainder(&bought, &sold).is_empty()); } #[test] - fn imbalances_report_surplus_and_deficit_mints() { + fn remainder_keeps_only_what_the_other_side_does_not_cover() { let oversold = Pubkey::new_unique(); let overbought = Pubkey::new_unique(); let sold = HashMap::from([(oversold, 100)]); let bought = HashMap::from([(oversold, 90), (overbought, 11)]); - let (surplus, deficits) = compute_imbalances(&sold, &bought); - assert_eq!(surplus, HashMap::from([(oversold, 10)])); - assert_eq!(deficits, vec![(overbought, 11)]); + // Called the way `run` does: surplus first, then deficits. + assert_eq!( + union_with_positive_remainder(&sold, &bought), + HashMap::from([(oversold, 10)]), + ); + assert_eq!( + union_with_positive_remainder(&bought, &sold), + HashMap::from([(overbought, 11)]), + ); + } + + #[test] + fn remainder_ignores_keys_missing_from_the_left_side() { + let only_bought = Pubkey::new_unique(); + let sold = HashMap::new(); + let bought = HashMap::from([(only_bought, 7)]); + + // A mint the left side never mentions contributes nothing, however large the + // right side's entry is. + assert!(union_with_positive_remainder(&sold, &bought).is_empty()); + assert_eq!( + union_with_positive_remainder(&bought, &sold), + HashMap::from([(only_bought, 7)]), + ); } #[test] @@ -584,7 +604,7 @@ mod tests { let sold = HashMap::from([(mint, 100)]); let payer_pulls = HashMap::from([(mint, 30)]); - let sinks = compute_sinks(&ctx, &sold, &payer_pulls); + let sinks = compute_sinks(&ctx, &sold, &payer_pulls).expect("shouldn't overflow"); let mint_sinks = &sinks[&mint]; let total: u64 = mint_sinks.iter().map(|p| p.amount).sum(); assert_eq!(total, 100); @@ -607,7 +627,7 @@ mod tests { }, ]; - let pulls = carve_pulls(100, &mut mint_sinks); + let pulls = carve_pulls(100, &mut mint_sinks).expect("overflow shouldn't happen"); let total: u64 = pulls.iter().map(|p| p.amount).sum(); assert_eq!(total, 100); assert!(mint_sinks.is_empty()); @@ -621,7 +641,7 @@ mod tests { amount: 70, }]; - let pulls = carve_pulls(40, &mut mint_sinks); + let pulls = carve_pulls(40, &mut mint_sinks).expect("overflow shouldn't happen"); assert_eq!( pulls, vec![Pull { diff --git a/test-cli/src/liquidity/mod.rs b/test-cli/src/liquidity/mod.rs new file mode 100644 index 0000000..b74a58b --- /dev/null +++ b/test-cli/src/liquidity/mod.rs @@ -0,0 +1,20 @@ +use std::collections::HashMap; + +use solana_instruction::Instruction; +use solana_pubkey::Pubkey; + +pub mod orca; + +/// Defines the instructions needed to cover every deficit, and the amount of sink funds needed to do so +pub struct SwapPlan { + /// Txns to execute before the swap, such as + pub setup_ixs: Vec, + /// The swap transactions which fund the deficits to the buffer + pub swap_ixs: Vec, + /// Any transactions that should be run following the swap + pub teardown_ixs: Vec, + + /// How much of each surplus mint needs to be pulled to the specified token account + /// to fund the swaps + pub sinks: HashMap, +} diff --git a/test-cli/src/orca.rs b/test-cli/src/liquidity/orca.rs similarity index 55% rename from test-cli/src/orca.rs rename to test-cli/src/liquidity/orca.rs index e4666dd..714d7d9 100644 --- a/test-cli/src/orca.rs +++ b/test-cli/src/liquidity/orca.rs @@ -2,7 +2,7 @@ //! settlement into the swap instructions (and payer-wallet routing) needed to //! cover the deficits. //! -//! The Whirlpools SDK is async-only; [`OrcaClient`] bridges into it with a +//! The Whirlpools SDK is async-only, so [`OrcaClient`] bridges into it with a //! small Tokio runtime scoped to this module so the rest of the CLI can stay //! synchronous. @@ -19,16 +19,19 @@ use solana_instruction::Instruction; use solana_sdk::signature::Signer; use spl_associated_token_account_interface::{ address::get_associated_token_address_with_program_id, - instruction::create_associated_token_account_idempotent, + instruction::create_associated_token_account_idempotent, program::ID as ATA_PROGRAM_ID, }; use std::collections::{HashMap, HashSet}; +use super::SwapPlan; use crate::cmd::Context; +use crate::network::{DEVNET_GENESIS_HASH, MAINNET_GENESIS_HASH}; /// Bridges this otherwise-synchronous CLI into the (async-only) Orca Whirlpools SDK. pub struct OrcaClient { rpc: AsyncRpcClient, runtime: tokio::runtime::Runtime, + deployment: WhirlpoolDeployment, } impl OrcaClient { @@ -38,34 +41,49 @@ impl OrcaClient { .build() .context("failed to start the Tokio runtime needed to drive the Orca SDK")?; - // The solver funds swaps through its own regular ATAs, same as how the rest of - // this CLI treats WSOL, instead of Orca's default ephemeral-keypair wrapping — - // so a swap never needs an extra transaction signer beyond the payer. - set_native_mint_wrapping_strategy(NativeMintWrappingStrategy::Ata).map_err(|e| { + // The SDK's default wraps SOL through an ephemeral keypair account, which would + // make every swap need a second transaction signer, and its `Ata` strategy closes + // any native-mint ATA it had to create. `None` does neither, so WSOL goes through + // the solver's own ATA and survives the swap like any other mint — which is what + // lets the planning below assume the SDK emits nothing but ATA creations and the + // swap itself. + set_native_mint_wrapping_strategy(NativeMintWrappingStrategy::None).map_err(|e| { anyhow::anyhow!("failed to configure Orca's SOL-wrapping strategy: {e}") })?; + let rpc = AsyncRpcClient::new_with_commitment(rpc_url.to_string(), commitment); + let deployment = runtime.block_on(resolve_deployment(&rpc))?; + Ok(Self { - rpc: AsyncRpcClient::new_with_commitment(rpc_url.to_string(), commitment), + rpc, runtime, + deployment, }) } } -/// Orca swap instructions needed to cover every deficit, plus the payer-wallet -/// routing [`crate::cmd::settle`] needs to fold into its pull plan. -pub struct SwapPlan { - /// Idempotent payer-ATA creation for every surplus mint a swap draws from. - /// Must run before `BeginSettle`, since its `Pull` destinations have to - /// already exist on-chain. - pub setup_ixs: Vec, - /// The swaps themselves, plus the transfers moving their output into the - /// deficit mints' buffer PDAs. Must run between `BeginSettle` and - /// `FinalizeSettle`. - pub swap_ixs: Vec, - /// How much of each surplus mint needs to be pulled to the payer's own - /// wallet (instead of its buffer) to fund the swaps above. - pub payer_pulls: HashMap, +/// The Orca deployment on the cluster `--rpc-url` points at, which every pool lookup, +/// quote and swap instruction is then aimed at. +/// +/// Mainnet and devnet share a program and differ only in the `WhirlpoolsConfig` account +/// their pools hang off, so aiming at the wrong one doesn't fail loudly — it quietly +/// finds no liquidity. Deriving the deployment from the cluster's genesis hash, the same +/// way [`crate::token`] keys its mint registry, keeps the two in step on their own. +async fn resolve_deployment(rpc: &AsyncRpcClient) -> anyhow::Result { + let genesis_hash = rpc + .get_genesis_hash() + .await + .context("failed to fetch genesis hash (is the RPC URL correct?)")? + .to_string(); + + match genesis_hash.as_str() { + DEVNET_GENESIS_HASH => Ok(WhirlpoolDeployment::devnet()), + MAINNET_GENESIS_HASH => Ok(WhirlpoolDeployment::mainnet()), + other => anyhow::bail!( + "no Orca Whirlpools deployment is known for the network with genesis hash {other}. please connect to \ + devnet and mainnet only", + ), + } } /// Find Orca swaps that cover every mint in `deficits` (mints the settlement @@ -76,16 +94,17 @@ pub fn plan_swaps( orca: &OrcaClient, ctx: &Context, surplus: &HashMap, - deficits: &[(Pubkey, u64)], + deficits: &HashMap, ) -> anyhow::Result { let mut surplus_remaining = surplus.clone(); let mut setup_ixs = Vec::new(); let mut swap_ixs = Vec::new(); - let mut payer_pulls: HashMap = HashMap::new(); + let mut sinks: HashMap = HashMap::new(); let mut payer_atas_created: HashSet = HashSet::new(); - for &(deficit_mint, deficit_amount) in deficits { - let mut remaining = deficit_amount; + for (deficit_mint, deficit_amount) in deficits { + let mut remaining = *deficit_amount; + let (deficit_buffer, _) = find_buffer_pda(&ctx.program_id, deficit_mint); for (&surplus_mint, avail) in surplus_remaining.iter_mut() { if remaining == 0 { @@ -99,7 +118,8 @@ pub fn plan_swaps( orca, ctx.payer.pubkey(), surplus_mint, - deficit_mint, + *deficit_mint, + deficit_buffer, remaining, *avail, ))? @@ -119,29 +139,18 @@ pub fn plan_swaps( swap_ixs.extend(fill.instructions); - let (deficit_buffer, _) = find_buffer_pda(&ctx.program_id, &deficit_mint); - let payer_output_ata = get_associated_token_address_with_program_id( - &ctx.payer.pubkey(), - &deficit_mint, - &spl_token_interface::id(), - ); - swap_ixs.push( - spl_token_interface::instruction::transfer( - &spl_token_interface::id(), - &payer_output_ata, - &deficit_buffer, - &ctx.payer.pubkey(), - &[], - fill.output_covered, - ) - .context("failed to build transfer of swap proceeds into the buffer")?, - ); - - payer_pulls - .entry(surplus_mint) - .and_modify(|v| *v = v.saturating_add(fill.input_used)) - .or_insert(fill.input_used); - *avail = avail.saturating_sub(fill.input_used); + let sink = sinks.entry(surplus_mint).or_insert(0); + *sink = sink + .checked_add(fill.input_used) + .with_context(|| format!("swap input tally overflow for mint {surplus_mint}"))?; + + // `plan_pair` never spends more than the `*avail` it was handed, so an + // underflow here would mean it ignored the budget. + *avail = avail.checked_sub(fill.input_used).with_context(|| { + format!("swap consumed more of mint {surplus_mint} than was available") + })?; + + // Unlike the input side, the output can legitimately overshoot, so saturating is fine here remaining = remaining.saturating_sub(fill.output_covered); } @@ -155,7 +164,8 @@ pub fn plan_swaps( Ok(SwapPlan { setup_ixs, swap_ixs, - payer_pulls, + teardown_ixs: vec![], + sinks, }) } @@ -164,7 +174,9 @@ struct PairFill { instructions: Vec, /// How much of `surplus_mint` this swap consumes. input_used: u64, - /// How much of `deficit_mint` this swap's output covers. + /// How much of `deficit_mint` this swap's output covers. The swap pays into + /// the deficit mint's buffer directly, so this is a lower bound on what the + /// buffer actually receives, not an amount transferred separately. output_covered: u64, } @@ -172,13 +184,15 @@ struct PairFill { /// with an exact-output swap; if the surplus mint can't cover that swap's /// worst-case input, fall back to spending all of `surplus_avail` via an /// exact-input swap instead (a partial fill, leaving the rest of the deficit -/// for another surplus mint). Returns `None` if there's no initialized Orca -/// pool for this pair. +/// for another surplus mint). The proceeds are paid into `deficit_buffer` +/// rather than the signer's wallet. Returns `None` if there's no initialized +/// Orca pool for this pair. async fn plan_pair( orca: &OrcaClient, signer: Pubkey, surplus_mint: Pubkey, deficit_mint: Pubkey, + deficit_buffer: Pubkey, deficit_remaining: u64, surplus_avail: u64, ) -> anyhow::Result> { @@ -186,29 +200,25 @@ async fn plan_pair( return Ok(None); }; - // Both of the swap's own token accounts need to survive it intact: the output ATA - // still has to hold the proceeds for the transfer into the deficit buffer that - // follows, and the input ATA shouldn't get swept out from under a later swap that - // also draws on this surplus mint. `NativeMintWrappingStrategy::Ata` (set in - // `OrcaClient::new`) auto-closes a native-mint ATA it had to create for the swap, - // which would silently destroy either of those — see `strip_close_account`. - let protected = [ - get_associated_token_address_with_program_id( - &signer, - &surplus_mint, - &spl_token_interface::id(), - ), - get_associated_token_address_with_program_id( - &signer, - &deficit_mint, - &spl_token_interface::id(), - ), - ]; + let output_ata = get_associated_token_address_with_program_id( + &signer, + &deficit_mint, + &spl_token_interface::id(), + ); + + let swap_instruction_rewrites = |instructions| { + rewrite_swap_output_to_buffer( + instructions, + orca.deployment.id(), + output_ata, + deficit_buffer, + ) + }; let config = SwapConfig { slippage_tolerance_bps: None, signer: Some(signer), - whirlpool_deployment: Some(WhirlpoolDeployment::devnet()), + whirlpool_deployment: Some(orca.deployment), }; let exact_out = swap_instructions( @@ -232,7 +242,7 @@ async fn plan_pair( if quote.token_max_in <= surplus_avail { return Ok(Some(PairFill { - instructions: strip_close_account(exact_out.instructions, &protected), + instructions: swap_instruction_rewrites(exact_out.instructions)?, input_used: quote.token_max_in, output_covered: deficit_remaining, })); @@ -260,7 +270,7 @@ async fn plan_pair( ); Ok(Some(PairFill { - instructions: strip_close_account(exact_in.instructions, &protected), + instructions: swap_instruction_rewrites(exact_in.instructions)?, input_used: surplus_avail, // `token_min_out` is what the swap instruction is guaranteed to deliver even at // the edge of its slippage tolerance; using the estimate instead could leave the @@ -269,31 +279,63 @@ async fn plan_pair( })) } -/// Drop any SPL Token `CloseAccount` instruction targeting one of `protected` accounts. -/// `CloseAccount`'s instruction data is always the single discriminant byte `9` with no -/// extra payload, and its first account is always the account being closed. -fn strip_close_account(instructions: Vec, protected: &[Pubkey]) -> Vec { - instructions +/// Pay the swap's proceeds into `buffer` instead of the signer's own `output_ata`. +/// It also removes any instructions that create the `output_ata`. +/// +/// This function is needed since orca's own library doesn't provide a way to specify swap accounts +/// outside the ATA (and using the lower level libs would be way too much more work) +fn rewrite_swap_output_to_buffer( + instructions: Vec, + whirlpool_program: Pubkey, + output_ata: Pubkey, + buffer: Pubkey, +) -> anyhow::Result> { + let mut redirected = false; + + let routed = instructions .into_iter() - .filter(|ix| { - let is_close_account = ix.program_id == spl_token_interface::id() && ix.data == [9]; - let targets_protected = ix - .accounts - .first() - .is_some_and(|a| protected.contains(&a.pubkey)); - !(is_close_account && targets_protected) + .filter_map(|mut ix| { + if ix.program_id == whirlpool_program { + for account in &mut ix.accounts { + if account.pubkey == output_ata { + account.pubkey = buffer; + redirected = true; + } + } + return Some(ix); + } + + // Nothing references the output ATA any more, so drop the idempotent creation + // the SDK prepends for it: an account the swap no longer touches isn't worth + // the instruction or the rent. + (!creates_token_account(&ix, output_ata)).then_some(ix) }) - .collect() + .collect(); + + anyhow::ensure!( + redirected, + "no swap instruction references the payer's output ATA {output_ata}; Orca's account \ + layout may have changed", + ); + + Ok(routed) +} + +/// Whether `ix` is an Associated Token Account program instruction creating `account`, +/// which every one of its variants passes as its second account. +fn creates_token_account(ix: &Instruction, account: Pubkey) -> bool { + ix.program_id == ATA_PROGRAM_ID && ix.accounts.get(1).is_some_and(|a| a.pubkey == account) } -/// Find the Orca Whirlpool with the most liquidity for `mint_a`/`mint_b` on devnet, -/// across every fee tier plus the splash pool. Returns `None` if none are initialized. +/// Find the Orca Whirlpool with the most liquidity for `mint_a`/`mint_b` on the +/// cluster's deployment, across every fee tier plus the splash pool. Returns `None` +/// if none are initialized. async fn find_pool( orca: &OrcaClient, mint_a: Pubkey, mint_b: Pubkey, ) -> anyhow::Result> { - let deployment = WhirlpoolDeployment::devnet(); + let deployment = orca.deployment; let mut pools = fetch_whirlpools_by_token_pair(&orca.rpc, mint_a, mint_b, Some(deployment)) .await diff --git a/test-cli/src/main.rs b/test-cli/src/main.rs index 2732d44..2353609 100644 --- a/test-cli/src/main.rs +++ b/test-cli/src/main.rs @@ -3,7 +3,8 @@ use settlement_client::settlement_interface::Pubkey; mod cmd; mod instructions; -mod orca; +mod liquidity; +mod network; mod token; fn default_keypair_path() -> String { diff --git a/test-cli/src/network.rs b/test-cli/src/network.rs new file mode 100644 index 0000000..0f1dd11 --- /dev/null +++ b/test-cli/src/network.rs @@ -0,0 +1,7 @@ +//! Identifies which Solana cluster an RPC endpoint serves, by genesis hash. + +/// Genesis hash of Solana Devnet. +pub const DEVNET_GENESIS_HASH: &str = "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG"; + +/// Genesis hash of Solana Mainnet Beta. +pub const MAINNET_GENESIS_HASH: &str = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d"; diff --git a/test-cli/src/token.rs b/test-cli/src/token.rs index a353af5..bc045bb 100644 --- a/test-cli/src/token.rs +++ b/test-cli/src/token.rs @@ -17,6 +17,8 @@ use spl_associated_token_account_interface::instruction::create_associated_token use spl_token_interface::native_mint; use spl_token_interface::state::{Account as TokenAccount, Mint}; +use crate::network::DEVNET_GENESIS_HASH; + /// Inline registry of recognised token symbols. /// Avoids an RPC round-trip for well-known mints whose decimals are fixed. /// Replace with a proper on-chain registry or quote-API lookup when available. @@ -24,8 +26,6 @@ struct KnownToken { mint: Pubkey, } -const DEVNET_GENESIS_HASH: &str = "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG"; - // Temporary registry mapping solana networks (isolated by "genesis" hash) and token symbols to mint addresess. Intended to be replaced in the // future with something more robust. static REGISTRY: &[(&str, &str, KnownToken)] = &[( From d2439460ffb5c3831e8f8f97f6070ca3ddba7e11 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 27 Aug 2026 18:05:45 +0900 Subject: [PATCH 3/4] manual PR review --- test-cli/src/cmd/settle.rs | 20 +++++----- test-cli/src/liquidity/mod.rs | 6 +-- test-cli/src/liquidity/orca.rs | 73 ++++++++++++++++------------------ 3 files changed, 47 insertions(+), 52 deletions(-) diff --git a/test-cli/src/cmd/settle.rs b/test-cli/src/cmd/settle.rs index ecbe37e..3d2e71e 100644 --- a/test-cli/src/cmd/settle.rs +++ b/test-cli/src/cmd/settle.rs @@ -94,14 +94,15 @@ pub fn run(ctx: Context, args: SettleArgs) -> anyhow::Result<()> { let mut all_ixs: Vec = vec![]; let (sell_amount_pulled, buy_amount_pushed) = prepare_setup_ixs(&ctx, &intents, &mut all_ixs)?; + // figure out the net amount we have to buy (deficits) and the net amount we have to buy it with (surplus) let surplus = union_with_positive_remainder(&sell_amount_pulled, &buy_amount_pushed); let deficits = union_with_positive_remainder(&buy_amount_pushed, &sell_amount_pulled); - // Mints the orders can't fully CoW against each other need an Orca swap, funded by - // the mints left over from the CoW matching, to make up the difference. let mut swap_ixs: Vec = vec![]; let mut teardown_ixs: Vec = vec![]; let mut swap_sinks: HashMap = HashMap::new(); + + // If we don't have enough of any token just from the order inputs, we are going to need to swap for it on a DEX if !deficits.is_empty() { let orca = OrcaClient::new(&ctx.rpc.url(), ctx.rpc.commitment())?; let plan = orca::plan_swaps(&orca, &ctx, &surplus, &deficits)?; @@ -111,6 +112,8 @@ pub fn run(ctx: Context, args: SettleArgs) -> anyhow::Result<()> { swap_sinks = plan.sinks; } + // At this point, we know what DEX swaps we need to do. Figure out the routing + // of funds to make that happen. let mut sinks = compute_sinks(&ctx, &sell_amount_pulled, &swap_sinks)?; let pulls = compute_pulls(&intents, &mut sinks)?; @@ -286,8 +289,7 @@ fn tally_and_register_buffer( } /// Create any missing buffer PDAs before the settle tx, and tally up the -/// total sell/buy amount per mint across all orders, which -/// [`union_with_positive_remainder`] then nets against each other. +/// total sell/buy amount per mint across all orders. fn prepare_setup_ixs( ctx: &Context, intents: &[ResolvedIntent], @@ -331,6 +333,7 @@ fn prepare_setup_ixs( } /// Computes a new set that subtracts key-matching elements of b_set from a_set +/// Useful for getting net per-token settlement amounts. fn union_with_positive_remainder( a_set: &HashMap, b_set: &HashMap, @@ -349,10 +352,7 @@ fn union_with_positive_remainder( .collect() } -/// Compute each sold mint's pull destinations. Whatever `payer_pulls` (built by -/// [`orca::plan_swaps`]) reserves from a mint goes to the payer's own wallet to -/// fund the swaps that cover another mint's deficit; the rest goes straight -/// into the mint's buffer, same as a pure CoW settlement. +/// Compute each sold mint's pull destinations. fn compute_sinks( ctx: &Context, sell_amount_pulled: &HashMap, @@ -432,8 +432,8 @@ fn carve_pulls(amount: u64, mint_sinks: &mut Vec) -> anyhow::Result>, diff --git a/test-cli/src/liquidity/mod.rs b/test-cli/src/liquidity/mod.rs index b74a58b..fe3276b 100644 --- a/test-cli/src/liquidity/mod.rs +++ b/test-cli/src/liquidity/mod.rs @@ -7,14 +7,14 @@ pub mod orca; /// Defines the instructions needed to cover every deficit, and the amount of sink funds needed to do so pub struct SwapPlan { - /// Txns to execute before the swap, such as + /// Txns to execute before pulling user funds pub setup_ixs: Vec, /// The swap transactions which fund the deficits to the buffer pub swap_ixs: Vec, - /// Any transactions that should be run following the swap + /// Any transactions that should be run following the pushing of user funds pub teardown_ixs: Vec, - /// How much of each surplus mint needs to be pulled to the specified token account + /// How much of each input token needs to be pulled to the keyed token account /// to fund the swaps pub sinks: HashMap, } diff --git a/test-cli/src/liquidity/orca.rs b/test-cli/src/liquidity/orca.rs index 714d7d9..a33a81f 100644 --- a/test-cli/src/liquidity/orca.rs +++ b/test-cli/src/liquidity/orca.rs @@ -1,6 +1,4 @@ -//! Orca Whirlpool swap planning: turns per-mint surplus/deficit tallies from a -//! settlement into the swap instructions (and payer-wallet routing) needed to -//! cover the deficits. +//! Orca Whirlpool swap planning //! //! The Whirlpools SDK is async-only, so [`OrcaClient`] bridges into it with a //! small Tokio runtime scoped to this module so the rest of the CLI can stay @@ -41,12 +39,9 @@ impl OrcaClient { .build() .context("failed to start the Tokio runtime needed to drive the Orca SDK")?; - // The SDK's default wraps SOL through an ephemeral keypair account, which would - // make every swap need a second transaction signer, and its `Ata` strategy closes - // any native-mint ATA it had to create. `None` does neither, so WSOL goes through - // the solver's own ATA and survives the swap like any other mint — which is what - // lets the planning below assume the SDK emits nothing but ATA creations and the - // swap itself. + // Compatibility hack: The SDK's default wraps SOL through an ephemeral keypair account. + // But we are sophisticated and want do our own wrapping. To prevent this from happening, + // set `NativeMintWrappingStrategy::None`. set_native_mint_wrapping_strategy(NativeMintWrappingStrategy::None).map_err(|e| { anyhow::anyhow!("failed to configure Orca's SOL-wrapping strategy: {e}") })?; @@ -62,13 +57,7 @@ impl OrcaClient { } } -/// The Orca deployment on the cluster `--rpc-url` points at, which every pool lookup, -/// quote and swap instruction is then aimed at. -/// -/// Mainnet and devnet share a program and differ only in the `WhirlpoolsConfig` account -/// their pools hang off, so aiming at the wrong one doesn't fail loudly — it quietly -/// finds no liquidity. Deriving the deployment from the cluster's genesis hash, the same -/// way [`crate::token`] keys its mint registry, keeps the two in step on their own. +/// Resolve the Orca deployment configuration corresponding to the configured RPC URL async fn resolve_deployment(rpc: &AsyncRpcClient) -> anyhow::Result { let genesis_hash = rpc .get_genesis_hash() @@ -80,8 +69,7 @@ async fn resolve_deployment(rpc: &AsyncRpcClient) -> anyhow::Result Ok(WhirlpoolDeployment::devnet()), MAINNET_GENESIS_HASH => Ok(WhirlpoolDeployment::mainnet()), other => anyhow::bail!( - "no Orca Whirlpools deployment is known for the network with genesis hash {other}. please connect to \ - devnet and mainnet only", + "no Orca Whirlpools deployment is known for the network with genesis hash {other}.", ), } } @@ -103,28 +91,29 @@ pub fn plan_swaps( let mut payer_atas_created: HashSet = HashSet::new(); for (deficit_mint, deficit_amount) in deficits { - let mut remaining = *deficit_amount; + let mut deficit_remaining = *deficit_amount; let (deficit_buffer, _) = find_buffer_pda(&ctx.program_id, deficit_mint); for (&surplus_mint, avail) in surplus_remaining.iter_mut() { - if remaining == 0 { + if deficit_remaining == 0 { break; } if *avail == 0 { continue; } + // See if there is a swap route from surplus to deficit let Some(fill) = orca.runtime.block_on(plan_pair( orca, ctx.payer.pubkey(), surplus_mint, *deficit_mint, deficit_buffer, - remaining, + deficit_remaining, *avail, ))? else { - // No initialized Orca pool for this pair; try the next surplus mint. + // No route for this pair; try the next surplus mint. continue; }; @@ -139,6 +128,7 @@ pub fn plan_swaps( swap_ixs.extend(fill.instructions); + // update the amount of funds needed to complete the settlement let sink = sinks.entry(surplus_mint).or_insert(0); *sink = sink .checked_add(fill.input_used) @@ -151,12 +141,12 @@ pub fn plan_swaps( })?; // Unlike the input side, the output can legitimately overshoot, so saturating is fine here - remaining = remaining.saturating_sub(fill.output_covered); + deficit_remaining = deficit_remaining.saturating_sub(fill.output_covered); } anyhow::ensure!( - remaining == 0, - "no Orca liquidity/surplus available to cover the remaining {remaining} of mint \ + deficit_remaining == 0, + "no Orca liquidity/surplus available to cover the remaining {deficit_remaining} of mint \ {deficit_mint}'s deficit", ); } @@ -169,24 +159,23 @@ pub fn plan_swaps( }) } -/// The result of covering (some or all of) one deficit from one surplus mint. +/// The result of covering (some or all of) one deficit mint from one surplus mint. struct PairFill { + /// Instructions to execute to actually complete the swap instructions: Vec, /// How much of `surplus_mint` this swap consumes. input_used: u64, - /// How much of `deficit_mint` this swap's output covers. The swap pays into - /// the deficit mint's buffer directly, so this is a lower bound on what the - /// buffer actually receives, not an amount transferred separately. + /// How much of `deficit_mint` this swap's output covers. output_covered: u64, } -/// Plan a single surplus/deficit pair: try to buy the full remaining deficit -/// with an exact-output swap; if the surplus mint can't cover that swap's -/// worst-case input, fall back to spending all of `surplus_avail` via an -/// exact-input swap instead (a partial fill, leaving the rest of the deficit -/// for another surplus mint). The proceeds are paid into `deficit_buffer` -/// rather than the signer's wallet. Returns `None` if there's no initialized -/// Orca pool for this pair. +/// Create a route for single surplus/deficit pair. +/// Try to buy the full remaining deficit with an exact-output swap. +/// If the surplus mint can't cover that swap's worst-case input, fall back +/// to spending all of `surplus_avail` via an exact-input swap instead +/// (a partial fill, leaving the rest of the deficit for another surplus mint). +/// The proceeds are paid into `deficit_buffer` rather than the signer's wallet. +/// Returns `None` if there's no suitable Orca pool for this pair. async fn plan_pair( orca: &OrcaClient, signer: Pubkey, @@ -200,6 +189,10 @@ async fn plan_pair( return Ok(None); }; + // HACK: orca's rust library only lets us put output tokens into our wallet's ATA. + // however, we actually want to put it into the buffers for the settlement program. + // So we need to search for where the output_ata appears in the transaciton, and replace + // with the actual wallet for the buffer. let output_ata = get_associated_token_address_with_program_id( &signer, &deficit_mint, @@ -221,6 +214,8 @@ async fn plan_pair( whirlpool_deployment: Some(orca.deployment), }; + // First we try an exact_out transaction. If input funds are insufficient, this will fail + // and we can exact_in the funds we actually have instead. let exact_out = swap_instructions( &orca.rpc, pool, @@ -240,6 +235,7 @@ async fn plan_pair( "swap unexpectedly requires extra signers beyond the payer", ); + // Exit early if we got the necessary funds if quote.token_max_in <= surplus_avail { return Ok(Some(PairFill { instructions: swap_instruction_rewrites(exact_out.instructions)?, @@ -305,9 +301,8 @@ fn rewrite_swap_output_to_buffer( return Some(ix); } - // Nothing references the output ATA any more, so drop the idempotent creation - // the SDK prepends for it: an account the swap no longer touches isn't worth - // the instruction or the rent. + // Orca also tries to add account creation transaction for the ATA, + // but we don't need it. (!creates_token_account(&ix, output_ata)).then_some(ix) }) .collect(); From bc7662d0ef0f6b77d97dfffbe712885c65780392 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 27 Aug 2026 18:54:36 +0900 Subject: [PATCH 4/4] undo changes to the bench report --- bench-report.json | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/bench-report.json b/bench-report.json index 870eb05..c4676e9 100644 --- a/bench-report.json +++ b/bench-report.json @@ -24,28 +24,28 @@ "transfer_authority/reclaim_authority_can_transfer_itself": 4 }, "compute_units": { - "create_buffers/happy_path_creates_initialized_buffer_token_account": 10335, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 21722, - "create_buffers/max_buffers_in_one_instruction": 176884, - "create_order/happy_path_creates_order_pda_with_expected_body": 4971, - "initialize/happy_path_initializes_state_pda_with_expected_data": 4521, - "reclaim_buffer/funded_buffer_is_skipped": 6290, - "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7441, - "reclaim_buffer/max_buffers_in_one_instruction": 136495, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18034, - "reclaim_order/happy_path_returns_lamports_and_closes_pda": 2179, - "settle/finalizes_with_no_pushes": 7042, - "settle/pulls_from_multiple_orders": 19872, - "settle/pulls_funds_to_destination": 13489, - "settle/pulls_to_multiple_destinations": 14636, - "settle/pushes_a_single_order": 12337, - "settle/pushes_several_orders_from_different_buffers": 17571, - "settle/pushes_several_orders_from_one_buffer": 17572, - "settle/settles_a_single_order": 12355, - "settle/settles_multiple_orders": 22849, - "transfer_authority/manager_can_transfer_manager": 3164, - "transfer_authority/manager_can_transfer_reclaim_authority": 3166, - "transfer_authority/reclaim_authority_can_transfer_itself": 3169 + "create_buffers/happy_path_creates_initialized_buffer_token_account": 10340, + "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 21731, + "create_buffers/max_buffers_in_one_instruction": 176947, + "create_order/happy_path_creates_order_pda_with_expected_body": 4976, + "initialize/happy_path_initializes_state_pda_with_expected_data": 4526, + "reclaim_buffer/funded_buffer_is_skipped": 6299, + "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7447, + "reclaim_buffer/max_buffers_in_one_instruction": 136501, + "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18043, + "reclaim_order/happy_path_returns_lamports_and_closes_pda": 2183, + "settle/finalizes_with_no_pushes": 7062, + "settle/pulls_from_multiple_orders": 19921, + "settle/pulls_funds_to_destination": 13524, + "settle/pulls_to_multiple_destinations": 14675, + "settle/pushes_a_single_order": 12366, + "settle/pushes_several_orders_from_different_buffers": 17608, + "settle/pushes_several_orders_from_one_buffer": 17609, + "settle/settles_a_single_order": 12384, + "settle/settles_multiple_orders": 22894, + "transfer_authority/manager_can_transfer_manager": 3170, + "transfer_authority/manager_can_transfer_reclaim_authority": 3172, + "transfer_authority/reclaim_authority_can_transfer_itself": 3175 }, "transaction_bytes": { "create_buffers/happy_path_creates_initialized_buffer_token_account": 303,