From f5af11e204b55697f2e75e5ec841582081a65bb6 Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Mon, 13 Apr 2026 23:25:44 -0400 Subject: [PATCH 01/15] feat: add solana_test_utils crate with SurfpoolManager Co-Authored-By: Claude Sonnet 4.6 --- src/Cargo.lock | 1221 ++++++++++++++++- src/Cargo.toml | 1 + src/solana_test_utils/Cargo.toml | 16 + src/solana_test_utils/src/lib.rs | 3 + src/solana_test_utils/src/surfpool/config.rs | 69 + src/solana_test_utils/src/surfpool/manager.rs | 159 +++ src/solana_test_utils/src/surfpool/mod.rs | 5 + 7 files changed, 1435 insertions(+), 39 deletions(-) create mode 100644 src/solana_test_utils/Cargo.toml create mode 100644 src/solana_test_utils/src/lib.rs create mode 100644 src/solana_test_utils/src/surfpool/config.rs create mode 100644 src/solana_test_utils/src/surfpool/manager.rs create mode 100644 src/solana_test_utils/src/surfpool/mod.rs diff --git a/src/Cargo.lock b/src/Cargo.lock index abf54322..f0d67500 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -718,8 +718,8 @@ dependencies = [ "rand 0.8.5", "rcgen", "ring", - "rustls", - "rustls-webpki", + "rustls 0.23.35", + "rustls-webpki 0.103.8", "serde", "serde_json", "socket2 0.5.10", @@ -729,7 +729,7 @@ dependencies = [ "tokio-util", "tower 0.4.13", "tracing", - "x509-parser", + "x509-parser 0.17.0", ] [[package]] @@ -1191,14 +1191,30 @@ dependencies = [ "serde", ] +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive 0.4.0", + "asn1-rs-impl 0.1.0", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "asn1-rs" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", + "asn1-rs-derive 0.6.0", + "asn1-rs-impl 0.2.0", "displaydoc", "nom", "num-traits", @@ -1207,6 +1223,18 @@ dependencies = [ "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-derive" version = "0.6.0" @@ -1219,6 +1247,17 @@ dependencies = [ "synstructure 0.13.2", ] +[[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 = "asn1-rs-impl" version = "0.2.0" @@ -1230,6 +1269,17 @@ dependencies = [ "syn 2.0.112", ] +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + [[package]] name = "async-compression" version = "0.4.36" @@ -1243,6 +1293,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 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -1413,7 +1474,7 @@ dependencies = [ "sha1", "sync_wrapper 1.0.2", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.28.0", "tower 0.5.2", "tower-layer", "tower-service", @@ -1505,6 +1566,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.21.7" @@ -1996,6 +2063,15 @@ 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 = "cbc" version = "0.1.2" @@ -2017,6 +2093,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" @@ -2158,7 +2240,7 @@ checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" dependencies = [ "serde", "termcolor", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -2177,6 +2259,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[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.35" @@ -2197,6 +2289,15 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "consensus-config" version = "0.1.0" @@ -2229,6 +2330,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", + "unicode-width 0.2.2", "windows-sys 0.59.0", ] @@ -2327,6 +2429,16 @@ dependencies = [ "libc", ] +[[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" @@ -2385,6 +2497,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -2674,13 +2795,27 @@ 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 0.5.2", + "displaydoc", + "nom", + "num-bigint 0.4.6", + "num-traits", + "rusticata-macros", +] + [[package]] name = "der-parser" version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.1", "displaydoc", "nom", "num-bigint 0.4.6", @@ -2842,6 +2977,29 @@ dependencies = [ "syn 2.0.112", ] +[[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.112", +] + [[package]] name = "dunce" version = "1.0.5" @@ -3176,6 +3334,33 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "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 5.4.1", + "pin-project-lite", +] + [[package]] name = "eyre" version = "0.6.12" @@ -3186,6 +3371,18 @@ dependencies = [ "once_cell", ] +[[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.2", + "siphasher 1.0.1", +] + [[package]] name = "fastcrypto" version = "0.1.8" @@ -3981,6 +4178,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "histogram" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cb882ccb290b8646e554b157ab0b71e64e8d5bef775cd66b6531e52d302669" + [[package]] name = "hkdf" version = "0.12.4" @@ -4174,10 +4377,10 @@ dependencies = [ "http 1.4.0", "hyper 1.8.1", "hyper-util", - "rustls", + "rustls 0.23.35", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tower-service", "webpki-roots 1.0.5", ] @@ -4471,6 +4674,19 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width 0.2.2", + "web-time", +] + [[package]] name = "inline_colorization" version = "0.1.6" @@ -4599,6 +4815,50 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +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.112", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -4628,6 +4888,21 @@ dependencies = [ "tabled", ] +[[package]] +name = "jsonrpc-core" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" +dependencies = [ + "futures", + "futures-executor", + "futures-util", + "log", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "jupiter-swap-api-client" version = "0.2.0" @@ -5265,7 +5540,7 @@ dependencies = [ "indexmap 2.12.1", "leb128", "move-proc-macros", - "num", + "num 0.4.3", "once_cell", "primitive-types 0.10.1", "rand 0.8.5", @@ -5599,12 +5874,12 @@ dependencies = [ "pin-project-lite", "prometheus", "rand 0.8.5", - "rustls", + "rustls 0.23.35", "serde", "snap", "sui-http", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-stream", "tonic 0.13.1", "tonic-health", @@ -5622,10 +5897,10 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -5675,6 +5950,19 @@ dependencies = [ "memoffset 0.9.1", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset 0.9.1", +] + [[package]] name = "no-std-compat" version = "0.4.1" @@ -5712,6 +6000,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint 0.2.6", + "num-complex 0.2.4", + "num-integer", + "num-iter", + "num-rational 0.2.4", + "num-traits", +] + [[package]] name = "num" version = "0.4.3" @@ -5719,10 +6021,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-bigint 0.4.6", - "num-complex", + "num-complex 0.4.6", "num-integer", "num-iter", - "num-rational", + "num-rational 0.4.2", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", "num-traits", ] @@ -5764,6 +6077,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -5811,6 +6134,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-bigint 0.2.6", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -5885,6 +6220,12 @@ dependencies = [ "syn 2.0.112", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "nybbles" version = "0.4.6" @@ -5910,11 +6251,20 @@ dependencies = [ [[package]] name = "oid-registry" -version = "0.8.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" dependencies = [ - "asn1-rs", + "asn1-rs 0.5.2", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs 0.7.1", ] [[package]] @@ -5967,6 +6317,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "openssl-sys" version = "0.9.111" @@ -6066,7 +6422,7 @@ checksum = "ae7891b22598926e4398790c8fe6447930c72a67d36d983a49d6ce682ce83290" dependencies = [ "bytecount", "fnv", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -6123,6 +6479,12 @@ dependencies = [ "syn 2.0.112", ] +[[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" @@ -6274,6 +6636,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 = "pem" version = "3.0.6" @@ -6308,6 +6679,15 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "percentage" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd23b938276f14057220b707937bcb42fa76dda7560e57a2da30cb52d557937" +dependencies = [ + "num 0.2.1", +] + [[package]] name = "pest" version = "2.8.4" @@ -7039,7 +7419,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls", + "rustls 0.23.35", "socket2 0.6.1", "thiserror 2.0.17", "tokio", @@ -7054,13 +7434,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", + "fastbloom", "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", "rustc-hash", - "rustls", + "rustls 0.23.35", "rustls-pki-types", + "rustls-platform-verifier", "slab", "thiserror 2.0.17", "tinyvec", @@ -7283,7 +7665,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" dependencies = [ - "pem", + "pem 3.0.6", "ring", "rustls-pki-types", "time", @@ -7406,7 +7788,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls", + "rustls 0.23.35", "rustls-pki-types", "serde", "serde_json", @@ -7414,7 +7796,7 @@ dependencies = [ "sync_wrapper 1.0.2", "tokio", "tokio-native-tls", - "tokio-rustls", + "tokio-rustls 0.26.4", "tower 0.5.2", "tower-http 0.6.8", "tower-service", @@ -7425,6 +7807,21 @@ dependencies = [ "webpki-roots 1.0.5", ] +[[package]] +name = "reqwest-middleware" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" +dependencies = [ + "anyhow", + "async-trait", + "http 1.4.0", + "reqwest", + "serde", + "thiserror 1.0.69", + "tower-service", +] + [[package]] name = "rfc6979" version = "0.3.1" @@ -7664,6 +8061,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.23.35" @@ -7674,11 +8083,23 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.8", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + [[package]] name = "rustls-pemfile" version = "2.2.0" @@ -7698,6 +8119,43 @@ 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 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.35", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.8", + "security-framework 3.5.1", + "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.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.103.8" @@ -7806,6 +8264,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "seahash" version = "4.1.0" @@ -7888,7 +8356,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -8583,6 +9064,52 @@ dependencies = [ "borsh 1.6.0", ] +[[package]] +name = "solana-client" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc55d1f263e0be4127daf33378d313ea0977f9ffd3fba50fa544ca26722fc695" +dependencies = [ + "async-trait", + "bincode", + "dashmap 5.5.3", + "futures", + "futures-util", + "indexmap 2.12.1", + "indicatif", + "log", + "quinn", + "rayon", + "solana-account", + "solana-client-traits", + "solana-commitment-config", + "solana-connection-cache", + "solana-epoch-info", + "solana-hash 2.3.0", + "solana-instruction 2.3.3", + "solana-keypair", + "solana-measure", + "solana-message", + "solana-pubkey 2.4.0", + "solana-pubsub-client", + "solana-quic-client", + "solana-quic-definitions", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-rpc-client-nonce-utils", + "solana-signature 2.3.0", + "solana-signer 2.2.1", + "solana-streamer", + "solana-thin-client", + "solana-time-utils", + "solana-tpu-client", + "solana-transaction", + "solana-transaction-error 2.2.1", + "solana-udp-client", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "solana-client-traits" version = "2.2.1" @@ -8677,6 +9204,29 @@ dependencies = [ "solana-program", ] +[[package]] +name = "solana-connection-cache" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c1cff5ebb26aefff52f1a8e476de70ec1683f8cc6e4a8c86b615842d91f436" +dependencies = [ + "async-trait", + "bincode", + "crossbeam-channel", + "futures-util", + "indexmap 2.12.1", + "log", + "rand 0.8.5", + "rayon", + "solana-keypair", + "solana-measure", + "solana-metrics", + "solana-time-utils", + "solana-transaction-error 2.2.1", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "solana-cpi" version = "2.2.1" @@ -9265,6 +9815,12 @@ dependencies = [ "signal-hook", ] +[[package]] +name = "solana-measure" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11dcd67cd2ae6065e494b64e861e0498d046d95a61cbbf1ae3d58be1ea0f42ed" + [[package]] name = "solana-message" version = "2.4.0" @@ -9288,6 +9844,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "solana-metrics" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0375159d8460f423d39e5103dcff6e07796a5ec1850ee1fcfacfd2482a8f34b5" +dependencies = [ + "crossbeam-channel", + "gethostname", + "log", + "reqwest", + "solana-cluster-type", + "solana-sha256-hasher 2.3.0", + "solana-time-utils", + "thiserror 2.0.17", +] + [[package]] name = "solana-msg" version = "2.2.1" @@ -9312,6 +9884,27 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61515b880c36974053dd499c0510066783f0cc6ac17def0c7ef2a244874cf4a9" +[[package]] +name = "solana-net-utils" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a9e831d0f09bd92135d48c5bc79071bb59c0537b9459f1b4dec17ecc0558fa" +dependencies = [ + "anyhow", + "bincode", + "bytes", + "itertools 0.12.1", + "log", + "nix 0.30.1", + "rand 0.8.5", + "serde", + "serde_derive", + "socket2 0.5.10", + "solana-serde", + "tokio", + "url", +] + [[package]] name = "solana-nonce" version = "2.2.1" @@ -9378,6 +9971,38 @@ dependencies = [ "solana_parser", ] +[[package]] +name = "solana-perf" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37192c0be5c222ca49dbc5667288c5a8bb14837051dd98e541ee4dad160a5da9" +dependencies = [ + "ahash 0.8.12", + "bincode", + "bv", + "bytes", + "caps", + "curve25519-dalek 4.1.3", + "dlopen2", + "fnv", + "libc", + "log", + "nix 0.30.1", + "rand 0.8.5", + "rayon", + "serde", + "solana-hash 2.3.0", + "solana-message", + "solana-metrics", + "solana-packet", + "solana-pubkey 2.4.0", + "solana-rayon-threadlimit", + "solana-sdk-ids 2.2.1", + "solana-short-vec", + "solana-signature 2.3.0", + "solana-time-utils", +] + [[package]] name = "solana-poh-config" version = "2.2.1" @@ -9648,6 +10273,63 @@ dependencies = [ "solana-address 2.0.0", ] +[[package]] +name = "solana-pubsub-client" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d18a7476e1d2e8df5093816afd8fffee94fbb6e442d9be8e6bd3e85f88ce8d5c" +dependencies = [ + "crossbeam-channel", + "futures-util", + "http 0.2.12", + "log", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "solana-account-decoder-client-types", + "solana-clock 2.2.2", + "solana-pubkey 2.4.0", + "solana-rpc-client-types", + "solana-signature 2.3.0", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-tungstenite 0.20.1", + "tungstenite 0.20.1", + "url", +] + +[[package]] +name = "solana-quic-client" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44feb5f4a97494459c435aa56de810500cc24e22d0afc632990a8e54a07c05a4" +dependencies = [ + "async-lock", + "async-trait", + "futures", + "itertools 0.12.1", + "log", + "quinn", + "quinn-proto", + "rustls 0.23.35", + "solana-connection-cache", + "solana-keypair", + "solana-measure", + "solana-metrics", + "solana-net-utils", + "solana-pubkey 2.4.0", + "solana-quic-definitions", + "solana-rpc-client-api", + "solana-signer 2.2.1", + "solana-streamer", + "solana-tls-utils", + "solana-transaction-error 2.2.1", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "solana-quic-definitions" version = "2.3.1" @@ -9657,6 +10339,15 @@ dependencies = [ "solana-keypair", ] +[[package]] +name = "solana-rayon-threadlimit" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cc2a4cae3ef7bb6346b35a60756d2622c297d5fa204f96731db9194c0dc75b" +dependencies = [ + "num_cpus", +] + [[package]] name = "solana-rent" version = "2.2.1" @@ -9732,6 +10423,111 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "solana-rpc-client" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d3161ac0918178e674c1f7f1bfac40de3e7ed0383bd65747d63113c156eaeb" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bincode", + "bs58 0.5.1", + "futures", + "indicatif", + "log", + "reqwest", + "reqwest-middleware", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "solana-account", + "solana-account-decoder-client-types", + "solana-clock 2.2.2", + "solana-commitment-config", + "solana-epoch-info", + "solana-epoch-schedule 2.2.1", + "solana-feature-gate-interface", + "solana-hash 2.3.0", + "solana-instruction 2.3.3", + "solana-message", + "solana-pubkey 2.4.0", + "solana-rpc-client-api", + "solana-signature 2.3.0", + "solana-transaction", + "solana-transaction-error 2.2.1", + "solana-transaction-status-client-types", + "solana-version", + "solana-vote-interface", + "tokio", +] + +[[package]] +name = "solana-rpc-client-api" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dbc138685c79d88a766a8fd825057a74ea7a21e1dd7f8de275ada899540fff7" +dependencies = [ + "anyhow", + "jsonrpc-core", + "reqwest", + "reqwest-middleware", + "serde", + "serde_derive", + "serde_json", + "solana-account-decoder-client-types", + "solana-clock 2.2.2", + "solana-rpc-client-types", + "solana-signer 2.2.1", + "solana-transaction-error 2.2.1", + "solana-transaction-status-client-types", + "thiserror 2.0.17", +] + +[[package]] +name = "solana-rpc-client-nonce-utils" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f0ee41b9894ff36adebe546a110b899b0d0294b07845d8acdc73822e6af4b0" +dependencies = [ + "solana-account", + "solana-commitment-config", + "solana-hash 2.3.0", + "solana-message", + "solana-nonce", + "solana-pubkey 2.4.0", + "solana-rpc-client", + "solana-sdk-ids 2.2.1", + "thiserror 2.0.17", +] + +[[package]] +name = "solana-rpc-client-types" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea428a81729255d895ea47fba9b30fd4dacbfe571a080448121bd0592751676" +dependencies = [ + "base64 0.22.1", + "bs58 0.5.1", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "solana-account", + "solana-account-decoder-client-types", + "solana-clock 2.2.2", + "solana-commitment-config", + "solana-fee-calculator 2.2.1", + "solana-inflation", + "solana-pubkey 2.4.0", + "solana-transaction-error 2.2.1", + "solana-transaction-status-client-types", + "solana-version", + "spl-generic-token", + "thiserror 2.0.17", +] + [[package]] name = "solana-sanitize" version = "2.2.1" @@ -10173,6 +10969,53 @@ dependencies = [ "solana-sysvar-id 2.2.1", ] +[[package]] +name = "solana-streamer" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5643516e5206b89dd4bdf67c39815606d835a51a13260e43349abdb92d241b1d" +dependencies = [ + "async-channel", + "bytes", + "crossbeam-channel", + "dashmap 5.5.3", + "futures", + "futures-util", + "governor", + "histogram", + "indexmap 2.12.1", + "itertools 0.12.1", + "libc", + "log", + "nix 0.30.1", + "pem 1.1.1", + "percentage", + "quinn", + "quinn-proto", + "rand 0.8.5", + "rustls 0.23.35", + "smallvec", + "socket2 0.5.10", + "solana-keypair", + "solana-measure", + "solana-metrics", + "solana-net-utils", + "solana-packet", + "solana-perf", + "solana-pubkey 2.4.0", + "solana-quic-definitions", + "solana-signature 2.3.0", + "solana-signer 2.2.1", + "solana-time-utils", + "solana-tls-utils", + "solana-transaction-error 2.2.1", + "solana-transaction-metrics-tracker", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "x509-parser 0.14.0", +] + [[package]] name = "solana-svm-feature-set" version = "2.3.13" @@ -10314,12 +11157,88 @@ dependencies = [ "solana-sdk-ids 3.1.0", ] +[[package]] +name = "solana-thin-client" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c1025715a113e0e2e379b30a6bfe4455770dc0759dabf93f7dbd16646d5acbe" +dependencies = [ + "bincode", + "log", + "rayon", + "solana-account", + "solana-client-traits", + "solana-clock 2.2.2", + "solana-commitment-config", + "solana-connection-cache", + "solana-epoch-info", + "solana-hash 2.3.0", + "solana-instruction 2.3.3", + "solana-keypair", + "solana-message", + "solana-pubkey 2.4.0", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-signature 2.3.0", + "solana-signer 2.2.1", + "solana-system-interface 1.0.0", + "solana-transaction", + "solana-transaction-error 2.2.1", +] + [[package]] name = "solana-time-utils" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6af261afb0e8c39252a04d026e3ea9c405342b08c871a2ad8aa5448e068c784c" +[[package]] +name = "solana-tls-utils" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14494aa87a75a883d1abcfee00f1278a28ecc594a2f030084879eb40570728f6" +dependencies = [ + "rustls 0.23.35", + "solana-keypair", + "solana-pubkey 2.4.0", + "solana-signer 2.2.1", + "x509-parser 0.14.0", +] + +[[package]] +name = "solana-tpu-client" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17895ce70fd1dd93add3fbac87d599954ded93c63fa1c66f702d278d96a6da14" +dependencies = [ + "async-trait", + "bincode", + "futures-util", + "indexmap 2.12.1", + "indicatif", + "log", + "rayon", + "solana-client-traits", + "solana-clock 2.2.2", + "solana-commitment-config", + "solana-connection-cache", + "solana-epoch-schedule 2.2.1", + "solana-measure", + "solana-message", + "solana-net-utils", + "solana-pubkey 2.4.0", + "solana-pubsub-client", + "solana-quic-definitions", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-signature 2.3.0", + "solana-signer 2.2.1", + "solana-transaction", + "solana-transaction-error 2.2.1", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "solana-transaction" version = "2.2.3" @@ -10386,6 +11305,22 @@ dependencies = [ "solana-sanitize 3.0.1", ] +[[package]] +name = "solana-transaction-metrics-tracker" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03fc4e1b6252dc724f5ee69db6229feb43070b7318651580d2174da8baefb993" +dependencies = [ + "base64 0.22.1", + "bincode", + "log", + "rand 0.8.5", + "solana-packet", + "solana-perf", + "solana-short-vec", + "solana-signature 2.3.0", +] + [[package]] name = "solana-transaction-status" version = "2.3.13" @@ -10453,12 +11388,43 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "solana-udp-client" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd36227dd3035ac09a89d4239551d2e3d7d9b177b61ccc7c6d393c3974d0efa" +dependencies = [ + "async-trait", + "solana-connection-cache", + "solana-keypair", + "solana-net-utils", + "solana-streamer", + "solana-transaction-error 2.2.1", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "solana-validator-exit" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bbf6d7a3c0b28dd5335c52c0e9eae49d0ae489a8f324917faf0ded65a812c1d" +[[package]] +name = "solana-version" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3324d46c7f7b7f5d34bf7dc71a2883bdc072c7b28ca81d0b2167ecec4cf8da9f" +dependencies = [ + "agave-feature-set", + "rand 0.8.5", + "semver 1.0.27", + "serde", + "serde_derive", + "solana-sanitize 2.2.1", + "solana-serde-varint", +] + [[package]] name = "solana-vote-interface" version = "2.2.6" @@ -10588,6 +11554,17 @@ dependencies = [ "solana_idl", ] +[[package]] +name = "solana_test_utils" +version = "0.1.0" +dependencies = [ + "anyhow", + "solana-client", + "solana-sdk", + "tokio", + "tracing", +] + [[package]] name = "spin" version = "0.9.8" @@ -11778,7 +12755,7 @@ dependencies = [ "pin-project-lite", "socket2 0.5.10", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-util", "tower 0.5.2", "tracing", @@ -12014,7 +12991,7 @@ dependencies = [ "tonic 0.13.1", "tracing", "typed-store-error", - "x509-parser", + "x509-parser 0.17.0", ] [[package]] @@ -12096,7 +13073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -12118,7 +13095,7 @@ checksum = "0ce69a5028cd9576063ec1f48edb2c75339fd835e6094ef3e05b3a079bf594a6" dependencies = [ "papergrid", "tabled_derive", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -12304,6 +13281,7 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2 0.6.1", @@ -12342,13 +13320,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls", + "rustls 0.23.35", "tokio", ] @@ -12364,6 +13352,21 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", + "tungstenite 0.20.1", + "webpki-roots 0.25.4", +] + [[package]] name = "tokio-tungstenite" version = "0.28.0" @@ -12373,7 +13376,7 @@ dependencies = [ "futures-util", "log", "tokio", - "tungstenite", + "tungstenite 0.28.0", ] [[package]] @@ -12491,7 +13494,7 @@ dependencies = [ "prost 0.13.5", "socket2 0.5.10", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-stream", "tower 0.5.2", "tower-layer", @@ -12617,13 +13620,18 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ + "async-compression", "bitflags 2.10.0", "bytes", + "futures-core", "futures-util", "http 1.4.0", "http-body 1.0.1", + "http-body-util", "iri-string", "pin-project-lite", + "tokio", + "tokio-util", "tower 0.5.2", "tower-layer", "tower-service", @@ -12770,6 +13778,27 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 0.2.12", + "httparse", + "log", + "rand 0.8.5", + "rustls 0.21.12", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", + "webpki-roots 0.24.0", +] + [[package]] name = "tungstenite" version = "0.28.0" @@ -12878,6 +13907,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -13126,7 +14161,7 @@ dependencies = [ "elliptic-curve-tools", "generic-array 1.3.5", "hex", - "num", + "num 0.4.3", "rand_core 0.6.4", "serde", "sha3", @@ -13285,6 +14320,30 @@ dependencies = [ "untrusted", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" +dependencies = [ + "rustls-webpki 0.101.7", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "webpki-roots" version = "0.26.11" @@ -13416,6 +14475,15 @@ dependencies = [ "windows-link", ] +[[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" @@ -13452,6 +14520,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" @@ -13485,6 +14568,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" @@ -13497,6 +14586,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" @@ -13509,6 +14604,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" @@ -13533,6 +14634,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" @@ -13545,6 +14652,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" @@ -13557,6 +14670,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" @@ -13569,6 +14688,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" @@ -13628,18 +14753,36 @@ dependencies = [ "spki 0.7.3", ] +[[package]] +name = "x509-parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8" +dependencies = [ + "asn1-rs 0.5.2", + "base64 0.13.1", + "data-encoding", + "der-parser 8.2.0", + "lazy_static", + "nom", + "oid-registry 0.6.1", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "x509-parser" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.1", "data-encoding", - "der-parser", + "der-parser 10.0.0", "lazy_static", "nom", - "oid-registry", + "oid-registry 0.8.1", "ring", "rusticata-macros", "thiserror 2.0.17", diff --git a/src/Cargo.toml b/src/Cargo.toml index c4e42db5..ee7a2624 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -17,6 +17,7 @@ members = [ "chain_parsers/visualsign-sui", "chain_parsers/visualsign-tron", "chain_parsers/visualsign-unspecified", + "solana_test_utils", ] resolver = "3" diff --git a/src/solana_test_utils/Cargo.toml b/src/solana_test_utils/Cargo.toml new file mode 100644 index 00000000..989615f8 --- /dev/null +++ b/src/solana_test_utils/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "solana_test_utils" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +solana-sdk = "2.1.15" +solana-client = "2.1.15" +tokio = { workspace = true, features = ["full"] } +anyhow = "1.0" +tracing = { workspace = true } + +[lints] +workspace = true diff --git a/src/solana_test_utils/src/lib.rs b/src/solana_test_utils/src/lib.rs new file mode 100644 index 00000000..b2427ebc --- /dev/null +++ b/src/solana_test_utils/src/lib.rs @@ -0,0 +1,3 @@ +pub mod surfpool; + +pub use surfpool::{SurfpoolConfig, SurfpoolManager}; diff --git a/src/solana_test_utils/src/surfpool/config.rs b/src/solana_test_utils/src/surfpool/config.rs new file mode 100644 index 00000000..f51e4292 --- /dev/null +++ b/src/solana_test_utils/src/surfpool/config.rs @@ -0,0 +1,69 @@ +use std::path::PathBuf; + +/// Configuration for a Surfpool validator instance. +#[derive(Debug, Clone)] +pub struct SurfpoolConfig { + /// RPC URL to fork from (e.g., mainnet-beta via Helius). + pub fork_url: Option, + /// Local RPC port (auto-selected if `None`). + pub rpc_port: Option, + /// Local WebSocket port (auto-selected if `None`). + pub ws_port: Option, + /// Ledger directory path. + pub ledger_path: Option, + /// Reset ledger on startup. + pub reset_ledger: bool, + /// Log level for the surfpool process. + pub log_level: String, +} + +impl Default for SurfpoolConfig { + fn default() -> Self { + let fork_url = std::env::var("HELIUS_API_KEY") + .ok() + .map(|key| format!("https://mainnet.helius-rpc.com/?api-key={key}")) + .or_else(|| std::env::var("SOLANA_RPC_URL").ok()) + .unwrap_or_else(|| "https://api.mainnet-beta.solana.com".to_string()); + + Self { + fork_url: Some(fork_url), + rpc_port: None, + ws_port: None, + ledger_path: None, + reset_ledger: true, + log_level: "info".to_string(), + } + } +} + +impl SurfpoolConfig { + pub fn with_fork_url(mut self, url: impl Into) -> Self { + self.fork_url = Some(url.into()); + self + } + + pub fn with_rpc_port(mut self, port: u16) -> Self { + self.rpc_port = Some(port); + self + } + + pub fn with_ws_port(mut self, port: u16) -> Self { + self.ws_port = Some(port); + self + } + + pub fn with_ledger_path(mut self, path: impl Into) -> Self { + self.ledger_path = Some(path.into()); + self + } + + pub fn with_reset_ledger(mut self, reset: bool) -> Self { + self.reset_ledger = reset; + self + } + + pub fn with_log_level(mut self, level: impl Into) -> Self { + self.log_level = level.into(); + self + } +} diff --git a/src/solana_test_utils/src/surfpool/manager.rs b/src/solana_test_utils/src/surfpool/manager.rs new file mode 100644 index 00000000..953b6c1c --- /dev/null +++ b/src/solana_test_utils/src/surfpool/manager.rs @@ -0,0 +1,159 @@ +use super::config::SurfpoolConfig; +use anyhow::{Context, Result}; +use solana_client::rpc_client::RpcClient; +use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Signature}; +use std::net::TcpListener; +use std::process::{Child, Command}; +use std::thread; +use std::time::Duration; +use tracing::{debug, info, warn}; + +/// Manages the lifecycle of a Surfpool validator instance. +/// +/// Spawns a `surfpool` subprocess on [`start`](Self::start), polls until the +/// RPC server is ready, and kills the process on [`Drop`]. +pub struct SurfpoolManager { + process: Option, + rpc_url: String, + ws_url: String, +} + +impl SurfpoolManager { + /// Start a new Surfpool instance with the given configuration. + pub async fn start(config: SurfpoolConfig) -> Result { + info!("Starting Surfpool with config: {:?}", config); + + let rpc_port = config.rpc_port.map_or_else(Self::find_free_port, Ok)?; + let ws_port = config.ws_port.map_or_else(Self::find_free_port, Ok)?; + + let rpc_url = format!("http://127.0.0.1:{rpc_port}"); + let ws_url = format!("ws://127.0.0.1:{ws_port}"); + + let mut args = vec![ + "--rpc-port".to_string(), + rpc_port.to_string(), + "--ws-port".to_string(), + ws_port.to_string(), + "--log".to_string(), + ]; + + if let Some(fork_url) = &config.fork_url { + args.push("--url".to_string()); + args.push(fork_url.clone()); + } + + if let Some(ledger_path) = &config.ledger_path { + args.push("--ledger".to_string()); + args.push(ledger_path.to_string_lossy().to_string()); + } + + if config.reset_ledger { + args.push("--reset".to_string()); + } + + debug!("Spawning surfpool with args: {:?}", args); + + let child = Command::new("surfpool") + .args(&args) + .spawn() + .context("Failed to spawn surfpool process. Is surfpool installed?")?; + + let manager = Self { + process: Some(child), + rpc_url: rpc_url.clone(), + ws_url, + }; + + manager + .wait_ready() + .await + .context("Surfpool failed to become ready")?; + + info!("Surfpool started successfully at {}", rpc_url); + Ok(manager) + } + + /// Poll the RPC server until it responds (up to 30 attempts, 500ms apart). + pub async fn wait_ready(&self) -> Result<()> { + let client = self.rpc_client(); + let max_attempts = 30; + let delay = Duration::from_millis(500); + + for attempt in 1..=max_attempts { + debug!( + "Checking if Surfpool is ready (attempt {}/{})", + attempt, max_attempts + ); + match client.get_version() { + Ok(version) => { + info!("Surfpool is ready! Version: {:?}", version); + return Ok(()); + } + Err(e) => { + if attempt == max_attempts { + return Err(anyhow::anyhow!( + "Surfpool did not become ready after {max_attempts} attempts: {e}" + )); + } + warn!("Surfpool not ready yet (attempt {}): {}", attempt, e); + thread::sleep(delay); + } + } + } + + Err(anyhow::anyhow!("Surfpool readiness check failed")) + } + + /// Return an RPC client pointed at this instance. + pub fn rpc_client(&self) -> RpcClient { + RpcClient::new_with_commitment(self.rpc_url.clone(), CommitmentConfig::confirmed()) + } + + pub fn rpc_url(&self) -> &str { + &self.rpc_url + } + + pub fn ws_url(&self) -> &str { + &self.ws_url + } + + /// Request an airdrop and wait for confirmation (bounded). + pub async fn airdrop(&self, pubkey: &Pubkey, lamports: u64) -> Result { + let client = self.rpc_client(); + let signature = client + .request_airdrop(pubkey, lamports) + .context("Failed to request airdrop")?; + + let max_attempts = 60; + for _ in 0..max_attempts { + if let Ok(Some(_status)) = client.get_signature_status(&signature) { + return Ok(signature); + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + + Err(anyhow::anyhow!( + "Airdrop confirmation timed out after {max_attempts} attempts" + )) + } + + /// Find a free TCP port by binding to port 0. + fn find_free_port() -> Result { + let listener = TcpListener::bind("127.0.0.1:0").context("Failed to bind ephemeral port")?; + let port = listener + .local_addr() + .context("Failed to get local address")? + .port(); + Ok(port) + } +} + +impl Drop for SurfpoolManager { + fn drop(&mut self) { + if let Some(mut child) = self.process.take() { + info!("Stopping Surfpool process"); + let _ = child.kill(); + let _ = child.wait(); + } + } +} diff --git a/src/solana_test_utils/src/surfpool/mod.rs b/src/solana_test_utils/src/surfpool/mod.rs new file mode 100644 index 00000000..474541bb --- /dev/null +++ b/src/solana_test_utils/src/surfpool/mod.rs @@ -0,0 +1,5 @@ +mod config; +mod manager; + +pub use config::SurfpoolConfig; +pub use manager::SurfpoolManager; From 282c42352260064cb63585e2e6caf98d4cc32bb7 Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Mon, 13 Apr 2026 23:35:38 -0400 Subject: [PATCH 02/15] test: add surfpool mainnet-fork integration test for Solana parser Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Cargo.lock | 2 + .../visualsign-solana/Cargo.toml | 2 + .../visualsign-solana/tests/surfpool_fuzz.rs | 94 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs diff --git a/src/Cargo.lock b/src/Cargo.lock index f0d67500..515325b4 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -14088,11 +14088,13 @@ dependencies = [ "solana-system-interface 1.0.0", "solana-transaction-status", "solana_parser", + "solana_test_utils", "spl-associated-token-account 6.0.0", "spl-stake-pool", "spl-token 7.0.0", "spl-token-2022 10.0.0", "spl-token-2022-interface", + "tokio", "tracing", "visualsign", ] diff --git a/src/chain_parsers/visualsign-solana/Cargo.toml b/src/chain_parsers/visualsign-solana/Cargo.toml index a07d5b3f..25c082ea 100644 --- a/src/chain_parsers/visualsign-solana/Cargo.toml +++ b/src/chain_parsers/visualsign-solana/Cargo.toml @@ -26,12 +26,14 @@ spl-token-2022-interface = "2.1.0" [dev-dependencies] solana-parser-fuzz-core = { git = "https://github.com/anchorageoss/solana-parser.git", rev = "a0c554d", features = ["proptest"] } +solana_test_utils = { path = "../../solana_test_utils" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" jupiter-swap-api-client = "0.2.0" base64 = "0.22.1" bs58 = "0.5" proptest = "1" +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } [lints] workspace = true diff --git a/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs b/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs new file mode 100644 index 00000000..707dcfc8 --- /dev/null +++ b/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs @@ -0,0 +1,94 @@ +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] +//! Surfpool-backed integration tests for the Solana visual-sign parser. +//! +//! These tests start a **surfpool** mainnet fork (requires the `surfpool` +//! binary on `$PATH` and network access) and exercise the parser against +//! transactions built from real on-chain state. +//! +//! All tests are `#[ignore]` — run them explicitly: +//! +//! ```bash +//! cargo test -p visualsign-solana --test surfpool_fuzz -- --ignored +//! ``` + +mod common; + +use common::{build_disc_data, build_transaction, load_idl_from_env, options_with_idl}; +use solana_sdk::pubkey::Pubkey; +use solana_test_utils::{SurfpoolConfig, SurfpoolManager}; +use visualsign::vsptrait::{Transaction, VisualSignConverter}; +use visualsign_solana::{SolanaTransactionWrapper, SolanaVisualSignConverter}; + +// ── Tests ──────────────────────────────────────────────────────────────────── + +/// Smoke-test: start surfpool, verify the RPC endpoint responds with a +/// version string, then let `SurfpoolManager` tear it down on drop. +#[tokio::test] +#[ignore] +async fn surfpool_lifecycle() { + let manager = SurfpoolManager::start(SurfpoolConfig::default()) + .await + .expect("surfpool should start"); + + let client = manager.rpc_client(); + let version = client + .get_version() + .expect("RPC should respond with a version"); + + assert!( + !version.solana_core.is_empty(), + "solana_core version string must not be empty" + ); +} + +/// End-to-end: load a real IDL from `IDL_FILE`, extract the first +/// instruction's discriminator, build a transaction containing those bytes, +/// and run it through the visual-sign converter. The converter must return +/// `Ok` with at least one field (the instruction line). +#[tokio::test] +#[ignore] +async fn surfpool_jupiter_swap_roundtrip() { + // Skip gracefully when IDL_FILE is not set. + let (idl_json, _idl) = match load_idl_from_env() { + Some(pair) => pair, + None => { + eprintln!("IDL_FILE not set or invalid -- skipping surfpool_jupiter_swap_roundtrip"); + return; + } + }; + + // Start surfpool (validates that a local fork is healthy). + let _manager = SurfpoolManager::start(SurfpoolConfig::default()) + .await + .expect("surfpool should start"); + + // Build instruction data using the first instruction's discriminator. + let inst_idx = 0; + let arg_bytes: &[u8] = &[0u8; 32]; // arbitrary argument padding + let (_parsed_idl, data) = build_disc_data(&idl_json, inst_idx, arg_bytes) + .expect("IDL should have at least one instruction with a discriminator"); + + // Use a unique program ID for the synthetic transaction. + let program_name = "test_program"; + let program_id = Pubkey::new_unique(); + + let tx = build_transaction(program_id, vec![Pubkey::new_unique()], data); + + // Serialize the transaction to base64 so we can round-trip through from_string. + let tx_bytes = bincode::serialize(&tx).expect("transaction should serialize"); + let tx_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &tx_bytes); + + let wrapper = SolanaTransactionWrapper::from_string(&tx_b64) + .expect("from_string should succeed for a valid base64 transaction"); + + let options = options_with_idl(&program_id, &idl_json, program_name); + + let payload = SolanaVisualSignConverter + .to_visual_sign_payload(wrapper, options) + .expect("converter should succeed"); + + assert!( + !payload.fields.is_empty(), + "payload must contain at least one field" + ); +} From 6284b2a51d17428cf1f71bb563637b1692d5f1a4 Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Mon, 13 Apr 2026 23:40:30 -0400 Subject: [PATCH 03/15] feat: add idl-meta tool for Python-free IDL metadata extraction --- src/Cargo.lock | 9 +++ src/Cargo.toml | 1 + src/tools/idl-meta/Cargo.toml | 14 +++++ src/tools/idl-meta/src/main.rs | 105 +++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 src/tools/idl-meta/Cargo.toml create mode 100644 src/tools/idl-meta/src/main.rs diff --git a/src/Cargo.lock b/src/Cargo.lock index 515325b4..496300ce 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -4563,6 +4563,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idl-meta" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", +] + [[package]] name = "idna" version = "1.1.0" diff --git a/src/Cargo.toml b/src/Cargo.toml index ee7a2624..d0d6ff8c 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -18,6 +18,7 @@ members = [ "chain_parsers/visualsign-tron", "chain_parsers/visualsign-unspecified", "solana_test_utils", + "tools/idl-meta", ] resolver = "3" diff --git a/src/tools/idl-meta/Cargo.toml b/src/tools/idl-meta/Cargo.toml new file mode 100644 index 00000000..5f5980dc --- /dev/null +++ b/src/tools/idl-meta/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "idl-meta" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +serde_json = { workspace = true } +serde = { workspace = true } +anyhow = "1.0" + +[lints] +workspace = true diff --git a/src/tools/idl-meta/src/main.rs b/src/tools/idl-meta/src/main.rs new file mode 100644 index 00000000..62f9bf2c --- /dev/null +++ b/src/tools/idl-meta/src/main.rs @@ -0,0 +1,105 @@ +//! Minimal tool for IDL metadata extraction. +//! +//! Replaces the Python snippets in `scripts/fuzz_all_idls.sh`. +//! +//! Usage: +//! idl-meta locate-idls --manifest-path +//! idl-meta counts + +use anyhow::{Context, Result}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn main() -> Result<()> { + let args: Vec = std::env::args().collect(); + let subcmd = args.get(1).map(String::as_str); + + match subcmd { + Some("locate-idls") => { + let manifest_path = args + .iter() + .position(|a| a == "--manifest-path") + .and_then(|i| args.get(i + 1)) + .context("usage: idl-meta locate-idls --manifest-path ")?; + let dir = locate_idl_dir(manifest_path)?; + println!("{}", dir.display()); + } + Some("counts") => { + let file = args + .get(2) + .context("usage: idl-meta counts ")?; + let (instructions, types) = idl_counts(file)?; + println!("{instructions} {types}"); + } + _ => { + anyhow::bail!( + "usage: idl-meta [args]\n\ + \n locate-idls --manifest-path \ + Print the solana_parser IDL directory\n \ + counts \ + Print instruction and type counts" + ); + } + } + Ok(()) +} + +/// Run `cargo metadata`, find the `solana_parser` package, and return +/// `/src/solana/idls`. +fn locate_idl_dir(manifest_path: &str) -> Result { + let output = Command::new("cargo") + .args([ + "metadata", + "--manifest-path", + manifest_path, + "--format-version", + "1", + ]) + .output() + .context("failed to run `cargo metadata`")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("cargo metadata failed: {stderr}"); + } + + let meta: serde_json::Value = + serde_json::from_slice(&output.stdout).context("failed to parse cargo metadata JSON")?; + + let packages = meta["packages"] + .as_array() + .context("no 'packages' array in cargo metadata")?; + + for pkg in packages { + if pkg["name"].as_str() == Some("solana_parser") { + let manifest = pkg["manifest_path"] + .as_str() + .context("missing manifest_path for solana_parser")?; + let pkg_dir = Path::new(manifest) + .parent() + .context("manifest_path has no parent")?; + let idl_dir = pkg_dir.join("src").join("solana").join("idls"); + if idl_dir.is_dir() { + return Ok(idl_dir); + } + anyhow::bail!( + "solana_parser found at {manifest} but IDL dir does not exist: {}", + idl_dir.display() + ); + } + } + + anyhow::bail!("package 'solana_parser' not found in cargo metadata") +} + +/// Parse an Anchor IDL JSON file and return (instruction_count, type_count). +fn idl_counts(path: &str) -> Result<(usize, usize)> { + let contents = + std::fs::read_to_string(path).with_context(|| format!("failed to read {path}"))?; + let idl: serde_json::Value = + serde_json::from_str(&contents).with_context(|| format!("invalid JSON in {path}"))?; + + let instructions = idl["instructions"].as_array().map_or(0, Vec::len); + let types = idl["types"].as_array().map_or(0, Vec::len); + Ok((instructions, types)) +} From 641ab05232f2f1235dc7b21d0828c5f36cdf0f35 Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Tue, 14 Apr 2026 10:20:30 -0400 Subject: [PATCH 04/15] refactor: replace python3 with idl-meta in fuzz_all_idls.sh --- scripts/fuzz_all_idls.sh | 35 +++++++---------------------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/scripts/fuzz_all_idls.sh b/scripts/fuzz_all_idls.sh index 7a0a123c..8b1b3dad 100755 --- a/scripts/fuzz_all_idls.sh +++ b/scripts/fuzz_all_idls.sh @@ -32,7 +32,7 @@ # PROPTEST_CASES=1000 ./scripts/fuzz_all_idls.sh # ./scripts/fuzz_all_idls.sh /path/to/extra.json ... # append extra IDLs # -# Requirements: cargo, python3 +# Requirements: cargo (builds idl-meta from workspace automatically) set -euo pipefail @@ -42,25 +42,11 @@ CASES="${PROPTEST_CASES:-256}" # ── Locate the solana_parser IDL directory via cargo metadata ───────────────── -IDL_DIR="$(python3 - "$WORKSPACE_TOML" <<'PY' -import json, os, subprocess, sys - -manifest = sys.argv[1] -result = subprocess.run( - ["cargo", "metadata", "--manifest-path", manifest, "--format-version", "1"], - capture_output=True, text=True, check=True, -) -data = json.loads(result.stdout) -for pkg in data["packages"]: - if pkg["name"] == "solana_parser": - idl_dir = os.path.join(os.path.dirname(pkg["manifest_path"]), "src", "solana", "idls") - if os.path.isdir(idl_dir): - print(idl_dir) - sys.exit(0) -print("error: solana_parser IDL directory not found", file=sys.stderr) -sys.exit(1) -PY -)" +# Build the idl-meta tool once (shares the workspace build cache). +cargo build --manifest-path "$WORKSPACE_TOML" -p idl-meta --quiet + +IDL_META="cargo run --manifest-path $WORKSPACE_TOML -p idl-meta --quiet --" +IDL_DIR="$($IDL_META locate-idls --manifest-path "$WORKSPACE_TOML")" # ── Collect IDL files: embedded + any extras passed as arguments ────────────── @@ -93,14 +79,7 @@ for idl_file in "${IDL_FILES[@]}"; do name="$(basename "$idl_file" .json)" # Get instruction/type counts - read -r inst_count type_count < <(python3 -c " -import json, sys -try: - d = json.load(open(sys.argv[1])) - print(len(d.get('instructions', [])), len(d.get('types', []))) -except Exception: - print(0, 0) -" "$idl_file") + read -r inst_count type_count < <($IDL_META counts "$idl_file") printf "%-30s %13s %7s " "$name" "$inst_count" "$type_count" From 491ade0bfd2c2ffc4ac00126e906eba5b68c0c88 Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Tue, 14 Apr 2026 10:22:01 -0400 Subject: [PATCH 05/15] test: add surfpool_fuzz_all_idls.sh for mainnet-fork IDL testing Co-Authored-By: Claude Sonnet 4.6 --- scripts/surfpool_fuzz_all_idls.sh | 85 +++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100755 scripts/surfpool_fuzz_all_idls.sh diff --git a/scripts/surfpool_fuzz_all_idls.sh b/scripts/surfpool_fuzz_all_idls.sh new file mode 100755 index 00000000..61af8d24 --- /dev/null +++ b/scripts/surfpool_fuzz_all_idls.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# surfpool_fuzz_all_idls.sh — run surfpool integration tests against every embedded IDL. +# +# Requires: cargo, surfpool binary +# Optional: HELIUS_API_KEY (for faster RPC), PROPTEST_CASES (default: 32) +# +# Usage: +# ./scripts/surfpool_fuzz_all_idls.sh +# HELIUS_API_KEY= PROPTEST_CASES=64 ./scripts/surfpool_fuzz_all_idls.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKSPACE_TOML="$SCRIPT_DIR/../src/Cargo.toml" +CASES="${PROPTEST_CASES:-32}" + +# Build tools +cargo build --manifest-path "$WORKSPACE_TOML" -p idl-meta --quiet + +IDL_META="cargo run --manifest-path $WORKSPACE_TOML -p idl-meta --quiet --" +IDL_DIR="$($IDL_META locate-idls --manifest-path "$WORKSPACE_TOML")" + +IDL_FILES=("$IDL_DIR"/*.json) + +# Build test binary once +echo "Building surfpool fuzz test binary..." +cargo test \ + --manifest-path "$WORKSPACE_TOML" \ + -p visualsign-solana \ + --test surfpool_fuzz \ + --no-run \ + 2>&1 | grep -E "^( Compiling| Finished|error)" || true +echo "" + +PASS=0 +FAIL=0 +FAILED_IDLS=() + +printf "%-30s %13s %7s %s\n" "IDL" "Instructions" "Types" "Result" +printf "%-30s %13s %7s %s\n" "───────────────────────────" "────────────" "─────" "──────" + +for idl_file in "${IDL_FILES[@]}"; do + name="$(basename "$idl_file" .json)" + read -r inst_count type_count < <($IDL_META counts "$idl_file") + + printf "%-30s %13s %7s " "$name" "$inst_count" "$type_count" + + output=$(IDL_FILE="$idl_file" PROPTEST_CASES="$CASES" \ + cargo test \ + --manifest-path "$WORKSPACE_TOML" \ + -p visualsign-solana \ + --test surfpool_fuzz \ + -- --ignored --quiet \ + 2>&1) + + summary=$(echo "$output" | grep -oE "[0-9]+ passed; [0-9]+ failed" | head -1) + + if [ -z "$summary" ]; then + echo "FAIL (no test result)" + FAIL=$(( FAIL + 1 )) + FAILED_IDLS+=("$name ($idl_file)") + else + failed_count=$(echo "$summary" | grep -oE "[0-9]+ failed" | grep -oE "[0-9]+") + if [ "${failed_count:-0}" -gt 0 ]; then + echo "FAIL ($summary)" + FAIL=$(( FAIL + 1 )) + FAILED_IDLS+=("$name ($idl_file)") + else + echo "PASS ($summary)" + PASS=$(( PASS + 1 )) + fi + fi +done + +echo "" +echo "Results: $PASS passed, $FAIL failed (PROPTEST_CASES=$CASES)" + +if (( FAIL > 0 )); then + echo "" + echo "Failed:" + for entry in "${FAILED_IDLS[@]}"; do + echo " $entry" + done + exit 1 +fi From 613c3b7cdd49889ba34a75292f5c13379d95121e Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Tue, 14 Apr 2026 14:36:07 -0400 Subject: [PATCH 06/15] fix: update SurfpoolManager for current surfpool CLI - Use `surfpool start` subcommand with correct flags (--port, --ws-port, --rpc-url, --no-tui, --ci) instead of obsolete top-level flags - Remove ledger_path/reset_ledger config (not surfpool concepts) - Use multi_thread tokio runtime in tests (required by solana-rpc-client) - All 15 IDLs pass surfpool integration tests Co-Authored-By: Claude Opus 4.6 (1M context) --- .../visualsign-solana/tests/surfpool_fuzz.rs | 4 +- src/solana_test_utils/src/surfpool/config.rs | 52 ++++++++----------- src/solana_test_utils/src/surfpool/manager.rs | 27 +++++----- 3 files changed, 38 insertions(+), 45 deletions(-) diff --git a/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs b/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs index 707dcfc8..39266052 100644 --- a/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs +++ b/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs @@ -23,7 +23,7 @@ use visualsign_solana::{SolanaTransactionWrapper, SolanaVisualSignConverter}; /// Smoke-test: start surfpool, verify the RPC endpoint responds with a /// version string, then let `SurfpoolManager` tear it down on drop. -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] #[ignore] async fn surfpool_lifecycle() { let manager = SurfpoolManager::start(SurfpoolConfig::default()) @@ -45,7 +45,7 @@ async fn surfpool_lifecycle() { /// instruction's discriminator, build a transaction containing those bytes, /// and run it through the visual-sign converter. The converter must return /// `Ok` with at least one field (the instruction line). -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] #[ignore] async fn surfpool_jupiter_swap_roundtrip() { // Skip gracefully when IDL_FILE is not set. diff --git a/src/solana_test_utils/src/surfpool/config.rs b/src/solana_test_utils/src/surfpool/config.rs index f51e4292..ad104303 100644 --- a/src/solana_test_utils/src/surfpool/config.rs +++ b/src/solana_test_utils/src/surfpool/config.rs @@ -1,49 +1,46 @@ -use std::path::PathBuf; - /// Configuration for a Surfpool validator instance. +/// +/// Maps to `surfpool start` CLI flags. See `surfpool start --help` for details. #[derive(Debug, Clone)] pub struct SurfpoolConfig { - /// RPC URL to fork from (e.g., mainnet-beta via Helius). - pub fork_url: Option, - /// Local RPC port (auto-selected if `None`). - pub rpc_port: Option, - /// Local WebSocket port (auto-selected if `None`). + /// Datasource RPC URL to fork from (`-u`/`--rpc-url`). + pub rpc_url: Option, + /// Local Simnet RPC port (`-p`/`--port`). Auto-selected if `None`. + pub port: Option, + /// Local Simnet WebSocket port (`-w`/`--ws-port`). Auto-selected if `None`. pub ws_port: Option, - /// Ledger directory path. - pub ledger_path: Option, - /// Reset ledger on startup. - pub reset_ledger: bool, - /// Log level for the surfpool process. + /// Log level (`-l`/`--log-level`). pub log_level: String, + /// Use CI-adequate settings (`--ci`). + pub ci: bool, } impl Default for SurfpoolConfig { fn default() -> Self { - let fork_url = std::env::var("HELIUS_API_KEY") + let rpc_url = std::env::var("HELIUS_API_KEY") .ok() .map(|key| format!("https://mainnet.helius-rpc.com/?api-key={key}")) .or_else(|| std::env::var("SOLANA_RPC_URL").ok()) .unwrap_or_else(|| "https://api.mainnet-beta.solana.com".to_string()); Self { - fork_url: Some(fork_url), - rpc_port: None, + rpc_url: Some(rpc_url), + port: None, ws_port: None, - ledger_path: None, - reset_ledger: true, log_level: "info".to_string(), + ci: true, } } } impl SurfpoolConfig { - pub fn with_fork_url(mut self, url: impl Into) -> Self { - self.fork_url = Some(url.into()); + pub fn with_rpc_url(mut self, url: impl Into) -> Self { + self.rpc_url = Some(url.into()); self } - pub fn with_rpc_port(mut self, port: u16) -> Self { - self.rpc_port = Some(port); + pub fn with_port(mut self, port: u16) -> Self { + self.port = Some(port); self } @@ -52,18 +49,13 @@ impl SurfpoolConfig { self } - pub fn with_ledger_path(mut self, path: impl Into) -> Self { - self.ledger_path = Some(path.into()); - self - } - - pub fn with_reset_ledger(mut self, reset: bool) -> Self { - self.reset_ledger = reset; + pub fn with_log_level(mut self, level: impl Into) -> Self { + self.log_level = level.into(); self } - pub fn with_log_level(mut self, level: impl Into) -> Self { - self.log_level = level.into(); + pub fn with_ci(mut self, ci: bool) -> Self { + self.ci = ci; self } } diff --git a/src/solana_test_utils/src/surfpool/manager.rs b/src/solana_test_utils/src/surfpool/manager.rs index 953b6c1c..f574fb6b 100644 --- a/src/solana_test_utils/src/surfpool/manager.rs +++ b/src/solana_test_utils/src/surfpool/manager.rs @@ -20,35 +20,36 @@ pub struct SurfpoolManager { impl SurfpoolManager { /// Start a new Surfpool instance with the given configuration. + /// + /// Runs `surfpool start` with `--no-tui` (headless) and the flags derived + /// from [`SurfpoolConfig`]. pub async fn start(config: SurfpoolConfig) -> Result { info!("Starting Surfpool with config: {:?}", config); - let rpc_port = config.rpc_port.map_or_else(Self::find_free_port, Ok)?; + let rpc_port = config.port.map_or_else(Self::find_free_port, Ok)?; let ws_port = config.ws_port.map_or_else(Self::find_free_port, Ok)?; let rpc_url = format!("http://127.0.0.1:{rpc_port}"); let ws_url = format!("ws://127.0.0.1:{ws_port}"); let mut args = vec![ - "--rpc-port".to_string(), + "start".to_string(), + "--no-tui".to_string(), + "--port".to_string(), rpc_port.to_string(), "--ws-port".to_string(), ws_port.to_string(), - "--log".to_string(), + "--log-level".to_string(), + config.log_level.clone(), ]; - if let Some(fork_url) = &config.fork_url { - args.push("--url".to_string()); - args.push(fork_url.clone()); + if let Some(upstream) = &config.rpc_url { + args.push("--rpc-url".to_string()); + args.push(upstream.clone()); } - if let Some(ledger_path) = &config.ledger_path { - args.push("--ledger".to_string()); - args.push(ledger_path.to_string_lossy().to_string()); - } - - if config.reset_ledger { - args.push("--reset".to_string()); + if config.ci { + args.push("--ci".to_string()); } debug!("Spawning surfpool with args: {:?}", args); From 82e3b3bcdc407927c70fd6360b3ce2eb5ab4d07f Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Thu, 7 May 2026 13:18:15 -0400 Subject: [PATCH 07/15] ci: add surfpool fuzz workflow gated on `surfpool` label Wires the existing `HELIUS_API_KEY` repo secret into a label-gated CI job that runs `scripts/surfpool_fuzz_all_idls.sh` against all 13 embedded IDLs. The Solana parser's `SurfpoolConfig::default()` already prefers `HELIUS_API_KEY` over the public mainnet endpoint, so passing the secret through the job env is sufficient -- no code changes required. Modelled on `fuzz-solana.yml` and `proptest-solana.yml`: label-gated to control RPC quota usage, caches `~/.cargo/bin/surfpool` to skip the ~minute-long install on subsequent runs, and tags the PR with `surfpool-failure` on a red run. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/surfpool-solana.yml | 67 +++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/surfpool-solana.yml diff --git a/.github/workflows/surfpool-solana.yml b/.github/workflows/surfpool-solana.yml new file mode 100644 index 00000000..874d0ae8 --- /dev/null +++ b/.github/workflows/surfpool-solana.yml @@ -0,0 +1,67 @@ +name: "Surfpool Fuzz: Solana" + +on: + pull_request: + types: [opened, synchronize, reopened, labeled] + +jobs: + surfpool: + if: contains(github.event.pull_request.labels.*.name, 'surfpool') + runs-on: ubuntu-latest-4-cores + permissions: + pull-requests: write + env: + HELIUS_API_KEY: ${{ secrets.HELIUS_API_KEY }} + PROPTEST_CASES: 32 + steps: + - name: git checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0 + - name: Cache Rust dependencies + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ~/.cargo/bin/surfpool + src/target/ + key: ${{ runner.os }}-cargo-surfpool-${{ hashFiles('src/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-surfpool- + ${{ runner.os }}-cargo- + - name: install protoc + uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0 + with: + version: "21.4" + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: free disk space + run: | + sudo swapoff -a + sudo rm -f /swapfile + sudo apt clean + df -h + - name: Install surfpool + run: | + if ! command -v surfpool >/dev/null 2>&1; then + cargo install surfpool --git https://github.com/txtx/surfpool --locked + fi + - name: Run codegen + run: make -C src generated + - name: Run surfpool fuzz against all embedded IDLs + id: surfpool + continue-on-error: true + run: ./scripts/surfpool_fuzz_all_idls.sh + - name: Label PR on surfpool failure + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + if [ "${{ steps.surfpool.outcome }}" = "failure" ]; then + gh pr edit "$PR_NUMBER" --add-label surfpool-failure || true + else + gh pr edit "$PR_NUMBER" --remove-label surfpool-failure || true + fi From 14d135121a20747da7896e9a0e601f2c8f5fe663 Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Thu, 7 May 2026 13:28:08 -0400 Subject: [PATCH 08/15] ci(surfpool): pin surfpool to v1.1.1, skip fork PRs, harden install gate Addresses code review on the prior commit: 1. Pin surfpool to tag v1.1.1 (matches the version verified locally against the Helius mainnet fork) and key the cached binary on the pinned version. The previous cache used `src/Cargo.lock` hash, which is decoupled from surfpool's upstream and would silently pin CI to the first-cached binary indefinitely. Splitting the binary cache from the workspace cache also lets each invalidate independently. 2. Skip the job on fork PRs (head repo != base repo). Secrets are not passed to fork PRs, so HELIUS_API_KEY would be empty and the labeling step would lack `gh pr edit` permissions; both would silently fall back to flaky public mainnet and a no-op label step. 3. Use `surfpool --version` instead of `command -v surfpool` to gate reinstall. A corrupted cached binary (interrupted previous install, ABI mismatch after toolchain bump) is now detected and rebuilt. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/surfpool-solana.yml | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/.github/workflows/surfpool-solana.yml b/.github/workflows/surfpool-solana.yml index 874d0ae8..e8da69cd 100644 --- a/.github/workflows/surfpool-solana.yml +++ b/.github/workflows/surfpool-solana.yml @@ -4,9 +4,16 @@ on: pull_request: types: [opened, synchronize, reopened, labeled] +env: + SURFPOOL_VERSION: v1.1.1 + jobs: surfpool: - if: contains(github.event.pull_request.labels.*.name, 'surfpool') + # Skip on fork PRs: secrets aren't passed across forks, so HELIUS_API_KEY + # would be empty and `gh pr edit` would lack write permissions. + if: >- + contains(github.event.pull_request.labels.*.name, 'surfpool') && + github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest-4-cores permissions: pull-requests: write @@ -20,6 +27,11 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - name: Install Rust uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0 + - name: Cache surfpool binary + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + with: + path: ~/.cargo/bin/surfpool + key: ${{ runner.os }}-surfpool-bin-${{ env.SURFPOOL_VERSION }} - name: Cache Rust dependencies uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: @@ -27,12 +39,10 @@ jobs: ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ - ~/.cargo/bin/surfpool src/target/ key: ${{ runner.os }}-cargo-surfpool-${{ hashFiles('src/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-surfpool- - ${{ runner.os }}-cargo- - name: install protoc uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0 with: @@ -46,8 +56,14 @@ jobs: df -h - name: Install surfpool run: | - if ! command -v surfpool >/dev/null 2>&1; then - cargo install surfpool --git https://github.com/txtx/surfpool --locked + # `surfpool --version` is a stronger gate than `command -v`: a + # corrupted cached binary (interrupted previous install, ABI mismatch + # with new toolchain) will fail this check and trigger a rebuild. + if ! surfpool --version >/dev/null 2>&1; then + cargo install surfpool \ + --git https://github.com/txtx/surfpool \ + --tag "${SURFPOOL_VERSION}" \ + --locked fi - name: Run codegen run: make -C src generated From e6f0f084f41c8deec749c799e4bf5d9a88cc81ab Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Thu, 7 May 2026 13:54:14 -0400 Subject: [PATCH 09/15] fix(surfpool): unblock Tokio worker, surface test diagnostics, fix CI install Three fixes addressing outstanding review comments and the failing CI: 1. CI install command -- the `surfpool` package was renamed to `surfpool-cli` upstream, so `cargo install surfpool --tag v1.1.1` fails with "could not find `surfpool` ... with version `*`". Drop the package name argument so cargo uses the workspace's `default-members` (`crates/cli`), which produces the `surfpool` binary regardless of future package renames within the workspace. 2. `wait_ready` no longer blocks a Tokio worker. The synchronous `RpcClient::get_version()` is now dispatched via `spawn_blocking` and the inter-attempt delay uses `tokio::time::sleep` instead of `thread::sleep`. Without this, a stalled RPC could starve other tasks on the same multi-thread runtime. 3. `surfpool_fuzz_all_idls.sh` now prints the captured cargo output on failure (both the no-summary and `N failed` paths). Previously a red IDL row printed only `FAIL (...)` and the diagnostics were silently dropped, making CI failures hard to debug. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/surfpool-solana.yml | 7 ++++++- scripts/surfpool_fuzz_all_idls.sh | 2 ++ src/solana_test_utils/src/surfpool/manager.rs | 20 +++++++++++++++---- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.github/workflows/surfpool-solana.yml b/.github/workflows/surfpool-solana.yml index e8da69cd..237114aa 100644 --- a/.github/workflows/surfpool-solana.yml +++ b/.github/workflows/surfpool-solana.yml @@ -59,8 +59,13 @@ jobs: # `surfpool --version` is a stronger gate than `command -v`: a # corrupted cached binary (interrupted previous install, ABI mismatch # with new toolchain) will fail this check and trigger a rebuild. + # + # No package name is passed: cargo uses the workspace's + # `default-members` (currently `crates/cli`, package `surfpool-cli`, + # which produces the `surfpool` binary). This survives upstream + # package renames within the workspace. if ! surfpool --version >/dev/null 2>&1; then - cargo install surfpool \ + cargo install \ --git https://github.com/txtx/surfpool \ --tag "${SURFPOOL_VERSION}" \ --locked diff --git a/scripts/surfpool_fuzz_all_idls.sh b/scripts/surfpool_fuzz_all_idls.sh index 61af8d24..a3013e7b 100755 --- a/scripts/surfpool_fuzz_all_idls.sh +++ b/scripts/surfpool_fuzz_all_idls.sh @@ -57,12 +57,14 @@ for idl_file in "${IDL_FILES[@]}"; do if [ -z "$summary" ]; then echo "FAIL (no test result)" + printf '%s\n' "$output" FAIL=$(( FAIL + 1 )) FAILED_IDLS+=("$name ($idl_file)") else failed_count=$(echo "$summary" | grep -oE "[0-9]+ failed" | grep -oE "[0-9]+") if [ "${failed_count:-0}" -gt 0 ]; then echo "FAIL ($summary)" + printf '%s\n' "$output" FAIL=$(( FAIL + 1 )) FAILED_IDLS+=("$name ($idl_file)") else diff --git a/src/solana_test_utils/src/surfpool/manager.rs b/src/solana_test_utils/src/surfpool/manager.rs index f574fb6b..388163b9 100644 --- a/src/solana_test_utils/src/surfpool/manager.rs +++ b/src/solana_test_utils/src/surfpool/manager.rs @@ -4,7 +4,6 @@ use solana_client::rpc_client::RpcClient; use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Signature}; use std::net::TcpListener; use std::process::{Child, Command}; -use std::thread; use std::time::Duration; use tracing::{debug, info, warn}; @@ -75,17 +74,30 @@ impl SurfpoolManager { } /// Poll the RPC server until it responds (up to 30 attempts, 500ms apart). + /// + /// `RpcClient::get_version` is synchronous and blocks for up to its HTTP + /// timeout. Running it on a Tokio worker thread would stall other tasks, + /// so each probe is dispatched via `spawn_blocking` and the inter-attempt + /// delay uses `tokio::time::sleep`. pub async fn wait_ready(&self) -> Result<()> { - let client = self.rpc_client(); let max_attempts = 30; let delay = Duration::from_millis(500); + let rpc_url = self.rpc_url.clone(); for attempt in 1..=max_attempts { debug!( "Checking if Surfpool is ready (attempt {}/{})", attempt, max_attempts ); - match client.get_version() { + + let url = rpc_url.clone(); + let probe = tokio::task::spawn_blocking(move || { + RpcClient::new_with_commitment(url, CommitmentConfig::confirmed()).get_version() + }) + .await + .context("Surfpool readiness probe task panicked")?; + + match probe { Ok(version) => { info!("Surfpool is ready! Version: {:?}", version); return Ok(()); @@ -97,7 +109,7 @@ impl SurfpoolManager { )); } warn!("Surfpool not ready yet (attempt {}): {}", attempt, e); - thread::sleep(delay); + tokio::time::sleep(delay).await; } } } From 2a1523fe7005cb3dc662d9cbbf921ef8a2a546bf Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Thu, 7 May 2026 14:01:56 -0400 Subject: [PATCH 10/15] ci(surfpool): pin to commit SHA instead of tag Tags are mutable -- a force-push could silently change what we install, and `--locked` only protects transitive deps (it uses the source's Cargo.lock; it does not lock the source commit). Pin to the SHA `v1.1.1` resolved to (d58df4c) so the install target is bit-for-bit reproducible. Cache key now keys on the rev. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/surfpool-solana.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/surfpool-solana.yml b/.github/workflows/surfpool-solana.yml index 237114aa..4d28e792 100644 --- a/.github/workflows/surfpool-solana.yml +++ b/.github/workflows/surfpool-solana.yml @@ -5,7 +5,9 @@ on: types: [opened, synchronize, reopened, labeled] env: - SURFPOOL_VERSION: v1.1.1 + # Pin to an immutable commit SHA, not a tag — git tags can be force-pushed. + # This is the commit `v1.1.1` pointed to at time of pinning. + SURFPOOL_REV: d58df4cd7c3188561e671d8da0401487e3a60762 jobs: surfpool: @@ -31,7 +33,7 @@ jobs: uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: ~/.cargo/bin/surfpool - key: ${{ runner.os }}-surfpool-bin-${{ env.SURFPOOL_VERSION }} + key: ${{ runner.os }}-surfpool-bin-${{ env.SURFPOOL_REV }} - name: Cache Rust dependencies uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: @@ -67,7 +69,7 @@ jobs: if ! surfpool --version >/dev/null 2>&1; then cargo install \ --git https://github.com/txtx/surfpool \ - --tag "${SURFPOOL_VERSION}" \ + --rev "${SURFPOOL_REV}" \ --locked fi - name: Run codegen From 33fc553baff3c6ab497433e86d6df260c98a9473 Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Thu, 7 May 2026 15:08:36 -0400 Subject: [PATCH 11/15] ci(surfpool): cap lints to warn during third-party install The toolchain action exports `RUSTFLAGS: -D warnings` by default, which made the `cargo install` step fail on a benign `unused_parens` lint in upstream `surfpool-gql` v1.1.1. Deny-warnings is the right policy for our own code but wrong for installing a third-party binary at a pinned revision -- whether it compiles cleanly under future rustc is not something we want CI to gate on. Set `RUSTFLAGS=--cap-lints=warn` for just the install step so upstream warnings stay warnings without affecting any subsequent step. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/surfpool-solana.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/surfpool-solana.yml b/.github/workflows/surfpool-solana.yml index 4d28e792..d889b78f 100644 --- a/.github/workflows/surfpool-solana.yml +++ b/.github/workflows/surfpool-solana.yml @@ -57,6 +57,12 @@ jobs: sudo apt clean df -h - name: Install surfpool + # `RUSTFLAGS=--cap-lints=warn` overrides the toolchain action's default + # `-D warnings`. That deny-warnings policy is meant for our own code; + # applying it to a third-party install is fragile (e.g. v1.1.1 of + # surfpool-gql trips a benign `unused_parens` lint on newer rustc). + env: + RUSTFLAGS: --cap-lints=warn run: | # `surfpool --version` is a stronger gate than `command -v`: a # corrupted cached binary (interrupted previous install, ABI mismatch From 771e5ed44acc664326a67b9a4aa8734462c3f73a Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Thu, 7 May 2026 15:12:24 -0400 Subject: [PATCH 12/15] ci(surfpool): use upstream prebuilt binary instead of cargo install Replaces the ~10-minute `cargo install` compile with a ~5-second curl of the upstream release asset. Pins the tarball's SHA256 in the workflow so a force-pushed release asset cannot silently change what we install. The binary is dropped into `~/.local/bin/` (added to `GITHUB_PATH` for subsequent steps). Cache is keyed on (version, sha256) so a checksum bump invalidates automatically. Drops `RUSTFLAGS=--cap-lints=warn` from the install step; that was only needed to work around upstream lint regressions during compile. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/surfpool-solana.yml | 42 ++++++++++++--------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/.github/workflows/surfpool-solana.yml b/.github/workflows/surfpool-solana.yml index d889b78f..27d8ae58 100644 --- a/.github/workflows/surfpool-solana.yml +++ b/.github/workflows/surfpool-solana.yml @@ -5,9 +5,11 @@ on: types: [opened, synchronize, reopened, labeled] env: - # Pin to an immutable commit SHA, not a tag — git tags can be force-pushed. - # This is the commit `v1.1.1` pointed to at time of pinning. - SURFPOOL_REV: d58df4cd7c3188561e671d8da0401487e3a60762 + # Use the upstream prebuilt binary instead of `cargo install` (which is a + # ~10 minute compile). Pin the tarball's SHA256 so a force-pushed release + # asset can't silently change what we install. + SURFPOOL_VERSION: v1.1.1 + SURFPOOL_SHA256: e17b44331ce3baa58fe530a061d6dc6df0e288521fec0bd5892333854ffeecf5 jobs: surfpool: @@ -32,8 +34,8 @@ jobs: - name: Cache surfpool binary uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: - path: ~/.cargo/bin/surfpool - key: ${{ runner.os }}-surfpool-bin-${{ env.SURFPOOL_REV }} + path: ~/.local/bin/surfpool + key: ${{ runner.os }}-surfpool-bin-${{ env.SURFPOOL_VERSION }}-${{ env.SURFPOOL_SHA256 }} - name: Cache Rust dependencies uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: @@ -57,27 +59,21 @@ jobs: sudo apt clean df -h - name: Install surfpool - # `RUSTFLAGS=--cap-lints=warn` overrides the toolchain action's default - # `-D warnings`. That deny-warnings policy is meant for our own code; - # applying it to a third-party install is fragile (e.g. v1.1.1 of - # surfpool-gql trips a benign `unused_parens` lint on newer rustc). - env: - RUSTFLAGS: --cap-lints=warn run: | + mkdir -p "${HOME}/.local/bin" + echo "${HOME}/.local/bin" >> "${GITHUB_PATH}" # `surfpool --version` is a stronger gate than `command -v`: a - # corrupted cached binary (interrupted previous install, ABI mismatch - # with new toolchain) will fail this check and trigger a rebuild. - # - # No package name is passed: cargo uses the workspace's - # `default-members` (currently `crates/cli`, package `surfpool-cli`, - # which produces the `surfpool` binary). This survives upstream - # package renames within the workspace. - if ! surfpool --version >/dev/null 2>&1; then - cargo install \ - --git https://github.com/txtx/surfpool \ - --rev "${SURFPOOL_REV}" \ - --locked + # corrupted cached binary will fail this check and trigger redownload. + if "${HOME}/.local/bin/surfpool" --version >/dev/null 2>&1; then + exit 0 fi + tarball="$(mktemp)" + curl -fsSL -o "${tarball}" \ + "https://github.com/txtx/surfpool/releases/download/${SURFPOOL_VERSION}/surfpool-linux-x64.tar.gz" + echo "${SURFPOOL_SHA256} ${tarball}" | sha256sum -c - + tar xzf "${tarball}" -C "${HOME}/.local/bin" + rm -f "${tarball}" + "${HOME}/.local/bin/surfpool" --version - name: Run codegen run: make -C src generated - name: Run surfpool fuzz against all embedded IDLs From 3daa36184aa2ebdd5f0b735e207962412b9449f9 Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Fri, 8 May 2026 00:03:08 -0400 Subject: [PATCH 13/15] ci(surfpool): guard against label-event re-trigger loops The workflow toggles a `surfpool-failure` label on PRs. With the prior `if: contains(labels, 'surfpool')` guard, that toggle re-fires the workflow on the same PR (since the `surfpool` label is still present when the `surfpool-failure` label is added). Any other `labeled` event on a PR that happens to carry the `surfpool` label would also re-trigger unnecessarily. Match the pattern used in `.github/workflows/stagex.yml`: on `labeled` events only run when the label being added is `surfpool`; on other events (`opened`, `synchronize`, `reopened`) require the label to already be present. Addresses copilot review comment on #253 (which proposed the same guard); folded here so #253 can be closed with the workflow's review comments resolved on this PR. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/surfpool-solana.yml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/surfpool-solana.yml b/.github/workflows/surfpool-solana.yml index 27d8ae58..11b49b3b 100644 --- a/.github/workflows/surfpool-solana.yml +++ b/.github/workflows/surfpool-solana.yml @@ -13,11 +13,20 @@ env: jobs: surfpool: - # Skip on fork PRs: secrets aren't passed across forks, so HELIUS_API_KEY - # would be empty and `gh pr edit` would lack write permissions. - if: >- - contains(github.event.pull_request.labels.*.name, 'surfpool') && - github.event.pull_request.head.repo.full_name == github.repository + # Two guards: + # 1. On `labeled` events only run when the label being added is `surfpool`. + # Without this, the workflow's own `surfpool-failure` add (and any other + # label change on a PR that happens to already carry `surfpool`) would + # re-fire the workflow. + # 2. Skip fork PRs: secrets aren't passed across forks, so HELIUS_API_KEY + # would be empty and `gh pr edit` would lack write permissions. + if: | + ( + (github.event.action == 'labeled' && github.event.label.name == 'surfpool') + || + (github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'surfpool')) + ) + && github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest-4-cores permissions: pull-requests: write From d853ace8b2d548ddd2247a4f0aeb99360f689096 Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Thu, 7 May 2026 16:28:38 -0400 Subject: [PATCH 14/15] test(surfpool): native cargo runner via build.rs + per-IDL macro Replaces the bash runner (`scripts/surfpool_fuzz_all_idls.sh`) with a fully native Rust setup: - `build.rs` runs `cargo metadata` once, locates `solana_parser`'s IDL directory (matching the rev in `Cargo.lock`), and emits it as `cargo:rustc-env=SOLANA_IDL_DIR`. Tests pick it up via `env!()` with no runtime cargo invocation. - A `macro_rules!` macro generates one `#[tokio::test]` per IDL (`surfpool_idl_`). Cargo's harness handles enumeration, parallelism, pass/fail counts, and per-test failure output -- the things the bash script was reimplementing by hand. - `collision.json` and `cyclic.json` are excluded: they're solana_parser's own negative test fixtures (duplicate type names / cyclic type refs) and are intentionally rejected by `decode_idl_data`. - The surfpool test now distinguishes the three failure modes (decode rejected, no instructions, missing discriminator) with messages that name the IDL file -- previously a malformed IDL produced a misleading "no discriminator" panic. CI now invokes `cargo test ... --test surfpool_fuzz -- --ignored --test-threads=1` directly. The 14 tests run serially in ~9s end-to-end on the local Helius pin. Documents the pattern in `CLAUDE.md`'s Testing Patterns section. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/surfpool-solana.yml | 7 +- CLAUDE.md | 1 + scripts/surfpool_fuzz_all_idls.sh | 87 ------------- .../visualsign-solana/Cargo.toml | 3 + src/chain_parsers/visualsign-solana/build.rs | 51 +++++++- .../visualsign-solana/tests/surfpool_fuzz.rs | 114 ++++++++++++------ 6 files changed, 134 insertions(+), 129 deletions(-) delete mode 100755 scripts/surfpool_fuzz_all_idls.sh diff --git a/.github/workflows/surfpool-solana.yml b/.github/workflows/surfpool-solana.yml index 11b49b3b..92d5292c 100644 --- a/.github/workflows/surfpool-solana.yml +++ b/.github/workflows/surfpool-solana.yml @@ -86,9 +86,14 @@ jobs: - name: Run codegen run: make -C src generated - name: Run surfpool fuzz against all embedded IDLs + # Tests are gated by `#[ignore]` so they only run on `--ignored`. + # `--test-threads=1` serialises the surfpool spawns so each test owns + # its mainnet fork; otherwise 14 surfpools start simultaneously and + # contend for ports / RPC quota. id: surfpool continue-on-error: true - run: ./scripts/surfpool_fuzz_all_idls.sh + working-directory: src + run: cargo test -p visualsign-solana --test surfpool_fuzz -- --ignored --test-threads=1 - name: Label PR on surfpool failure env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CLAUDE.md b/CLAUDE.md index 4277fdd3..c5296a0a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,6 +74,7 @@ Raw tx bytes → ChainPlugin (CLI) or gRPC request - Integration tests in `integration/tests/` use gRPC client against built binaries - `test_utils` module in `visualsign` provides shared test helpers - Place all `use` imports at the top of the test module, not inside individual test functions +- Surfpool-backed integration tests (`visualsign-solana/tests/surfpool_fuzz.rs`) are `#[ignore]` and require the `surfpool` binary on `$PATH`. The IDL directory is resolved at build time via `SOLANA_IDL_DIR` (set by `chain_parsers/visualsign-solana/build.rs`), so each `idl_test!(...)` invocation expands into one `#[tokio::test]` discoverable by cargo. Run with `HELIUS_API_KEY= cargo test -p visualsign-solana --test surfpool_fuzz -- --ignored --test-threads=1`. Adding a new IDL: drop the JSON into the upstream `solana_parser` IDL directory and add an `idl_test!(name, "file.json")` line. ### Local Dev Container diff --git a/scripts/surfpool_fuzz_all_idls.sh b/scripts/surfpool_fuzz_all_idls.sh deleted file mode 100755 index a3013e7b..00000000 --- a/scripts/surfpool_fuzz_all_idls.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env bash -# surfpool_fuzz_all_idls.sh — run surfpool integration tests against every embedded IDL. -# -# Requires: cargo, surfpool binary -# Optional: HELIUS_API_KEY (for faster RPC), PROPTEST_CASES (default: 32) -# -# Usage: -# ./scripts/surfpool_fuzz_all_idls.sh -# HELIUS_API_KEY= PROPTEST_CASES=64 ./scripts/surfpool_fuzz_all_idls.sh - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -WORKSPACE_TOML="$SCRIPT_DIR/../src/Cargo.toml" -CASES="${PROPTEST_CASES:-32}" - -# Build tools -cargo build --manifest-path "$WORKSPACE_TOML" -p idl-meta --quiet - -IDL_META="cargo run --manifest-path $WORKSPACE_TOML -p idl-meta --quiet --" -IDL_DIR="$($IDL_META locate-idls --manifest-path "$WORKSPACE_TOML")" - -IDL_FILES=("$IDL_DIR"/*.json) - -# Build test binary once -echo "Building surfpool fuzz test binary..." -cargo test \ - --manifest-path "$WORKSPACE_TOML" \ - -p visualsign-solana \ - --test surfpool_fuzz \ - --no-run \ - 2>&1 | grep -E "^( Compiling| Finished|error)" || true -echo "" - -PASS=0 -FAIL=0 -FAILED_IDLS=() - -printf "%-30s %13s %7s %s\n" "IDL" "Instructions" "Types" "Result" -printf "%-30s %13s %7s %s\n" "───────────────────────────" "────────────" "─────" "──────" - -for idl_file in "${IDL_FILES[@]}"; do - name="$(basename "$idl_file" .json)" - read -r inst_count type_count < <($IDL_META counts "$idl_file") - - printf "%-30s %13s %7s " "$name" "$inst_count" "$type_count" - - output=$(IDL_FILE="$idl_file" PROPTEST_CASES="$CASES" \ - cargo test \ - --manifest-path "$WORKSPACE_TOML" \ - -p visualsign-solana \ - --test surfpool_fuzz \ - -- --ignored --quiet \ - 2>&1) - - summary=$(echo "$output" | grep -oE "[0-9]+ passed; [0-9]+ failed" | head -1) - - if [ -z "$summary" ]; then - echo "FAIL (no test result)" - printf '%s\n' "$output" - FAIL=$(( FAIL + 1 )) - FAILED_IDLS+=("$name ($idl_file)") - else - failed_count=$(echo "$summary" | grep -oE "[0-9]+ failed" | grep -oE "[0-9]+") - if [ "${failed_count:-0}" -gt 0 ]; then - echo "FAIL ($summary)" - printf '%s\n' "$output" - FAIL=$(( FAIL + 1 )) - FAILED_IDLS+=("$name ($idl_file)") - else - echo "PASS ($summary)" - PASS=$(( PASS + 1 )) - fi - fi -done - -echo "" -echo "Results: $PASS passed, $FAIL failed (PROPTEST_CASES=$CASES)" - -if (( FAIL > 0 )); then - echo "" - echo "Failed:" - for entry in "${FAILED_IDLS[@]}"; do - echo " $entry" - done - exit 1 -fi diff --git a/src/chain_parsers/visualsign-solana/Cargo.toml b/src/chain_parsers/visualsign-solana/Cargo.toml index 25c082ea..6a708bb5 100644 --- a/src/chain_parsers/visualsign-solana/Cargo.toml +++ b/src/chain_parsers/visualsign-solana/Cargo.toml @@ -24,6 +24,9 @@ solana-system-interface = "1.0" spl-token-2022 = "10.0.0" spl-token-2022-interface = "2.1.0" +[build-dependencies] +serde_json = { workspace = true } + [dev-dependencies] solana-parser-fuzz-core = { git = "https://github.com/anchorageoss/solana-parser.git", rev = "a0c554d", features = ["proptest"] } solana_test_utils = { path = "../../solana_test_utils" } diff --git a/src/chain_parsers/visualsign-solana/build.rs b/src/chain_parsers/visualsign-solana/build.rs index 516c1f8c..48843d5d 100644 --- a/src/chain_parsers/visualsign-solana/build.rs +++ b/src/chain_parsers/visualsign-solana/build.rs @@ -1,13 +1,15 @@ // Build scripts run at compile time — panicking on failure is acceptable. -#![allow(clippy::unwrap_used)] +#![allow(clippy::unwrap_used, clippy::expect_used)] -use std::{env, fs, path::PathBuf}; +use std::{env, fs, path::PathBuf, process::Command}; fn main() { println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=src/presets"); println!("cargo:rerun-if-changed=src/integrations"); + emit_solana_idl_dir(); + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); let visualizers = collect_visualizers(); @@ -26,6 +28,51 @@ fn main() { fs::write(out_dir.join("generated_visualizers.rs"), code).unwrap(); } +/// Resolve the `solana_parser` package's IDL directory at compile time and +/// expose it to the test code as `env!("SOLANA_IDL_DIR")`. Runs `cargo +/// metadata` once per build and pins the path that matches the Cargo.lock'd +/// revision of `solana_parser`. Avoids forcing tests to invoke `cargo` at +/// runtime, and keeps the test binary working when its CWD changes. +fn emit_solana_idl_dir() { + let manifest_path = format!("{}/Cargo.toml", env::var("CARGO_MANIFEST_DIR").unwrap()); + let output = Command::new(env::var("CARGO").unwrap_or_else(|_| "cargo".into())) + .args([ + "metadata", + "--manifest-path", + &manifest_path, + "--format-version", + "1", + ]) + .output() + .expect("failed to run cargo metadata for solana_parser IDL discovery"); + assert!( + output.status.success(), + "cargo metadata failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let meta: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("invalid cargo metadata JSON"); + let pkg = meta["packages"] + .as_array() + .expect("packages array") + .iter() + .find(|p| p["name"].as_str() == Some("solana_parser")) + .expect("solana_parser package not found"); + let manifest = pkg["manifest_path"].as_str().expect("manifest_path"); + let pkg_dir = std::path::Path::new(manifest).parent().expect("pkg dir"); + let idl_dir = pkg_dir.join("src").join("solana").join("idls"); + assert!( + idl_dir.is_dir(), + "solana_parser IDL dir not present: {}", + idl_dir.display() + ); + println!("cargo:rustc-env=SOLANA_IDL_DIR={}", idl_dir.display()); + // Cargo doesn't track git-dep checkout dirs as inputs; tag the manifest as + // the rerun trigger (changes whenever solana_parser's rev bumps). + println!("cargo:rerun-if-changed={manifest}"); +} + fn collect_visualizers() -> Vec { let all_visualizers: Vec<(String, String)> = [ ("src/presets", "crate::presets"), diff --git a/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs b/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs index 39266052..69d0564a 100644 --- a/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs +++ b/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs @@ -1,28 +1,41 @@ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] //! Surfpool-backed integration tests for the Solana visual-sign parser. //! -//! These tests start a **surfpool** mainnet fork (requires the `surfpool` -//! binary on `$PATH` and network access) and exercise the parser against -//! transactions built from real on-chain state. +//! Tests are network-bound (start a `surfpool` mainnet fork; require the +//! `surfpool` binary on `$PATH`) and are therefore `#[ignore]`. The IDL +//! directory is resolved at build time via the `SOLANA_IDL_DIR` env var +//! emitted by `build.rs`, so the test binary needs no runtime cargo lookup. //! -//! All tests are `#[ignore]` — run them explicitly: +//! Run all surfpool tests: //! //! ```bash -//! cargo test -p visualsign-solana --test surfpool_fuzz -- --ignored +//! HELIUS_API_KEY= cargo test \ +//! --manifest-path src/Cargo.toml -p visualsign-solana \ +//! --test surfpool_fuzz -- --ignored --test-threads=1 //! ``` +//! +//! Run a single IDL: +//! +//! ```bash +//! cargo test ... --test surfpool_fuzz surfpool_idl_jupiter -- --ignored +//! ``` +//! +//! Adding a new IDL: drop the `.json` file into `solana_parser/src/solana/idls` +//! and add an `idl_test!(...)` line below; cargo's harness picks it up. mod common; -use common::{build_disc_data, build_transaction, load_idl_from_env, options_with_idl}; +use common::{build_transaction, options_with_idl}; +use solana_parser::decode_idl_data; use solana_sdk::pubkey::Pubkey; use solana_test_utils::{SurfpoolConfig, SurfpoolManager}; +use std::path::PathBuf; use visualsign::vsptrait::{Transaction, VisualSignConverter}; use visualsign_solana::{SolanaTransactionWrapper, SolanaVisualSignConverter}; -// ── Tests ──────────────────────────────────────────────────────────────────── +const IDL_DIR: &str = env!("SOLANA_IDL_DIR"); -/// Smoke-test: start surfpool, verify the RPC endpoint responds with a -/// version string, then let `SurfpoolManager` tear it down on drop. +/// Smoke test: start surfpool, verify the RPC responds, let `Drop` tear it down. #[tokio::test(flavor = "multi_thread")] #[ignore] async fn surfpool_lifecycle() { @@ -41,48 +54,43 @@ async fn surfpool_lifecycle() { ); } -/// End-to-end: load a real IDL from `IDL_FILE`, extract the first -/// instruction's discriminator, build a transaction containing those bytes, -/// and run it through the visual-sign converter. The converter must return -/// `Ok` with at least one field (the instruction line). -#[tokio::test(flavor = "multi_thread")] -#[ignore] -async fn surfpool_jupiter_swap_roundtrip() { - // Skip gracefully when IDL_FILE is not set. - let (idl_json, _idl) = match load_idl_from_env() { - Some(pair) => pair, - None => { - eprintln!("IDL_FILE not set or invalid -- skipping surfpool_jupiter_swap_roundtrip"); - return; - } - }; +/// Per-IDL roundtrip: load the IDL, build a synthetic transaction whose data +/// starts with the first instruction's discriminator, run it through the +/// visual-sign converter, and assert the payload is non-empty. +async fn run_idl_roundtrip(idl_filename: &str) { + let idl_path = PathBuf::from(IDL_DIR).join(idl_filename); + let idl_json = std::fs::read_to_string(&idl_path) + .unwrap_or_else(|e| panic!("read {}: {e}", idl_path.display())); + + // Distinguish the three failure modes explicitly so a red test names the + // IDL file and the actual cause (decode rejection from a malformed IDL, + // empty instruction list, or a missing discriminator). + let idl = decode_idl_data(&idl_json) + .unwrap_or_else(|e| panic!("{idl_filename}: decode_idl_data rejected the IDL: {e}")); + assert!( + !idl.instructions.is_empty(), + "{idl_filename}: IDL has no instructions" + ); + let disc = idl.instructions[0] + .discriminator + .as_ref() + .unwrap_or_else(|| panic!("{idl_filename}: instructions[0] has no discriminator")); + let mut data = disc.clone(); + data.extend_from_slice(&[0u8; 32]); - // Start surfpool (validates that a local fork is healthy). let _manager = SurfpoolManager::start(SurfpoolConfig::default()) .await .expect("surfpool should start"); - // Build instruction data using the first instruction's discriminator. - let inst_idx = 0; - let arg_bytes: &[u8] = &[0u8; 32]; // arbitrary argument padding - let (_parsed_idl, data) = build_disc_data(&idl_json, inst_idx, arg_bytes) - .expect("IDL should have at least one instruction with a discriminator"); - - // Use a unique program ID for the synthetic transaction. - let program_name = "test_program"; let program_id = Pubkey::new_unique(); - let tx = build_transaction(program_id, vec![Pubkey::new_unique()], data); - - // Serialize the transaction to base64 so we can round-trip through from_string. - let tx_bytes = bincode::serialize(&tx).expect("transaction should serialize"); + let tx_bytes = bincode::serialize(&tx).expect("tx should serialize"); let tx_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &tx_bytes); let wrapper = SolanaTransactionWrapper::from_string(&tx_b64) .expect("from_string should succeed for a valid base64 transaction"); - let options = options_with_idl(&program_id, &idl_json, program_name); - + let options = options_with_idl(&program_id, &idl_json, "test_program"); let payload = SolanaVisualSignConverter .to_visual_sign_payload(wrapper, options) .expect("converter should succeed"); @@ -92,3 +100,31 @@ async fn surfpool_jupiter_swap_roundtrip() { "payload must contain at least one field" ); } + +macro_rules! idl_test { + ($name:ident, $file:literal) => { + #[tokio::test(flavor = "multi_thread")] + #[ignore] + async fn $name() { + run_idl_roundtrip($file).await; + } + }; +} + +// `collision.json` and `cyclic.json` are solana_parser's negative test fixtures +// (duplicate type names / cyclic type refs); they're rejected by +// `decode_idl_data` and therefore excluded from this positive-path suite. + +idl_test!(surfpool_idl_ape_pro, "ape_pro.json"); +idl_test!(surfpool_idl_cndy, "cndy.json"); +idl_test!(surfpool_idl_drift, "drift.json"); +idl_test!(surfpool_idl_jupiter, "jupiter.json"); +idl_test!(surfpool_idl_jupiter_agg_v6, "jupiter_agg_v6.json"); +idl_test!(surfpool_idl_jupiter_limit, "jupiter_limit.json"); +idl_test!(surfpool_idl_kamino, "kamino.json"); +idl_test!(surfpool_idl_lifinity, "lifinity.json"); +idl_test!(surfpool_idl_meteora, "meteora.json"); +idl_test!(surfpool_idl_openbook, "openbook.json"); +idl_test!(surfpool_idl_orca, "orca.json"); +idl_test!(surfpool_idl_raydium, "raydium.json"); +idl_test!(surfpool_idl_stabble, "stabble.json"); From c89ad3ed385dc1636c8753ed7c2522420e14180a Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Thu, 7 May 2026 23:40:08 -0400 Subject: [PATCH 15/15] test(surfpool): consume embedded_idls consts directly, drop build-time IDL lookup `solana_parser` already exposes each IDL as a `pub const &str` via `solana::embedded_idls::*` (compiled in via `include_str!`). Use those consts directly in the per-IDL macro instead of resolving a filesystem path at build time. Net deletions: - `build.rs::emit_solana_idl_dir()` (the `cargo metadata` invocation) - `[build-dependencies] serde_json` from `visualsign-solana/Cargo.toml` - `env!("SOLANA_IDL_DIR")` and the `std::fs::read_to_string` per test Resolves the duplicated locate-IDL-dir logic the code review flagged between `tools/idl-meta::locate_idl_dir` and `build.rs::emit_solana_idl_dir`. 14 tests pass locally in ~9s (`HELIUS_API_KEY= cargo test ... -- --ignored --test-threads=1`). Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 2 +- .../visualsign-solana/Cargo.toml | 3 - src/chain_parsers/visualsign-solana/build.rs | 51 +----------- .../visualsign-solana/tests/surfpool_fuzz.rs | 80 +++++++++---------- 4 files changed, 43 insertions(+), 93 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c5296a0a..1880f716 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,7 +74,7 @@ Raw tx bytes → ChainPlugin (CLI) or gRPC request - Integration tests in `integration/tests/` use gRPC client against built binaries - `test_utils` module in `visualsign` provides shared test helpers - Place all `use` imports at the top of the test module, not inside individual test functions -- Surfpool-backed integration tests (`visualsign-solana/tests/surfpool_fuzz.rs`) are `#[ignore]` and require the `surfpool` binary on `$PATH`. The IDL directory is resolved at build time via `SOLANA_IDL_DIR` (set by `chain_parsers/visualsign-solana/build.rs`), so each `idl_test!(...)` invocation expands into one `#[tokio::test]` discoverable by cargo. Run with `HELIUS_API_KEY= cargo test -p visualsign-solana --test surfpool_fuzz -- --ignored --test-threads=1`. Adding a new IDL: drop the JSON into the upstream `solana_parser` IDL directory and add an `idl_test!(name, "file.json")` line. +- Surfpool-backed integration tests (`visualsign-solana/tests/surfpool_fuzz.rs`) are `#[ignore]` and require the `surfpool` binary on `$PATH`. Each `idl_test!(...)` invocation references a `pub const` from `solana_parser::solana::embedded_idls`, so IDL contents are baked in at compile time and the macro expands into one `#[tokio::test]` discoverable by cargo. Run with `HELIUS_API_KEY= cargo test -p visualsign-solana --test surfpool_fuzz -- --ignored --test-threads=1`. Adding a new IDL: once it's exposed as a `pub const` upstream in `solana_parser`, add an `idl_test!(name, CONST)` line. ### Local Dev Container diff --git a/src/chain_parsers/visualsign-solana/Cargo.toml b/src/chain_parsers/visualsign-solana/Cargo.toml index 6a708bb5..25c082ea 100644 --- a/src/chain_parsers/visualsign-solana/Cargo.toml +++ b/src/chain_parsers/visualsign-solana/Cargo.toml @@ -24,9 +24,6 @@ solana-system-interface = "1.0" spl-token-2022 = "10.0.0" spl-token-2022-interface = "2.1.0" -[build-dependencies] -serde_json = { workspace = true } - [dev-dependencies] solana-parser-fuzz-core = { git = "https://github.com/anchorageoss/solana-parser.git", rev = "a0c554d", features = ["proptest"] } solana_test_utils = { path = "../../solana_test_utils" } diff --git a/src/chain_parsers/visualsign-solana/build.rs b/src/chain_parsers/visualsign-solana/build.rs index 48843d5d..516c1f8c 100644 --- a/src/chain_parsers/visualsign-solana/build.rs +++ b/src/chain_parsers/visualsign-solana/build.rs @@ -1,15 +1,13 @@ // Build scripts run at compile time — panicking on failure is acceptable. -#![allow(clippy::unwrap_used, clippy::expect_used)] +#![allow(clippy::unwrap_used)] -use std::{env, fs, path::PathBuf, process::Command}; +use std::{env, fs, path::PathBuf}; fn main() { println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=src/presets"); println!("cargo:rerun-if-changed=src/integrations"); - emit_solana_idl_dir(); - let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); let visualizers = collect_visualizers(); @@ -28,51 +26,6 @@ fn main() { fs::write(out_dir.join("generated_visualizers.rs"), code).unwrap(); } -/// Resolve the `solana_parser` package's IDL directory at compile time and -/// expose it to the test code as `env!("SOLANA_IDL_DIR")`. Runs `cargo -/// metadata` once per build and pins the path that matches the Cargo.lock'd -/// revision of `solana_parser`. Avoids forcing tests to invoke `cargo` at -/// runtime, and keeps the test binary working when its CWD changes. -fn emit_solana_idl_dir() { - let manifest_path = format!("{}/Cargo.toml", env::var("CARGO_MANIFEST_DIR").unwrap()); - let output = Command::new(env::var("CARGO").unwrap_or_else(|_| "cargo".into())) - .args([ - "metadata", - "--manifest-path", - &manifest_path, - "--format-version", - "1", - ]) - .output() - .expect("failed to run cargo metadata for solana_parser IDL discovery"); - assert!( - output.status.success(), - "cargo metadata failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - - let meta: serde_json::Value = - serde_json::from_slice(&output.stdout).expect("invalid cargo metadata JSON"); - let pkg = meta["packages"] - .as_array() - .expect("packages array") - .iter() - .find(|p| p["name"].as_str() == Some("solana_parser")) - .expect("solana_parser package not found"); - let manifest = pkg["manifest_path"].as_str().expect("manifest_path"); - let pkg_dir = std::path::Path::new(manifest).parent().expect("pkg dir"); - let idl_dir = pkg_dir.join("src").join("solana").join("idls"); - assert!( - idl_dir.is_dir(), - "solana_parser IDL dir not present: {}", - idl_dir.display() - ); - println!("cargo:rustc-env=SOLANA_IDL_DIR={}", idl_dir.display()); - // Cargo doesn't track git-dep checkout dirs as inputs; tag the manifest as - // the rerun trigger (changes whenever solana_parser's rev bumps). - println!("cargo:rerun-if-changed={manifest}"); -} - fn collect_visualizers() -> Vec { let all_visualizers: Vec<(String, String)> = [ ("src/presets", "crate::presets"), diff --git a/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs b/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs index 69d0564a..2a7147bb 100644 --- a/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs +++ b/src/chain_parsers/visualsign-solana/tests/surfpool_fuzz.rs @@ -2,9 +2,10 @@ //! Surfpool-backed integration tests for the Solana visual-sign parser. //! //! Tests are network-bound (start a `surfpool` mainnet fork; require the -//! `surfpool` binary on `$PATH`) and are therefore `#[ignore]`. The IDL -//! directory is resolved at build time via the `SOLANA_IDL_DIR` env var -//! emitted by `build.rs`, so the test binary needs no runtime cargo lookup. +//! `surfpool` binary on `$PATH`) and are therefore `#[ignore]`. Each test +//! references a `solana_parser::solana::embedded_idls::*` const directly, +//! so the IDL contents are baked in at compile time -- no filesystem +//! lookup, no env var, no `cargo metadata`. //! //! Run all surfpool tests: //! @@ -20,21 +21,23 @@ //! cargo test ... --test surfpool_fuzz surfpool_idl_jupiter -- --ignored //! ``` //! -//! Adding a new IDL: drop the `.json` file into `solana_parser/src/solana/idls` -//! and add an `idl_test!(...)` line below; cargo's harness picks it up. +//! Adding a new IDL: once it's exposed as a `pub const` in +//! `solana_parser::solana::embedded_idls`, add an `idl_test!(name, CONST)` +//! line below; cargo's harness picks it up. mod common; use common::{build_transaction, options_with_idl}; use solana_parser::decode_idl_data; +use solana_parser::solana::embedded_idls::{ + APE_PRO_IDL, CANDY_MACHINE_IDL, DRIFT_IDL, JUPITER_AGG_V6_IDL, JUPITER_IDL, JUPITER_LIMIT_IDL, + KAMINO_IDL, LIFINITY_IDL, METEORA_IDL, OPENBOOK_IDL, ORCA_IDL, RAYDIUM_IDL, STABBLE_IDL, +}; use solana_sdk::pubkey::Pubkey; use solana_test_utils::{SurfpoolConfig, SurfpoolManager}; -use std::path::PathBuf; use visualsign::vsptrait::{Transaction, VisualSignConverter}; use visualsign_solana::{SolanaTransactionWrapper, SolanaVisualSignConverter}; -const IDL_DIR: &str = env!("SOLANA_IDL_DIR"); - /// Smoke test: start surfpool, verify the RPC responds, let `Drop` tear it down. #[tokio::test(flavor = "multi_thread")] #[ignore] @@ -54,27 +57,23 @@ async fn surfpool_lifecycle() { ); } -/// Per-IDL roundtrip: load the IDL, build a synthetic transaction whose data +/// Per-IDL roundtrip: decode the IDL, build a synthetic transaction whose data /// starts with the first instruction's discriminator, run it through the /// visual-sign converter, and assert the payload is non-empty. -async fn run_idl_roundtrip(idl_filename: &str) { - let idl_path = PathBuf::from(IDL_DIR).join(idl_filename); - let idl_json = std::fs::read_to_string(&idl_path) - .unwrap_or_else(|e| panic!("read {}: {e}", idl_path.display())); - +async fn run_idl_roundtrip(idl_label: &str, idl_json: &str) { // Distinguish the three failure modes explicitly so a red test names the - // IDL file and the actual cause (decode rejection from a malformed IDL, - // empty instruction list, or a missing discriminator). - let idl = decode_idl_data(&idl_json) - .unwrap_or_else(|e| panic!("{idl_filename}: decode_idl_data rejected the IDL: {e}")); + // IDL and the actual cause (decode rejection from a malformed IDL, empty + // instruction list, or a missing discriminator). + let idl = decode_idl_data(idl_json) + .unwrap_or_else(|e| panic!("{idl_label}: decode_idl_data rejected the IDL: {e}")); assert!( !idl.instructions.is_empty(), - "{idl_filename}: IDL has no instructions" + "{idl_label}: IDL has no instructions" ); let disc = idl.instructions[0] .discriminator .as_ref() - .unwrap_or_else(|| panic!("{idl_filename}: instructions[0] has no discriminator")); + .unwrap_or_else(|| panic!("{idl_label}: instructions[0] has no discriminator")); let mut data = disc.clone(); data.extend_from_slice(&[0u8; 32]); @@ -90,7 +89,7 @@ async fn run_idl_roundtrip(idl_filename: &str) { let wrapper = SolanaTransactionWrapper::from_string(&tx_b64) .expect("from_string should succeed for a valid base64 transaction"); - let options = options_with_idl(&program_id, &idl_json, "test_program"); + let options = options_with_idl(&program_id, idl_json, "test_program"); let payload = SolanaVisualSignConverter .to_visual_sign_payload(wrapper, options) .expect("converter should succeed"); @@ -102,29 +101,30 @@ async fn run_idl_roundtrip(idl_filename: &str) { } macro_rules! idl_test { - ($name:ident, $file:literal) => { + ($name:ident, $idl:expr) => { #[tokio::test(flavor = "multi_thread")] #[ignore] async fn $name() { - run_idl_roundtrip($file).await; + run_idl_roundtrip(stringify!($name), $idl).await; } }; } -// `collision.json` and `cyclic.json` are solana_parser's negative test fixtures -// (duplicate type names / cyclic type refs); they're rejected by -// `decode_idl_data` and therefore excluded from this positive-path suite. - -idl_test!(surfpool_idl_ape_pro, "ape_pro.json"); -idl_test!(surfpool_idl_cndy, "cndy.json"); -idl_test!(surfpool_idl_drift, "drift.json"); -idl_test!(surfpool_idl_jupiter, "jupiter.json"); -idl_test!(surfpool_idl_jupiter_agg_v6, "jupiter_agg_v6.json"); -idl_test!(surfpool_idl_jupiter_limit, "jupiter_limit.json"); -idl_test!(surfpool_idl_kamino, "kamino.json"); -idl_test!(surfpool_idl_lifinity, "lifinity.json"); -idl_test!(surfpool_idl_meteora, "meteora.json"); -idl_test!(surfpool_idl_openbook, "openbook.json"); -idl_test!(surfpool_idl_orca, "orca.json"); -idl_test!(surfpool_idl_raydium, "raydium.json"); -idl_test!(surfpool_idl_stabble, "stabble.json"); +// `collision.json` and `cyclic.json` exist in `solana_parser`'s `idls/` +// directory but are negative test fixtures (duplicate type names / cyclic +// type refs); they're rejected by `decode_idl_data` and therefore not +// exposed via `embedded_idls`. + +idl_test!(surfpool_idl_ape_pro, APE_PRO_IDL); +idl_test!(surfpool_idl_cndy, CANDY_MACHINE_IDL); +idl_test!(surfpool_idl_drift, DRIFT_IDL); +idl_test!(surfpool_idl_jupiter, JUPITER_IDL); +idl_test!(surfpool_idl_jupiter_agg_v6, JUPITER_AGG_V6_IDL); +idl_test!(surfpool_idl_jupiter_limit, JUPITER_LIMIT_IDL); +idl_test!(surfpool_idl_kamino, KAMINO_IDL); +idl_test!(surfpool_idl_lifinity, LIFINITY_IDL); +idl_test!(surfpool_idl_meteora, METEORA_IDL); +idl_test!(surfpool_idl_openbook, OPENBOOK_IDL); +idl_test!(surfpool_idl_orca, ORCA_IDL); +idl_test!(surfpool_idl_raydium, RAYDIUM_IDL); +idl_test!(surfpool_idl_stabble, STABBLE_IDL);