diff --git a/Cargo.lock b/Cargo.lock index cf6ba7c30..0415f6970 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static 1.5.0", + "regex", +] + [[package]] name = "addr2line" version = "0.25.1" @@ -61,6 +71,35 @@ dependencies = [ "subtle", ] +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + +[[package]] +name = "agave-feature-set" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe79fc4c114c51ea8461d829bb49853a21a76c7c8ef20e9041b071558f628ce" +dependencies = [ + "ahash 0.8.12", + "solana-epoch-schedule", + "solana-hash 3.1.0", + "solana-pubkey 3.0.0", + "solana-sha256-hasher", + "solana-svm-feature-set", +] + [[package]] name = "ahash" version = "0.2.19" @@ -77,6 +116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if 1.0.4", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -305,7 +345,7 @@ checksum = "e57586581f2008933241d16c3e3f633168b3a5d2738c5c42ea5246ec5e0ef17a" dependencies = [ "alloy-primitives", "alloy-sol-types", - "http", + "http 1.4.0", "serde", "serde_json", "thiserror 2.0.18", @@ -425,7 +465,7 @@ dependencies = [ "async-stream", "async-trait", "auto_impl", - "dashmap", + "dashmap 6.1.0", "either", "futures", "futures-utils-wasm", @@ -720,7 +760,7 @@ dependencies = [ "alloy-pubsub", "alloy-transport", "futures", - "http", + "http 1.4.0", "rustls 0.23.37", "serde_json", "tokio 1.50.0", @@ -834,6 +874,49 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "anza-quinn" +version = "0.11.9-rustsec20260037" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91bfa08f6e7e4187354ff4f793b81cc08218a6a95cc48f5de7616d44452bf6e0" +dependencies = [ + "anza-quinn-proto", + "bytes 1.11.1", + "cfg_aliases", + "pin-project-lite 0.2.17", + "quinn-udp", + "rustc-hash", + "rustls 0.23.37", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio 1.50.0", + "tracing", + "web-time", +] + +[[package]] +name = "anza-quinn-proto" +version = "0.11.13-rustsec20260037" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00a4d8cf8d72ee56e0ee20d3b4eef4785ce1b05299c4982c6de7f251c458efe" +dependencies = [ + "bytes 1.11.1", + "fastbloom", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls 0.23.37", + "rustls-pki-types", + "rustls-platform-verifier", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + [[package]] name = "arbitrary" version = "1.4.2" @@ -843,6 +926,15 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + [[package]] name = "ark-bls12-381" version = "0.5.0" @@ -870,7 +962,7 @@ dependencies = [ "fnv", "hashbrown 0.15.5", "itertools 0.13.0", - "num-bigint", + "num-bigint 0.4.6", "num-integer", "num-traits", "rayon", @@ -888,7 +980,7 @@ dependencies = [ "ark-serialize 0.3.0", "ark-std 0.3.0", "derivative", - "num-bigint", + "num-bigint 0.4.6", "num-traits", "paste", "rustc_version 0.3.3", @@ -908,7 +1000,7 @@ dependencies = [ "derivative", "digest 0.10.7", "itertools 0.10.5", - "num-bigint", + "num-bigint 0.4.6", "num-traits", "paste", "rustc_version 0.4.1", @@ -929,7 +1021,7 @@ dependencies = [ "digest 0.10.7", "educe", "itertools 0.13.0", - "num-bigint", + "num-bigint 0.4.6", "num-traits", "paste", "rayon", @@ -972,7 +1064,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" dependencies = [ - "num-bigint", + "num-bigint 0.4.6", "num-traits", "quote", "syn 1.0.109", @@ -984,7 +1076,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" dependencies = [ - "num-bigint", + "num-bigint 0.4.6", "num-traits", "proc-macro2", "quote", @@ -997,7 +1089,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" dependencies = [ - "num-bigint", + "num-bigint 0.4.6", "num-traits", "proc-macro2", "quote", @@ -1037,7 +1129,7 @@ checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ "ark-std 0.4.0", "digest 0.10.7", - "num-bigint", + "num-bigint 0.4.6", ] [[package]] @@ -1050,7 +1142,7 @@ dependencies = [ "ark-std 0.5.0", "arrayvec 0.7.6", "digest 0.10.7", - "num-bigint", + "num-bigint 0.4.6", "rayon", ] @@ -1129,14 +1221,36 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "ascii" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" + +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive 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", @@ -1145,6 +1259,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" @@ -1157,6 +1283,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" @@ -1222,6 +1359,18 @@ dependencies = [ "tokio 1.50.0", ] +[[package]] +name = "async-compression" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite 0.2.17", + "tokio 1.50.0", +] + [[package]] name = "async-executor" version = "1.14.0" @@ -1473,7 +1622,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ "base64 0.22.1", - "http", + "http 1.4.0", "log", "url", ] @@ -1538,7 +1687,7 @@ dependencies = [ "bytes 1.11.1", "form_urlencoded", "futures-util", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -1569,7 +1718,7 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes 1.11.1", "futures-core", - "http", + "http 1.4.0", "http-body", "http-body-util", "mime", @@ -1591,7 +1740,7 @@ dependencies = [ "bytes 1.11.1", "futures-util", "headers", - "http", + "http 1.4.0", "http-body", "http-body-util", "mime", @@ -1623,7 +1772,7 @@ dependencies = [ "axum", "futures-core", "futures-util", - "http", + "http 1.4.0", "opentelemetry", "opentelemetry-semantic-conventions", "pin-project-lite 0.2.17", @@ -1721,6 +1870,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bincode" version = "2.0.1" @@ -1935,6 +2093,30 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e31ea183f6ee62ac8b8a8cf7feddd766317adfb13ff469de57ce033efd6a790" +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive", + "bytes 1.11.1", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "brotli" version = "8.0.2" @@ -1975,6 +2157,16 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + [[package]] name = "byte-slice-cast" version = "1.2.3" @@ -1986,6 +2178,20 @@ name = "bytemuck" version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "byteorder" @@ -2141,6 +2347,15 @@ dependencies = [ "winx", ] +[[package]] +name = "caps" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1ddba47aba30b6a889298ad0109c3b8dcb0e8fc993b459daa7067d46f865e0" +dependencies = [ + "libc", +] + [[package]] name = "cargo-platform" version = "0.1.9" @@ -2246,6 +2461,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cfg_eval" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "chacha20" version = "0.9.1" @@ -2447,7 +2673,7 @@ dependencies = [ "coins-bip32", "hmac", "once_cell", - "pbkdf2", + "pbkdf2 0.12.2", "rand 0.8.5", "sha2 0.10.9", "thiserror 1.0.69", @@ -2494,6 +2720,19 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "combine" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" +dependencies = [ + "ascii", + "byteorder", + "either", + "memchr", + "unreachable", +] + [[package]] name = "combine" version = "4.6.7" @@ -2510,6 +2749,24 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2d5be4898d87e1ce07d30f93de999a5a0aca181c65e7a33467292482a62e84f" +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -2551,6 +2808,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width 0.2.2", + "windows-sys 0.61.2", +] + [[package]] name = "const-hex" version = "1.18.1" @@ -2759,7 +3028,7 @@ dependencies = [ "ecdsa", "ed25519-zebra", "k256", - "num-bigint", + "num-bigint 0.4.6", "num-traits", "p256", "rand_core 0.6.4", @@ -3267,7 +3536,9 @@ dependencies = [ "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", + "rand_core 0.6.4", "rustc_version 0.4.1", + "serde", "subtle", "zeroize", ] @@ -3353,7 +3624,7 @@ name = "cw-wavs-mock-api" version = "0.0.0" dependencies = [ "anyhow", - "bincode", + "bincode 2.0.1", "cosmwasm-schema", "cosmwasm-std", "layer-climb-address", @@ -3383,7 +3654,7 @@ name = "cw-wavs-trigger-api" version = "0.0.0" dependencies = [ "anyhow", - "bincode", + "bincode 2.0.1", "cosmwasm-schema", "cosmwasm-std", "layer-climb-address", @@ -3527,6 +3798,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if 1.0.4", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.12", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -3590,7 +3874,7 @@ dependencies = [ "dbus", "fastrand 2.3.0", "hkdf", - "num", + "num 0.4.3", "once_cell", "sha2 0.10.9", "zeroize", @@ -3634,16 +3918,30 @@ 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", + "num-bigint 0.4.6", "num-traits", "rusticata-macros", ] @@ -3658,6 +3956,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derivation-path" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" + [[package]] name = "derivative" version = "2.2.0" @@ -3764,7 +4068,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" dependencies = [ - "console", + "console 0.15.11", "shell-words", "tempfile", "thiserror 1.0.69", @@ -3887,18 +4191,41 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "dlopen2_derive 0.3.0", + "libc", + "once_cell", + "winapi 0.3.9", +] + [[package]] name = "dlopen2" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" dependencies = [ - "dlopen2_derive", + "dlopen2_derive 0.4.3", "libc", "once_cell", "winapi 0.3.9", ] +[[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.117", +] + [[package]] name = "dlopen2_derive" version = "0.4.3" @@ -4396,6 +4723,18 @@ dependencies = [ "synstructure 0.12.6", ] +[[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.2", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -4453,6 +4792,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + [[package]] name = "ff" version = "0.13.1" @@ -4499,6 +4844,30 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "five8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f76610e969fa1784327ded240f1e28a3fd9520c9cec93b636fcf62dd37f772" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_const" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0f1728185f277989ca573a402716ae0beaaea3f76a8ff87ef9dd8fb19436c5" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059c31d7d36c43fe39d89e55711858b4da8be7eb6dabac23c7289b1a19489406" + [[package]] name = "fix-path-env" version = "0.0.0" @@ -5003,6 +5372,16 @@ dependencies = [ "libc", ] +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -5214,6 +5593,26 @@ dependencies = [ "system-deps", ] +[[package]] +name = "governor" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" +dependencies = [ + "cfg-if 1.0.4", + "dashmap 5.5.3", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot 0.12.5", + "portable-atomic", + "quanta", + "rand 0.8.5", + "smallvec", + "spinning_top", +] + [[package]] name = "group" version = "0.13.0" @@ -5288,7 +5687,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", + "http 1.4.0", "indexmap 2.13.0", "slab", "tokio 1.50.0", @@ -5307,6 +5706,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.6.3" @@ -5394,7 +5802,7 @@ dependencies = [ "base64 0.22.1", "bytes 1.11.1", "headers-core", - "http", + "http 1.4.0", "httpdate", "mime", "sha1", @@ -5406,7 +5814,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http", + "http 1.4.0", ] [[package]] @@ -5513,6 +5921,12 @@ dependencies = [ "tracing", ] +[[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" @@ -5562,6 +5976,17 @@ dependencies = [ "markup5ever 0.36.1", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes 1.11.1", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.4.0" @@ -5588,7 +6013,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes 1.11.1", - "http", + "http 1.4.0", ] [[package]] @@ -5599,7 +6024,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes 1.11.1", "futures-core", - "http", + "http 1.4.0", "http-body", "pin-project-lite 0.2.17", ] @@ -5642,7 +6067,7 @@ dependencies = [ "futures-channel", "futures-core", "h2", - "http", + "http 1.4.0", "http-body", "httparse", "httpdate", @@ -5660,7 +6085,7 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http", + "http 1.4.0", "hyper", "hyper-util", "rustls 0.23.37", @@ -5710,7 +6135,7 @@ dependencies = [ "bytes 1.11.1", "futures-channel", "futures-util", - "http", + "http 1.4.0", "http-body", "hyper", "ipnet", @@ -6057,7 +6482,7 @@ dependencies = [ "attohttpc", "bytes 1.11.1", "futures", - "http", + "http 1.4.0", "http-body-util", "hyper", "hyper-util", @@ -6125,6 +6550,19 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console 0.16.3", + "portable-atomic", + "unicode-width 0.2.2", + "unit-prefix", + "web-time", +] + [[package]] name = "infer" version = "0.19.0" @@ -6362,7 +6800,7 @@ checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", "cfg-if 1.0.4", - "combine", + "combine 4.6.7", "jni-sys", "log", "thiserror 1.0.69", @@ -6429,6 +6867,21 @@ dependencies = [ "serde_json", ] +[[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 = "jwt" version = "0.16.0" @@ -6568,7 +7021,7 @@ checksum = "e1acb5125dc4b8d81c164ec0b7b58c35dfa8e0a729ed6b1285bfe2efe7dfe3e2" dependencies = [ "alloy-primitives", "anyhow", - "bincode", + "bincode 2.0.1", "const-hex", "cosmwasm-schema", "cosmwasm-std", @@ -6630,7 +7083,7 @@ dependencies = [ "futures", "futures-timer", "getrandom 0.3.4", - "http", + "http 1.4.0", "http-body", "http-body-util", "httparse", @@ -6710,13 +7163,14 @@ dependencies = [ "alloy-sol-types", "anyhow", "async-std", + "base64 0.22.1", "bip39", "clap 4.6.0", "const-hex", "cosmwasm-std", "cw-wavs-mock-api", "cw-wavs-trigger-api", - "dashmap", + "dashmap 6.1.0", "deadpool", "derive-enum-all-values", "example-types", @@ -6735,6 +7189,18 @@ dependencies = [ "reqwest 0.12.28", "serde", "serde_json", + "solana-client", + "solana-commitment-config", + "solana-hash 4.3.0", + "solana-instruction", + "solana-keypair", + "solana-message", + "solana-pubkey 3.0.0", + "solana-rpc-client-api", + "solana-sha256-hasher", + "solana-signature", + "solana-signer", + "solana-transaction", "tempfile", "tokio 1.50.0", "tracing", @@ -7228,7 +7694,7 @@ dependencies = [ "rustls 0.23.37", "rustls-webpki 0.103.9", "thiserror 2.0.18", - "x509-parser", + "x509-parser 0.17.0", "yasna", ] @@ -7516,6 +7982,18 @@ dependencies = [ "flat-tree 5.0.0", ] +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + [[package]] name = "miette" version = "7.6.0" @@ -7883,6 +8361,7 @@ dependencies = [ "cfg-if 1.0.4", "cfg_aliases", "libc", + "memoffset", ] [[package]] @@ -7897,6 +8376,12 @@ dependencies = [ "libc", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nodrop" version = "0.1.14" @@ -7919,6 +8404,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "normpath" version = "1.5.0" @@ -7937,17 +8428,42 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ - "num-bigint", - "num-complex", + "num-bigint 0.4.6", + "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 1.5.0", + "num-integer", "num-traits", ] @@ -7961,6 +8477,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg 1.5.0", + "num-traits", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -7976,6 +8502,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -7996,13 +8533,25 @@ 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 1.5.0", + "num-bigint 0.2.6", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "num-bigint", + "num-bigint 0.4.6", "num-integer", "num-traits", ] @@ -8207,7 +8756,7 @@ dependencies = [ "bytes 1.11.1", "chrono", "futures-util", - "http", + "http 1.4.0", "http-auth", "jwt", "lazy_static 1.5.0", @@ -8258,13 +8807,22 @@ dependencies = [ "wit-parser 0.230.0", ] +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "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", + "asn1-rs 0.7.1", ] [[package]] @@ -8372,7 +8930,7 @@ checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" dependencies = [ "async-trait", "bytes 1.11.1", - "http", + "http 1.4.0", "opentelemetry", "reqwest 0.12.28", ] @@ -8392,7 +8950,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" dependencies = [ - "http", + "http 1.4.0", "opentelemetry", "opentelemetry-http", "opentelemetry-proto", @@ -8625,6 +9183,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" + [[package]] name = "pathdiff" version = "0.2.3" @@ -8668,6 +9232,15 @@ dependencies = [ "serde", ] +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "pbkdf2" version = "0.12.2" @@ -8728,6 +9301,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca" +[[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" @@ -8753,6 +9335,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 = "permissions" version = "2.8.0" @@ -9671,6 +10262,30 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi 0.11.1+wasi-snapshot-preview1", + "web-sys", + "winapi 0.3.9", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -9966,6 +10581,15 @@ dependencies = [ "rustversion", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -9998,7 +10622,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", @@ -10121,7 +10745,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -10167,7 +10791,7 @@ dependencies = [ "bytes 1.11.1", "futures-core", "futures-util", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -10191,6 +10815,21 @@ dependencies = [ "web-sys", ] +[[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 0.12.28", + "serde", + "thiserror 1.0.69", + "tower-service", +] + [[package]] name = "resolv-conf" version = "0.7.6" @@ -10358,7 +10997,7 @@ dependencies = [ "bytes 1.11.1", "fastrlp 0.3.1", "fastrlp 0.4.0", - "num-bigint", + "num-bigint 0.4.6", "num-integer", "num-traits", "parity-scale-codec", @@ -10555,6 +11194,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.37", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.9", + "security-framework 3.7.0", + "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.102.8" @@ -10775,7 +11441,7 @@ dependencies = [ "futures-util", "generic-array", "hkdf", - "num", + "num 0.4.3", "once_cell", "rand 0.8.5", "serde", @@ -10906,6 +11572,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde-untagged" version = "0.1.9" @@ -11164,6 +11839,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + [[package]] name = "sha256" version = "1.6.0" @@ -11254,186 +11935,1903 @@ dependencies = [ ] [[package]] -name = "simd-adler32" -version = "0.3.8" +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simple-aggregator" +version = "2.8.0" +dependencies = [ + "alloy-primitives", + "example-helpers", + "example-types", + "layer-climb-address", + "serde", + "wasip2", + "wavs-types", + "wavs-wasi-utils", + "wit-bindgen 0.53.1", + "wstd", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "snow" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" +dependencies = [ + "aes-gcm", + "blake2 0.10.6", + "chacha20poly1305", + "curve25519-dalek 4.1.3", + "rand_core 0.6.4", + "ring", + "rustc_version 0.4.1", + "sha2 0.10.9", + "subtle", +] + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall 0.5.18", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "solana-account" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efc0ed36decb689413b9da5d57f2be49eea5bebb3cf7897015167b0c4336e731" +dependencies = [ + "bincode 1.3.3", + "serde", + "serde_bytes", + "serde_derive", + "solana-account-info", + "solana-clock", + "solana-instruction-error", + "solana-pubkey 4.2.0", + "solana-sdk-ids", + "solana-sysvar", +] + +[[package]] +name = "solana-account-decoder" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "751157b033a30c178a272ec2bbab8a5bae14b21490451627990cbe84391b8264" +dependencies = [ + "Inflector", + "base64 0.22.1", + "bincode 1.3.3", + "bs58", + "bv", + "serde", + "serde_json", + "solana-account", + "solana-account-decoder-client-types", + "solana-address-lookup-table-interface", + "solana-clock", + "solana-config-interface", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-instruction", + "solana-loader-v3-interface", + "solana-nonce", + "solana-program-option", + "solana-program-pack", + "solana-pubkey 3.0.0", + "solana-rent", + "solana-sdk-ids", + "solana-slot-hashes", + "solana-slot-history", + "solana-stake-interface", + "solana-sysvar", + "solana-vote-interface", + "spl-generic-token", + "spl-token-2022-interface", + "spl-token-group-interface", + "spl-token-interface", + "spl-token-metadata-interface", + "thiserror 2.0.18", + "zstd", +] + +[[package]] +name = "solana-account-decoder-client-types" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c200fb0019770f6ccf869a9b71653d37b76a633114d79176eedea6931c09cc7c" +dependencies = [ + "base64 0.22.1", + "bs58", + "serde", + "serde_json", + "solana-account", + "solana-pubkey 3.0.0", + "zstd", +] + +[[package]] +name = "solana-account-info" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9cf16495d9eb53e3d04e72366a33bb1c20c24e78c171d8b8f5978357b63ae95" +dependencies = [ + "solana-address 2.6.0", + "solana-program-error", + "solana-program-memory", +] + +[[package]] +name = "solana-address" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ecac8e1b7f74c2baa9e774c42817e3e75b20787134b76cc4d45e8a604488f5" +dependencies = [ + "solana-address 2.6.0", +] + +[[package]] +name = "solana-address" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1384b52c435a750cc9c538760fc7bb472fd78e65a9900a2d07312c5bb335b72" +dependencies = [ + "borsh", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek 4.1.3", + "five8", + "five8_const", + "serde", + "serde_derive", + "sha2-const-stable", + "solana-atomic-u64", + "solana-define-syscall 5.1.0", + "solana-nullable", + "solana-program-error", + "solana-sanitize", + "solana-sha256-hasher", + "wincode", +] + +[[package]] +name = "solana-address-lookup-table-interface" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115b4f773acc4f3f3cb986b0d335e9845c0368c82b0940410935bc11ae065578" +dependencies = [ + "bincode 1.3.3", + "bytemuck", + "serde", + "serde_derive", + "solana-clock", + "solana-instruction", + "solana-instruction-error", + "solana-pubkey 4.2.0", + "solana-sdk-ids", + "solana-slot-hashes", +] + +[[package]] +name = "solana-atomic-u64" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085db4906d89324cef2a30840d59eaecf3d4231c560ec7c9f6614a93c652f501" +dependencies = [ + "parking_lot 0.12.5", +] + +[[package]] +name = "solana-borsh" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c04abbae16f57178a163125805637b8a076175bb5c0002fb04f4792bea901cf7" +dependencies = [ + "borsh", +] + +[[package]] +name = "solana-client" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1021bbda77368c57f3d8b850c5d9b19077906d21d1c516b1ccfbb8fe7737553f" +dependencies = [ + "anza-quinn", + "async-trait", + "bincode 1.3.3", + "dashmap 5.5.3", + "futures", + "futures-util", + "indexmap 2.13.0", + "indicatif", + "log", + "rayon", + "solana-account", + "solana-client-traits", + "solana-commitment-config", + "solana-connection-cache", + "solana-epoch-info", + "solana-hash 3.1.0", + "solana-instruction", + "solana-keypair", + "solana-measure", + "solana-message", + "solana-net-utils", + "solana-pubkey 3.0.0", + "solana-pubsub-client", + "solana-quic-client", + "solana-quic-definitions", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-rpc-client-nonce-utils", + "solana-signature", + "solana-signer", + "solana-streamer", + "solana-time-utils", + "solana-tpu-client", + "solana-transaction", + "solana-transaction-error", + "solana-transaction-status-client-types", + "solana-udp-client", + "thiserror 2.0.18", + "tokio 1.50.0", + "tokio-util", +] + +[[package]] +name = "solana-client-traits" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08618ed587e128105510c54ae3e456b9a06d674d8640db75afe66dad65cb4e02" +dependencies = [ + "solana-account", + "solana-commitment-config", + "solana-epoch-info", + "solana-hash 3.1.0", + "solana-instruction", + "solana-keypair", + "solana-message", + "solana-pubkey 3.0.0", + "solana-signature", + "solana-signer", + "solana-system-interface", + "solana-transaction", + "solana-transaction-error", +] + +[[package]] +name = "solana-clock" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95cf11109c3b6115cc510f1e31f06fdd52f504271bc24ef5f1249fbbcae5f9f3" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-cluster-type" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a494cf8eda7d98d9f0144b288bb409c88308d2e86f15cc1045aa77b83304718" +dependencies = [ + "solana-hash 4.3.0", +] + +[[package]] +name = "solana-commitment-config" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1517aa49dcfa9cb793ef90e7aac81346d62ca4a546bb1a754030a033e3972e1c" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "solana-config-interface" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e401ae56aed512821cc7a0adaa412ff97fecd2dff4602be7b1330d2daec0c4" +dependencies = [ + "bincode 1.3.3", + "serde", + "serde_derive", + "solana-account", + "solana-instruction", + "solana-pubkey 3.0.0", + "solana-sdk-ids", + "solana-short-vec", + "solana-system-interface", +] + +[[package]] +name = "solana-connection-cache" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a1f37271395e9e5371ef18d93197d5d53a18acd5651b66a5aa2ac68bd6dd54" +dependencies = [ + "async-trait", + "bincode 1.3.3", + "crossbeam-channel", + "futures-util", + "indexmap 2.13.0", + "log", + "rand 0.8.5", + "rayon", + "solana-keypair", + "solana-measure", + "solana-metrics", + "solana-time-utils", + "solana-transaction-error", + "thiserror 2.0.18", + "tokio 1.50.0", +] + +[[package]] +name = "solana-cpi" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dea26709d867aada85d0d3617db0944215c8bb28d3745b912de7db13a23280c" +dependencies = [ + "solana-account-info", + "solana-define-syscall 4.0.1", + "solana-instruction", + "solana-program-error", + "solana-pubkey 4.2.0", + "solana-stable-layout", +] + +[[package]] +name = "solana-curve25519" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aff7432cdf2ec6a44ac06b4d64d2ee006f6c0066d6456e032a7fe25be40cd5c" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "curve25519-dalek 4.1.3", + "solana-define-syscall 3.0.0", + "subtle", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-define-syscall" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9697086a4e102d28a156b8d6b521730335d6951bd39a5e766512bbe09007cee" + +[[package]] +name = "solana-define-syscall" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e5b1c0bc1d4a4d10c88a4100499d954c09d3fecfae4912c1a074dff68b1738" + +[[package]] +name = "solana-define-syscall" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e14a4f604117f379840956a8fc8695e4c84f5b0ebed192f31f60d9b85d581d" + +[[package]] +name = "solana-derivation-path" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff71743072690fdbdfcdc37700ae1cb77485aaad49019473a81aee099b1e0b8c" +dependencies = [ + "derivation-path", + "qstring", + "uriparse", +] + +[[package]] +name = "solana-epoch-info" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e093c84f6ece620a6b10cd036574b0cd51944231ab32d81f80f76d54aba833e6" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "solana-epoch-rewards" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e7b0ba210593ba8ddd39d6d234d81795d1671cebf3026baa10d5dc23ac42f0" +dependencies = [ + "serde", + "serde_derive", + "solana-hash 4.3.0", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-epoch-schedule" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce264b7b42322325947c4136a09460bf5c73d9aa8262c9b0a2064be63ba8639" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-event-relay" +version = "2.8.0" +dependencies = [ + "example-helpers", +] + +[[package]] +name = "solana-feature-gate-interface" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ca9b5cbb6f500f7fd73db5bd95640f71a83f04d6121a0e59a43b202dca2731" +dependencies = [ + "serde", + "serde_derive", + "solana-program-error", + "solana-pubkey 4.2.0", + "solana-sdk-ids", +] + +[[package]] +name = "solana-fee-calculator" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e8add96b5741573e9f7529c4bb7719cfcfa999c3847a68cdfaef0cb6adf567" +dependencies = [ + "log", + "serde", + "serde_derive", +] + +[[package]] +name = "solana-hash" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "337c246447142f660f778cf6cb582beba8e28deb05b3b24bfb9ffd7c562e5f41" +dependencies = [ + "solana-hash 4.3.0", +] + +[[package]] +name = "solana-hash" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b113239362cee7093bfb250467138f079a2a03673181dc15bff6ccd677912d" +dependencies = [ + "borsh", + "bytemuck", + "bytemuck_derive", + "five8", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-sanitize", + "wincode", +] + +[[package]] +name = "solana-inflation" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f762559c5f962727efdcb03c61f5cf6c5364645695978fb145d25c88bbacdada" + +[[package]] +name = "solana-instruction" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ebb0ffd19263051bc3f683fcc086134b8ff23af894dcb63f7563c7137b42f1" +dependencies = [ + "bincode 1.3.3", + "serde", + "serde_derive", + "solana-define-syscall 5.1.0", + "solana-instruction-error", + "solana-pubkey 4.2.0", +] + +[[package]] +name = "solana-instruction-error" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b188842592fdf6cb96f55263ae1bf11713ab5114401d1d5a881ed7cc41bef6" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-program-error", +] + +[[package]] +name = "solana-instructions-sysvar" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddf67876c541aa1e21ee1acae35c95c6fbc61119814bfef70579317a5e26955" +dependencies = [ + "bitflags 2.11.0", + "solana-account-info", + "solana-instruction", + "solana-instruction-error", + "solana-program-error", + "solana-pubkey 3.0.0", + "solana-sanitize", + "solana-sdk-ids", + "solana-serialize-utils", + "solana-sysvar-id", +] + +[[package]] +name = "solana-keypair" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "263d614c12aa267a3278703175fd6440552ca61bc960b5a02a4482720c53438b" +dependencies = [ + "ed25519-dalek 2.2.0", + "five8", + "five8_core", + "rand 0.9.2", + "solana-address 2.6.0", + "solana-seed-phrase", + "solana-signature", + "solana-signer", +] + +[[package]] +name = "solana-last-restart-slot" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcda154ec827f5fc1e4da0af3417951b7e9b8157540f81f936c4a8b1156134d0" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-loader-v3-interface" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e0538d4dbc9022e01616f1c58f2db98ece739c5d5ed4a2ef8737a953e76a2d4" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey 4.2.0", + "solana-sdk-ids", +] + +[[package]] +name = "solana-measure" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b0eef68497ca818f50da3de449c64dc406d28170e395fd2ec40b64b6798afec" + +[[package]] +name = "solana-message" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0448b1fd891c5f46491e5dc7d9986385ba3c852c340db2911dd29faa01d2b08d" +dependencies = [ + "bincode 1.3.3", + "lazy_static 1.5.0", + "serde", + "serde_derive", + "solana-address 2.6.0", + "solana-hash 4.3.0", + "solana-instruction", + "solana-sanitize", + "solana-sdk-ids", + "solana-short-vec", + "solana-transaction-error", +] + +[[package]] +name = "solana-metrics" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba33d34f2297e307bc124c3108da4b5b536a63022d705c0367a8512caebc9c4f" +dependencies = [ + "crossbeam-channel", + "gethostname", + "log", + "reqwest 0.12.28", + "solana-cluster-type", + "solana-sha256-hasher", + "solana-time-utils", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-msg" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726b7cbbc6be6f1c6f29146ac824343b9415133eee8cce156452ad1db93f8008" +dependencies = [ + "solana-define-syscall 5.1.0", +] + +[[package]] +name = "solana-net-utils" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2a83f0f11be6d2c0104871136ae22250a5dafd013fe137678312b73a7e35044" +dependencies = [ + "anyhow", + "bincode 1.3.3", + "bytes 1.11.1", + "cfg-if 1.0.4", + "dashmap 5.5.3", + "itertools 0.12.1", + "log", + "nix 0.30.1", + "rand 0.8.5", + "serde", + "socket2 0.6.3", + "solana-serde", + "solana-svm-type-overrides", + "tokio 1.50.0", + "url", +] + +[[package]] +name = "solana-nonce" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95dbc9f2e33b6c10e231df15cb2a3bff9ea7eab6347f9e316fe75c97fd67bbb" +dependencies = [ + "serde", + "serde_derive", + "solana-fee-calculator", + "solana-hash 4.3.0", + "solana-pubkey 4.2.0", + "solana-sha256-hasher", +] + +[[package]] +name = "solana-nullable" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0f95d3028ef0f682bb174b77379c19d5dae2904a649f4a103fe29be7a139980" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "solana-packet" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edf2f25743c95229ac0fdc32f8f5893ef738dbf332c669e9861d33ddb0f469d" +dependencies = [ + "bincode 1.3.3", + "bitflags 2.11.0", + "cfg_eval", + "serde", + "serde_derive", + "serde_with", +] + +[[package]] +name = "solana-perf" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "794506987f9168331a84836eb66e762ecd5fd18920f0c5bcdf349eefff553e59" +dependencies = [ + "ahash 0.8.12", + "bincode 1.3.3", + "bv", + "bytes 1.11.1", + "caps", + "curve25519-dalek 4.1.3", + "dlopen2 0.5.0", + "fnv", + "libc", + "log", + "nix 0.30.1", + "rand 0.8.5", + "rayon", + "serde", + "solana-hash 3.1.0", + "solana-message", + "solana-metrics", + "solana-packet", + "solana-pubkey 3.0.0", + "solana-rayon-threadlimit", + "solana-sdk-ids", + "solana-short-vec", + "solana-signature", + "solana-time-utils", + "solana-transaction-context", +] + +[[package]] +name = "solana-program-entrypoint" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c9b0a1ff494e05f503a08b3d51150b73aa639544631e510279d6375f290997" +dependencies = [ + "solana-account-info", + "solana-define-syscall 4.0.1", + "solana-program-error", + "solana-pubkey 4.2.0", +] + +[[package]] +name = "solana-program-error" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f04fa578707b3612b095f0c8e19b66a1233f7c42ca8082fcb3b745afcc0add6" +dependencies = [ + "borsh", +] + +[[package]] +name = "solana-program-memory" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4068648649653c2c50546e9a7fb761791b5ab0cda054c771bb5808d3a4b9eb52" +dependencies = [ + "solana-define-syscall 4.0.1", +] + +[[package]] +name = "solana-program-option" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a88006a9b8594088cec9027ab77caaaa258a2aaa2083d3f086c44b42e50aeab" + +[[package]] +name = "solana-program-pack" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7701cb15b90667ae1c89ef4ac35a59c61e66ce58ddee13d729472af7f41d59" +dependencies = [ + "solana-program-error", +] + +[[package]] +name = "solana-pubkey" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8909d399deb0851aa524420beeb5646b115fd253ef446e35fe4504c904da3941" +dependencies = [ + "solana-address 1.1.0", +] + +[[package]] +name = "solana-pubkey" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db719574990de7e8b0f55a8593ac92a5ccb42c8ce67b3e4bf05b139d5d9ee71" +dependencies = [ + "solana-address 2.6.0", +] + +[[package]] +name = "solana-pubsub-client" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3eb58936a5c35140f7eb0fc0a30685a32523001b24c4e3e18e70ca05c8c680b" +dependencies = [ + "crossbeam-channel", + "futures-util", + "http 0.2.12", + "log", + "semver 1.0.27", + "serde", + "serde_json", + "solana-account-decoder-client-types", + "solana-clock", + "solana-pubkey 3.0.0", + "solana-rpc-client-types", + "solana-signature", + "thiserror 2.0.18", + "tokio 1.50.0", + "tokio-stream", + "tokio-tungstenite 0.28.0", + "tungstenite 0.28.0", + "url", +] + +[[package]] +name = "solana-quic-client" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab47dde0f9b63bfee69fb7f4c7525b87b6685de2e538e8d0ab614625df3d093" +dependencies = [ + "anza-quinn", + "anza-quinn-proto", + "async-lock", + "async-trait", + "futures", + "itertools 0.12.1", + "log", + "rustls 0.23.37", + "solana-connection-cache", + "solana-keypair", + "solana-measure", + "solana-metrics", + "solana-net-utils", + "solana-pubkey 3.0.0", + "solana-quic-definitions", + "solana-rpc-client-api", + "solana-signer", + "solana-streamer", + "solana-tls-utils", + "solana-transaction-error", + "thiserror 2.0.18", + "tokio 1.50.0", +] + +[[package]] +name = "solana-quic-definitions" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15319accf7d3afd845817aeffa6edd8cc185f135cefbc6b985df29cfd8c09609" +dependencies = [ + "solana-keypair", +] + +[[package]] +name = "solana-rayon-threadlimit" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06a84c1c10b97998364e06e1254e2e2364c338ce0cf1bdcfc996ab39e85c618" +dependencies = [ + "log", + "num_cpus", +] + +[[package]] +name = "solana-rent" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e860d5499a705369778647e97d760f7670adfb6fc8419dd3d568deccd46d5487" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-reward-info" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82be7946105c2ee6be9f9ee7bd18a068b558389221d29efa92b906476102bfcc" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "solana-rpc-client" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a11dd2723748fb6a7a2b5b83e4186cb99f35f1384d8b310cd57122ea8627839a" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bincode 1.3.3", + "bs58", + "futures", + "indicatif", + "log", + "reqwest 0.12.28", + "reqwest-middleware", + "semver 1.0.27", + "serde", + "serde_json", + "solana-account", + "solana-account-decoder", + "solana-account-decoder-client-types", + "solana-clock", + "solana-commitment-config", + "solana-epoch-info", + "solana-epoch-schedule", + "solana-feature-gate-interface", + "solana-hash 3.1.0", + "solana-instruction", + "solana-message", + "solana-pubkey 3.0.0", + "solana-rpc-client-api", + "solana-signature", + "solana-transaction", + "solana-transaction-error", + "solana-transaction-status-client-types", + "solana-version", + "solana-vote-interface", + "tokio 1.50.0", +] + +[[package]] +name = "solana-rpc-client-api" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a81a57ed54176e3aa4bca8e894f9326638a3f6870f79b078d8b3fadda8a4c9b" +dependencies = [ + "anyhow", + "jsonrpc-core", + "reqwest 0.12.28", + "reqwest-middleware", + "serde", + "serde_json", + "solana-account-decoder-client-types", + "solana-clock", + "solana-rpc-client-types", + "solana-signer", + "solana-transaction-error", + "solana-transaction-status-client-types", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-rpc-client-nonce-utils" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4804524b94b49e721c6febc0b4c8f745cd76fc05dfbd972a1e6257e23502b59" +dependencies = [ + "solana-account", + "solana-commitment-config", + "solana-hash 3.1.0", + "solana-message", + "solana-nonce", + "solana-pubkey 3.0.0", + "solana-rpc-client", + "solana-sdk-ids", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-rpc-client-types" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a6f06038b9f88ecaecb0d283a325040429c700c1d04526c015c2c8182686c9b" +dependencies = [ + "base64 0.22.1", + "bs58", + "semver 1.0.27", + "serde", + "serde_json", + "solana-account", + "solana-account-decoder-client-types", + "solana-address 1.1.0", + "solana-clock", + "solana-commitment-config", + "solana-fee-calculator", + "solana-inflation", + "solana-reward-info", + "solana-transaction", + "solana-transaction-error", + "solana-transaction-status-client-types", + "solana-version", + "spl-generic-token", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-sanitize" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" + +[[package]] +name = "solana-sbpf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15b079e08471a9dbfe1e48b2c7439c85aa2a055cbd54eddd8bd257b0a7dbb29" +dependencies = [ + "byteorder", + "combine 3.8.1", + "hash32", + "log", + "rustc-demangle", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-sdk-ids" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def234c1956ff616d46c9dd953f251fa7096ddbaa6d52b165218de97882b7280" +dependencies = [ + "solana-address 2.6.0", +] + +[[package]] +name = "solana-sdk-macro" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8765316242300c48242d84a41614cb3388229ec353ba464f6fe62a733e41806f" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "solana-seed-derivable" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7bdb72758e3bec33ed0e2658a920f1f35dfb9ed576b951d20d63cb61ecd95c" +dependencies = [ + "solana-derivation-path", +] + +[[package]] +name = "solana-seed-phrase" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc905b200a95f2ea9146e43f2a7181e3aeb55de6bc12afb36462d00a3c7310de" +dependencies = [ + "hmac", + "pbkdf2 0.11.0", + "sha2 0.10.9", +] + +[[package]] +name = "solana-serde" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709a93cab694c70f40b279d497639788fc2ccbcf9b4aa32273d4b361322c02dd" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-serde-varint" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950e5b83e839dc0f92c66afc124bb8f40e89bc90f0579e8ec5499296d27f54e3" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-serialize-utils" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7cc401931d178472358e6b78dc72d031dc08f752d7410f0e8bd259dd6f02fa" +dependencies = [ + "solana-instruction-error", + "solana-pubkey 4.2.0", + "solana-sanitize", +] + +[[package]] +name = "solana-sha256-hasher" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f" +dependencies = [ + "sha2 0.10.9", + "solana-define-syscall 4.0.1", + "solana-hash 4.3.0", +] + +[[package]] +name = "solana-short-vec" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb8cc883fc7b8ce4a7814cb1441b48c06437049ec11847005cf63bcfa85c546" +dependencies = [ + "serde_core", +] + +[[package]] +name = "solana-signature" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a73c6e97cc2108be0adf6a6ea326434f8398df9d7eed81da2a4548b69e971c" +dependencies = [ + "ed25519-dalek 2.2.0", + "five8", + "serde", + "serde-big-array", + "serde_derive", + "solana-sanitize", + "wincode", +] + +[[package]] +name = "solana-signer" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bfea97951fee8bae0d6038f39a5efcb6230ecdfe33425ac75196d1a1e3e3235" +dependencies = [ + "solana-pubkey 3.0.0", + "solana-signature", + "solana-transaction-error", +] + +[[package]] +name = "solana-slot-hashes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2585f70191623887329dfb5078da3a00e15e3980ea67f42c2e10b07028419f43" +dependencies = [ + "serde", + "serde_derive", + "solana-hash 4.3.0", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-slot-history" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f914f6b108f5bba14a280b458d023e3621c9973f27f015a4d755b50e88d89e97" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-stable-layout" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9f6a291ba063a37780af29e7db14bdd3dc447584d8ba5b3fc4b88e2bbc982fa" +dependencies = [ + "solana-instruction", + "solana-pubkey 4.2.0", +] + +[[package]] +name = "solana-stake-interface" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9bc26191b533f9a6e5a14cca05174119819ced680a80febff2f5051a713f0db" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-clock", + "solana-cpi", + "solana-instruction", + "solana-program-error", + "solana-pubkey 3.0.0", + "solana-system-interface", + "solana-sysvar", + "solana-sysvar-id", +] + +[[package]] +name = "solana-streamer" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1506f6dc927e86fd673bee67fafa27c942527874bac7d4aea63869dcbf22615" +dependencies = [ + "anza-quinn", + "anza-quinn-proto", + "arc-swap", + "bytes 1.11.1", + "crossbeam-channel", + "dashmap 5.5.3", + "futures", + "futures-util", + "governor", + "histogram", + "indexmap 2.13.0", + "itertools 0.12.1", + "libc", + "log", + "nix 0.30.1", + "num_cpus", + "pem 1.1.1", + "percentage", + "rand 0.8.5", + "rustls 0.23.37", + "smallvec", + "socket2 0.6.3", + "solana-keypair", + "solana-measure", + "solana-metrics", + "solana-net-utils", + "solana-packet", + "solana-perf", + "solana-pubkey 3.0.0", + "solana-quic-definitions", + "solana-signature", + "solana-signer", + "solana-time-utils", + "solana-tls-utils", + "solana-transaction-error", + "solana-transaction-metrics-tracker", + "thiserror 2.0.18", + "tokio 1.50.0", + "tokio-util", + "x509-parser 0.14.0", +] + +[[package]] +name = "solana-svm-feature-set" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc2e2fdebd77159b7a14ee45c9dbb3f1d202e8e7ccc14e4cda78c006a7a78a9" + +[[package]] +name = "solana-svm-type-overrides" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b78cd0bfb102d4197ce8c590f800a119ba0d358369ca57b0f66e94d1317fd0e" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "solana-system-interface" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e1790547bfc3061f1ee68ea9d8dc6c973c02a163697b24263a8e9f2e6d4afa2" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-instruction", + "solana-msg", + "solana-program-error", + "solana-pubkey 3.0.0", +] + +[[package]] +name = "solana-sysvar" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6690d3dd88f15c21edff68eb391ef8800df7a1f5cec84ee3e8d1abf05affdf74" +dependencies = [ + "base64 0.22.1", + "bincode 1.3.3", + "lazy_static 1.5.0", + "serde", + "serde_derive", + "solana-account-info", + "solana-clock", + "solana-define-syscall 4.0.1", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-hash 4.3.0", + "solana-instruction", + "solana-last-restart-slot", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-pubkey 4.2.0", + "solana-rent", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-slot-hashes", + "solana-slot-history", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sysvar-id" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17358d1e9a13e5b9c2264d301102126cf11a47fd394cdf3dec174fe7bc96e1de" +dependencies = [ + "solana-address 2.6.0", + "solana-sdk-ids", +] + +[[package]] +name = "solana-time-utils" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced92c60aa76ec4780a9d93f3bd64dfa916e1b998eacc6f1c110f3f444f02c9" + +[[package]] +name = "solana-tls-utils" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e195a1cd2dbb54fb234abb1fd5c92021152669f0d8853075a52b67bc9e7095" +dependencies = [ + "rustls 0.23.37", + "solana-keypair", + "solana-pubkey 3.0.0", + "solana-signer", + "x509-parser 0.14.0", +] + +[[package]] +name = "solana-tpu-client" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71b3e8d4db00daee1efb357c2e810ce07fd5f0d9167c722434b77c4f9aa668b1" +dependencies = [ + "async-trait", + "bincode 1.3.3", + "futures-util", + "indexmap 2.13.0", + "indicatif", + "log", + "rayon", + "solana-client-traits", + "solana-clock", + "solana-commitment-config", + "solana-connection-cache", + "solana-epoch-schedule", + "solana-measure", + "solana-message", + "solana-net-utils", + "solana-pubkey 3.0.0", + "solana-pubsub-client", + "solana-quic-definitions", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-signature", + "solana-signer", + "solana-transaction", + "solana-transaction-error", + "thiserror 2.0.18", + "tokio 1.50.0", +] + +[[package]] +name = "solana-transaction" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96697cff5075a028265324255efed226099f6d761ca67342b230d09f72cc48d2" +dependencies = [ + "bincode 1.3.3", + "serde", + "serde_derive", + "solana-address 2.6.0", + "solana-hash 4.3.0", + "solana-instruction", + "solana-instruction-error", + "solana-message", + "solana-sanitize", + "solana-sdk-ids", + "solana-short-vec", + "solana-signature", + "solana-signer", + "solana-transaction-error", +] + +[[package]] +name = "solana-transaction-context" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a3c3a69688293a195b02c60a5384d855b8de19981f404c71ccb9e7f139b98f" +dependencies = [ + "bincode 1.3.3", + "serde", + "solana-account", + "solana-instruction", + "solana-instructions-sysvar", + "solana-pubkey 3.0.0", + "solana-rent", + "solana-sbpf", + "solana-sdk-ids", +] + +[[package]] +name = "solana-transaction-error" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a2165ad25b694c654d5395fc7a049452a192376e4c96a7fad05580f6ba5ba1c" +dependencies = [ + "serde", + "serde_derive", + "solana-instruction-error", + "solana-sanitize", +] + +[[package]] +name = "solana-transaction-metrics-tracker" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcfdbabf25f0b1335c4042924011f1be7d93b668d3315b12bbb33781ad388adc" +dependencies = [ + "base64 0.22.1", + "bincode 1.3.3", + "log", + "rand 0.8.5", + "solana-packet", + "solana-perf", + "solana-short-vec", + "solana-signature", +] + +[[package]] +name = "solana-transaction-status-client-types" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f7c3d15f25111cd1e320ad8ee3515ccae9983ab750cec83517c8553419b70a" +dependencies = [ + "base64 0.22.1", + "bincode 1.3.3", + "bs58", + "serde", + "serde_json", + "solana-account-decoder-client-types", + "solana-commitment-config", + "solana-instruction", + "solana-message", + "solana-pubkey 3.0.0", + "solana-reward-info", + "solana-signature", + "solana-transaction", + "solana-transaction-context", + "solana-transaction-error", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-udp-client" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e01a3aa2c2f0c1cd9e493e59ef9c7f0281cdf56845900927d3e3a021222bbab" +dependencies = [ + "async-trait", + "solana-connection-cache", + "solana-keypair", + "solana-net-utils", + "solana-streamer", + "solana-transaction-error", + "thiserror 2.0.18", + "tokio 1.50.0", +] + +[[package]] +name = "solana-version" +version = "3.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8b34a38f1aab0be22e3d9810c84229a5668a50839fc38e9a409912dabbc227" +dependencies = [ + "agave-feature-set", + "rand 0.8.5", + "semver 1.0.27", + "serde", + "solana-sanitize", + "solana-serde-varint", +] + +[[package]] +name = "solana-vote-interface" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6e123e16bfdd7a81d71b4c4699e0b29580b619f4cd2ef5b6aae1eb85e8979f" +dependencies = [ + "bincode 1.3.3", + "cfg_eval", + "num-derive", + "num-traits", + "serde", + "serde_derive", + "serde_with", + "solana-clock", + "solana-hash 3.1.0", + "solana-instruction", + "solana-instruction-error", + "solana-pubkey 3.0.0", + "solana-rent", + "solana-sdk-ids", + "solana-serde-varint", + "solana-serialize-utils", + "solana-short-vec", + "solana-system-interface", +] + +[[package]] +name = "solana-zero-copy" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a91404c7de468dd80658cdb5d894ec803d1092ea6e2bfdf84eee6f07559c0d" +dependencies = [ + "borsh", + "bytemuck", + "bytemuck_derive", +] + +[[package]] +name = "solana-zk-sdk" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9602bcb1f7af15caef92b91132ec2347e1c51a72ecdbefdaefa3eac4b8711475" +dependencies = [ + "aes-gcm-siv", + "base64 0.22.1", + "bincode 1.3.3", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek 4.1.3", + "getrandom 0.2.17", + "itertools 0.12.1", + "js-sys", + "merlin", + "num-derive", + "num-traits", + "rand 0.8.5", + "serde", + "serde_derive", + "serde_json", + "sha3", + "solana-derivation-path", + "solana-instruction", + "solana-pubkey 3.0.0", + "solana-sdk-ids", + "solana-seed-derivable", + "solana-seed-phrase", + "solana-signature", + "solana-signer", + "subtle", + "thiserror 2.0.18", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "simple-aggregator" -version = "2.8.0" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" dependencies = [ - "alloy-primitives", - "example-helpers", - "example-types", - "layer-climb-address", - "serde", - "wasip2", - "wavs-types", - "wavs-wasi-utils", - "wit-bindgen 0.53.1", - "wstd", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", ] [[package]] -name = "siphasher" -version = "0.3.11" +name = "spdx" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] [[package]] -name = "siphasher" -version = "1.0.2" +name = "spinning_top" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] [[package]] -name = "sized-chunks" -version = "0.6.5" +name = "spki" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ - "bitmaps", - "typenum", + "base64ct", + "der", ] [[package]] -name = "slab" -version = "0.4.12" +name = "spl-discriminator" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "e597c5ff9ed7c74a54dbc47bae2f06e4db8c98f4356ad280200dc11878266db1" +dependencies = [ + "bytemuck", + "solana-program-error", + "solana-sha256-hasher", + "spl-discriminator-derive", +] [[package]] -name = "slotmap" -version = "1.1.1" +name = "spl-discriminator-derive" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750" dependencies = [ - "version_check", + "quote", + "spl-discriminator-syn", + "syn 2.0.117", ] [[package]] -name = "smallvec" -version = "1.15.1" +name = "spl-discriminator-syn" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "5d1dbc82ab91422345b6df40a79e2b78c7bce1ebb366da323572dd60b7076b67" dependencies = [ - "serde", + "proc-macro2", + "quote", + "sha2 0.10.9", + "syn 2.0.117", + "thiserror 1.0.69", ] [[package]] -name = "snow" -version = "0.9.6" +name = "spl-generic-token" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" +checksum = "233df81b75ab99b42f002b5cdd6e65a7505ffa930624f7096a7580a56765e9cf" dependencies = [ - "aes-gcm", - "blake2 0.10.6", - "chacha20poly1305", - "curve25519-dalek 4.1.3", - "rand_core 0.6.4", - "ring", - "rustc_version 0.4.1", - "sha2 0.10.9", - "subtle", + "bytemuck", + "solana-pubkey 3.0.0", ] [[package]] -name = "socket2" -version = "0.3.19" +name = "spl-pod" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +checksum = "2f9c6e142cdf1e7e77f480053ec9f0ce989890768ddf91f619b50f39d1b456f5" dependencies = [ - "cfg-if 1.0.4", - "libc", - "winapi 0.3.9", + "borsh", + "bytemuck", + "bytemuck_derive", + "num-derive", + "num-traits", + "num_enum", + "solana-program-error", + "solana-program-option", + "solana-pubkey 3.0.0", + "solana-zero-copy", + "solana-zk-sdk", + "thiserror 2.0.18", ] [[package]] -name = "socket2" -version = "0.5.10" +name = "spl-token-2022-interface" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "2fcd81188211f4b3c8a5eba7fd534c7142f9dd026123b3472492782cc72f4dc6" dependencies = [ - "libc", - "windows-sys 0.52.0", + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-account-info", + "solana-instruction", + "solana-program-error", + "solana-program-option", + "solana-program-pack", + "solana-pubkey 3.0.0", + "solana-sdk-ids", + "solana-zk-sdk", + "spl-pod", + "spl-token-confidential-transfer-proof-extraction", + "spl-token-confidential-transfer-proof-generation", + "spl-token-group-interface", + "spl-token-metadata-interface", + "spl-type-length-value", + "thiserror 2.0.18", ] [[package]] -name = "socket2" -version = "0.6.3" +name = "spl-token-confidential-transfer-proof-extraction" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "879a9ebad0d77383d3ea71e7de50503554961ff0f4ef6cbca39ad126e6f6da3a" dependencies = [ - "libc", - "windows-sys 0.61.2", + "bytemuck", + "solana-account-info", + "solana-curve25519", + "solana-instruction", + "solana-instructions-sysvar", + "solana-msg", + "solana-program-error", + "solana-pubkey 3.0.0", + "solana-sdk-ids", + "solana-zk-sdk", + "spl-pod", + "thiserror 2.0.18", ] [[package]] -name = "softbuffer" -version = "0.4.8" +name = "spl-token-confidential-transfer-proof-generation" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +checksum = "a0cd59fce3dc00f563c6fa364d67c3f200d278eae681f4dc250240afcfe044b1" dependencies = [ - "bytemuck", - "js-sys", - "ndk", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "objc2-quartz-core", - "raw-window-handle", - "redox_syscall 0.5.18", - "tracing", - "wasm-bindgen", - "web-sys", - "windows-sys 0.61.2", + "curve25519-dalek 4.1.3", + "solana-zk-sdk", + "thiserror 2.0.18", ] [[package]] -name = "soup3" -version = "0.5.0" +name = "spl-token-group-interface" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +checksum = "841cbd6f2322d02719be4da1affedbe6495b1048b7b985ec9796032564026e22" dependencies = [ - "futures-channel", - "gio", - "glib", - "libc", - "soup3-sys", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-address 2.6.0", + "solana-instruction", + "solana-nullable", + "solana-program-error", + "solana-zero-copy", + "spl-discriminator", + "thiserror 2.0.18", ] [[package]] -name = "soup3-sys" -version = "0.5.0" +name = "spl-token-interface" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +checksum = "8c564ac05a7c8d8b12e988a37d82695b5ba4db376d07ea98bc4882c81f96c7f3" dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-instruction", + "solana-program-error", + "solana-program-option", + "solana-program-pack", + "solana-pubkey 3.0.0", + "solana-sdk-ids", + "thiserror 2.0.18", ] [[package]] -name = "spdx" -version = "0.10.9" +name = "spl-token-metadata-interface" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +checksum = "9c467c7c3bd056f8fe60119e7ec34ddd6f23052c2fa8f1f51999098063b72676" dependencies = [ - "smallvec", + "borsh", + "num-derive", + "num-traits", + "solana-borsh", + "solana-instruction", + "solana-program-error", + "solana-pubkey 3.0.0", + "spl-discriminator", + "spl-pod", + "spl-type-length-value", + "thiserror 2.0.18", ] [[package]] -name = "spki" -version = "0.7.3" +name = "spl-type-length-value" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +checksum = "2504631748c48d2a937414d64a12dcac4588d34bd07d355d648619c189d29435" dependencies = [ - "base64ct", - "der", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-account-info", + "solana-program-error", + "solana-zero-copy", + "spl-discriminator", + "thiserror 2.0.18", ] [[package]] @@ -11733,7 +14131,7 @@ dependencies = [ "core-graphics", "crossbeam-channel", "dispatch2", - "dlopen2", + "dlopen2 0.8.2", "dpi", "gdkwayland-sys", "gdkx11-sys", @@ -11804,7 +14202,7 @@ dependencies = [ "glob", "gtk", "heck 0.5.0", - "http", + "http 1.4.0", "http-range", "jni", "libc", @@ -11969,7 +14367,7 @@ dependencies = [ "cookie", "dpi", "gtk", - "http", + "http 1.4.0", "jni", "objc2", "objc2-ui-kit", @@ -11992,7 +14390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" dependencies = [ "gtk", - "http", + "http 1.4.0", "jni", "log", "objc2", @@ -12024,7 +14422,7 @@ dependencies = [ "dunce", "glob", "html5ever 0.29.1", - "http", + "http 1.4.0", "infer", "json-patch", "kuchikiki", @@ -12469,6 +14867,7 @@ dependencies = [ "tokio 1.50.0", "tokio-rustls 0.26.4", "tungstenite 0.28.0", + "webpki-roots 0.26.11", ] [[package]] @@ -12481,6 +14880,7 @@ dependencies = [ "futures-core", "futures-io", "futures-sink", + "futures-util", "pin-project-lite 0.2.17", "tokio 1.50.0", ] @@ -12618,7 +15018,7 @@ dependencies = [ "base64 0.22.1", "bytes 1.11.1", "h2", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -12648,7 +15048,7 @@ dependencies = [ "async-trait", "base64 0.22.1", "bytes 1.11.1", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -12686,7 +15086,7 @@ dependencies = [ "byteorder", "bytes 1.11.1", "futures-util", - "http", + "http 1.4.0", "http-body", "http-body-util", "httparse", @@ -12732,13 +15132,18 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ + "async-compression", "bitflags 2.11.0", "bytes 1.11.1", + "futures-core", "futures-util", - "http", + "http 1.4.0", "http-body", + "http-body-util", "iri-string", "pin-project-lite 0.2.17", + "tokio 1.50.0", + "tokio-util", "tower", "tower-layer", "tower-service", @@ -12823,7 +15228,7 @@ version = "0.32.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc2a7ad7b8bd011f482d1fdf95be20377cdd19f45aa9d1f9f902d746eddc3cad" dependencies = [ - "http", + "http 1.4.0", "opentelemetry", "opentelemetry-semantic-conventions", "tracing", @@ -12926,7 +15331,7 @@ checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" dependencies = [ "bytes 1.11.1", "data-encoding", - "http", + "http 1.4.0", "httparse", "log", "rand 0.9.2", @@ -12945,7 +15350,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes 1.11.1", "data-encoding", - "http", + "http 1.4.0", "httparse", "log", "rand 0.9.2", @@ -12954,6 +15359,7 @@ dependencies = [ "sha1", "thiserror 2.0.18", "utf-8", + "webpki-roots 0.26.11", ] [[package]] @@ -13128,6 +15534,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "universal-hash" version = "0.5.1" @@ -13138,6 +15550,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -13168,6 +15589,16 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" +[[package]] +name = "uriparse" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" +dependencies = [ + "fnv", + "lazy_static 1.5.0", +] + [[package]] name = "url" version = "2.5.8" @@ -13233,7 +15664,7 @@ dependencies = [ "cid", "const-hex", "cw-wavs-mock-api", - "dashmap", + "dashmap 6.1.0", "deadpool", "dirs 6.0.0", "dotenvy", @@ -13251,6 +15682,8 @@ dependencies = [ "serde", "serde_json", "shellexpand", + "solana-client", + "solana-commitment-config", "subtle", "temp-env", "tempfile", @@ -13378,6 +15811,12 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "vswhom" version = "0.1.0" @@ -13860,7 +16299,7 @@ dependencies = [ "bytes 1.11.1", "etcetera", "futures-util", - "http", + "http 1.4.0", "semver 1.0.27", "serde", "serde_json", @@ -14284,7 +16723,7 @@ dependencies = [ "async-trait", "bytes 1.11.1", "futures", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -14389,7 +16828,8 @@ dependencies = [ "axum-tracing-opentelemetry", "base64 0.22.1", "bimap", - "bincode", + "bincode 2.0.1", + "bs58", "chrono", "clap 4.6.0", "const-hex", @@ -14398,7 +16838,7 @@ dependencies = [ "cron", "crossbeam", "ctrlc", - "dashmap", + "dashmap 6.1.0", "example-types", "futures", "futures-lite 1.13.0", @@ -14415,6 +16855,11 @@ dependencies = [ "serde", "serde_json", "slotmap", + "solana-client", + "solana-commitment-config", + "solana-pubkey 3.0.0", + "solana-rpc-client-api", + "solana-transaction-status-client-types", "tauri", "tempfile", "thiserror 2.0.18", @@ -14486,6 +16931,7 @@ dependencies = [ "alloy-primitives", "alloy-provider", "anyhow", + "bs58", "cfg-if 1.0.4", "clap 4.6.0", "const-hex", @@ -14498,6 +16944,9 @@ dependencies = [ "serde", "serde_json", "shellexpand", + "solana-client", + "solana-commitment-config", + "solana-pubkey 3.0.0", "tempfile", "tokio 1.50.0", "tracing", @@ -14533,7 +16982,7 @@ dependencies = [ "anyhow", "const-hex", "crossbeam", - "dashmap", + "dashmap 6.1.0", "example-types", "iri-string", "layer-climb", @@ -14606,7 +17055,8 @@ dependencies = [ "alloy-sol-types", "anyhow", "async-trait", - "bincode", + "bincode 2.0.1", + "bs58", "cfg-if 1.0.4", "chrono", "const-hex", @@ -14643,7 +17093,7 @@ dependencies = [ "anyhow", "cfg-if 1.0.4", "futures-utils-wasm", - "http", + "http 1.4.0", "serde", "serde_json", "tower-service", @@ -14726,6 +17176,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -14903,6 +17362,31 @@ dependencies = [ "wasmtime-internal-cranelift", ] +[[package]] +name = "wincode" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37095eb18dd6254c66217edc61a29d83d51f8818de8a2ffe88e4584ad73fb5f9" +dependencies = [ + "pastey", + "proc-macro2", + "quote", + "thiserror 2.0.18", + "wincode-derive", +] + +[[package]] +name = "wincode-derive" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262d55d1261f31e2cfe49cc6385a421d14d99faa0526bbe3cc1bda0d3005c62" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "window-vibrancy" version = "0.6.0" @@ -15778,7 +18262,7 @@ dependencies = [ "dunce", "gdkx11", "gtk", - "http", + "http 1.4.0", "javascriptcore-rs", "jni", "libc", @@ -15845,7 +18329,7 @@ dependencies = [ "async-task", "bytes 1.11.1", "futures-lite 1.13.0", - "http", + "http 1.4.0", "http-body", "http-body-util", "itoa", @@ -15909,18 +18393,36 @@ dependencies = [ "zeroize", ] +[[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 1.5.0", + "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 1.5.0", "nom", - "oid-registry", + "oid-registry 0.8.1", "rusticata-macros", "thiserror 2.0.18", "time", diff --git a/Cargo.toml b/Cargo.toml index 3fa1183c6..5b6559d77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = [ "examples/components/_types", "examples/components/kv-store", "examples/components/simple-aggregator", + "examples/components/solana-event-relay", "examples/components/timer-aggregator", "examples/contracts/cosmwasm/trigger/api", "examples/contracts/cosmwasm/trigger/simple", @@ -28,6 +29,11 @@ members = [ "examples/contracts/cosmwasm/mock/service-handler", "app/src-tauri", ] +exclude = [ + # Solana fixture program (Anchor) — has its own workspace; built via + # `anchor build`, not `cargo build --workspace`. See README. + "examples/contracts/solana/event-emitter", +] resolver = "2" [profile.dev] @@ -150,6 +156,28 @@ alloy-eip2930 = "=0.2.1" alloy-eip7702 = "=0.6.1" alloy-json-rpc = "=1.6.3" +# Solana-specific dependencies (used by the trigger stream; v1 is +# trigger-only, so we intentionally avoid pulling in the full solana-sdk +# crate constellation - just the pubsub client, RPC client, and the +# Pubkey / commitment primitives we need). +solana-client = "3.1.14" +solana-pubkey = "3" +solana-commitment-config = "3" +solana-rpc-client-api = "3.1.14" +solana-transaction-status-client-types = "3.1.14" + +# Slice 3: transaction-construction primitives. Only pulled into the +# layer-tests + integration-test crates so the runtime trigger surface +# stays slim. +solana-keypair = "3.1.2" +solana-signer = "3" +solana-signature = "3" +solana-message = "3" +solana-transaction = "3" +solana-instruction = "3" +solana-hash = "4" +solana-sha256-hasher = "3" + layer-climb = "0.9.0" layer-climb-address = { version = "0.9.0", features = [ "cw-storage", @@ -205,6 +233,7 @@ bip39 = { version = "2.2.0", features = ["rand"] } sha2 = "0.10.9" const-hex = "1.16.0" base64 = "0.22" +bs58 = "0.5.1" ripemd = "0.1.3" hypercore-protocol = { version = "0.6.1", default-features = false, features = [ "tokio", diff --git a/checksums.txt b/checksums.txt index 70bb79844..f86f6d413 100644 --- a/checksums.txt +++ b/checksums.txt @@ -1,10 +1,10 @@ -5306b31c131cbbfc07a49f0ab43774c13ef5da1ff584c1af549bb7dff16d2223 ./examples/build/components/chain_trigger_lookup.wasm -4c828e793e771289e820ea4227712ea51eb5fe8ce050ac6ceb7c35a04a7e3a72 ./examples/build/components/cosmos_query.wasm -ae5956a15e43f9f0f1e33b457829fa72d6343a4dd20e6049d209b0849f941cb1 ./examples/build/components/echo_block_interval.wasm -1bc1fa1af778d535a00ce2174ae3fa24ddd2ef961abe859832959155efb08636 ./examples/build/components/echo_cron_interval.wasm -8e1ec1471fe0845a2d8d158f6610f6f6134d4f090569b7b1911fcc7258f8eb01 ./examples/build/components/echo_data.wasm -3ccd4f8b6817dcb2c28e688effea2597590228ff7060d180205240db0765a333 ./examples/build/components/kv_store.wasm -88f28f046e2e379032a93d6a98abb70b6fa7a037e23ddd4382a14dfe25c48d37 ./examples/build/components/permissions.wasm -f9e1c1144bbfbfe7139d9a0be6890452058c1a700ce96bb5b6fc8f50535109f1 ./examples/build/components/simple_aggregator.wasm -c2d31fe21f971a71983354c32127852af5cff011dd4686abf98c956d3361b682 ./examples/build/components/square.wasm -1fc8363742b207eb7ddb32caa629e1e8ac7a953e755afb85c67fa0d729923f66 ./examples/build/components/timer_aggregator.wasm +73ecaeaf63e4c8c8f74bbc704ed43a76d8177cedaed315f571c3e19f67594efa ./examples/build/components/chain_trigger_lookup.wasm +cedee652e4f929b6447819a555f2a52164329650dcac6ab2e93ac25c3e80e2d1 ./examples/build/components/cosmos_query.wasm +a4c1f583fa06169adab882bb548f8bf4b733915f508d1fe9a32d048cb7022093 ./examples/build/components/echo_block_interval.wasm +ffe6158696dd2bf42fefdddb4a5907be9d22ec088eb89bca70989f921f9499f2 ./examples/build/components/echo_cron_interval.wasm +130f70f789a180bcab9292ab884af05793301c6cb859ddaad99bef694dfa5413 ./examples/build/components/echo_data.wasm +bf2634ca72b738facfe503e9f5553f381a15883b4c232fbe7a9c379efe71cedd ./examples/build/components/kv_store.wasm +f89ff8172f04af1e88cf5b7be7b7fbe6cebe453ca999917cc5bb7c6c7de32a5f ./examples/build/components/permissions.wasm +c04c984c1b7e767dd2ff70e5a8122016d1b5f3edc216a72e765ed50fab5f6cf4 ./examples/build/components/simple_aggregator.wasm +dbd0d89e2738c31daf7880caff76d39dc7254b42ff37792e55459f71193b686b ./examples/build/components/square.wasm +47e7e0d8ac19bcc2f6a3a08d454224c6b0badd681dc3a2859612ae969fd1d868 ./examples/build/components/timer_aggregator.wasm diff --git a/docs/design/svm-support.md b/docs/design/svm-support.md new file mode 100644 index 000000000..34b15f989 --- /dev/null +++ b/docs/design/svm-support.md @@ -0,0 +1,144 @@ +# Solana / SVM Support — Design Doc + +**Status:** Draft, 2026-05-17 +**Tracking issue:** [Lay3rLabs/WAVS#1149](https://github.com/Lay3rLabs/WAVS/issues/1149) +**Scope:** Boundaries and v1 scope. Not a spec — does not define PDAs, WIT diffs, or program layouts. + +## Summary + +WAVS supports EVM and Cosmos chains today. This doc proposes making Solana a first-class chain alongside them, in two phases: + +- **v1 — trigger-only.** A Solana program event triggers a WAVS component; submission happens through the existing EVM or Cosmos paths. No on-chain Solana writes, no middleware program. Validates abstraction boundaries before committing to the harder half. +- **v2 — full bidi.** Solana submission path, middleware program (signature verification + operator registry + replay protection on-program), and a `wavs-anchor-template`. Open decisions on signature scheme, operator-set shape, and Anchor-vs-native are listed but not resolved in this doc. + +The v1/v2 split is deliberate. Trigger-only is roughly one-third of the work and unlocks "Solana event → EVM handler" demos that prove the design without on-chain Solana write infrastructure. + +## Context + +Issue #1149 asks for SVM support that is **a peer of EVM and Cosmos, not an overload of either.** Eight implementation areas are called out: chain config, trigger source, submission target, service-manager program, operator signing/quorum, WIT/SDK exposure, CLI/service-JSON/UX, and a test harness against `solana-test-validator`. The acceptance criteria in the issue all sit in the trigger half except the service-manager program and the submission path. + +The repo already contains an empty toehold: `contracts/svm-middleware/` is an Anchor 0.32.1 scaffold with a single empty `initialize` instruction (`programs/svm-middleware/src/lib.rs`, 16 lines) and a `PLAN.md` (7 lines) committing to "two programs, BLS + ECDSA, mirroring `cw-middleware` and `poa-middleware`." Nothing in `WAVS/packages/` references Solana, SVM, or Anchor today. The only `ed25519` mentions are "future:" comments in `packages/types/src/service.rs` (around lines 524 and 560). + +## Abstraction Boundaries + +These are the seams in WAVS core where chain type forks today. Every one needs a Solana arm in either v1 or v2. + +| Concern | File | What forks today | +|---|---|---| +| Chain config | `packages/types/src/chain_config.rs:85` | `AnyChainConfig::{Cosmos, Evm}` | +| Service manager | `packages/types/src/service.rs:102` | `ServiceManager::{Evm, Cosmos}` | +| Trigger (declared) | `packages/types/src/service.rs` | `Trigger::{EvmContractEvent, CosmosContractEvent, BlockInterval, Cron, AtProtoEvent, HypercoreAppend, Manual}` | +| TriggerData (delivered) | `packages/types/src/service.rs:446` | mirror of the above, plus `Raw` | +| Signature kind | `packages/types/src/service.rs:558` | `SignatureAlgorithm::Secp256k1`, `SignaturePrefix::Eip191` only | +| Submission path | `packages/wavs/src/subsystems/submission.rs` | EVM-signer only (`MissingEvmSigner` at :161, `FailedToCreateEvmSigner` at :259) | +| Trigger streams | `packages/wavs/src/subsystems/trigger/streams/` | `evm_stream.rs`, `cosmos_stream.rs`, `atproto_jetstream.rs`, `hypercore_stream.rs`, `cron_stream.rs`, `local_command_stream.rs` | +| Engine ↔ component bindings | `packages/engine/src/bindings/types/` | mirrors the type enums | +| WIT | `wit-definitions/types/wit/{service,events,chain}.wit` | `service-manager`, `trigger`, `trigger-data`, address types | +| CLI service JSON validation | `packages/cli/src/service_json.rs` | per-variant matches | +| WASI helper utils | `packages/wasi-utils/src/evm/` | EVM-only `event.rs`, `provider.rs` | + +Two things to note: + +1. The trigger surface is well-trodden. Non-chain triggers (`Cron`, `AtProtoEvent`, `HypercoreAppend`) already live next to the EVM/Cosmos triggers, so adding a Solana trigger source does not require carving new abstraction — it follows an existing pattern. +2. The submission surface is **single-target and EVM-shaped.** `submission.rs` ties the signer type to EVM. v2 will need a Solana submitter alongside it. + +## v1 — Trigger-Only Solana Support + +### Goal + +A WAVS service can declare a Solana chain and a Solana program-event trigger. When the configured Solana program emits the matching event at the configured commitment level, WAVS observes it exactly once, hands typed trigger data to the operator component via WIT bindings, and the operator submits back through an existing EVM or Cosmos service manager. + +The "exactly once" property is enforced by a trigger identity composed of `(slot, signature, instruction_index, inner_instruction_index, log_index)` — sufficient to dedupe across reorgs at the configured commitment. + +### Work Breakdown (inventory only — no code in this doc) + +- **Types** — add `AnyChainConfig::Solana(SolanaChainConfig)`, `Trigger::SolanaProgramEvent { chain, program_id, event_discriminator | log_filter, commitment }`, and matching `TriggerData::SolanaProgramEvent { chain, slot, signature, instruction_index, inner_instruction_index, log_index, data }`. Extend `type_name` and `chain()` accessors. +- **Trigger stream** — new `solana_stream.rs` peer to `evm_stream.rs` and `cosmos_stream.rs`, using `solana_client::nonblocking::pubsub_client::PubsubClient` (`logs_subscribe`, optionally `program_subscribe` for state-change triggers in a later iteration). Commitment is a config knob, default `confirmed`. +- **Lookup** — extend `subsystems/trigger/lookup.rs` to key Solana triggers by `(chain, program_id, event_discriminator)`. +- **WIT** — add `solana-address`/`pubkey` type in `chain.wit`, a `solana-program-event` record in `events.wit`, and the corresponding `trigger`/`trigger-data` variants in `service.wit`. Regenerate component bindings. +- **CLI** — branch in `service_json.rs` validators for the new variant; surface in service-JSON examples. +- **Dev loop** — `solana-test-validator` wired into a `justfile` target alongside `start-anvil`. A worked example (Solana program → WAVS component → EVM submission) that doubles as the integration test. + +### Acceptance Criteria + +1. A service.json can declare `chain: solana`, configure RPC/commitment, and a `SolanaProgramEvent` trigger. +2. A test fixture program running on `solana-test-validator` emits an event; the WAVS node observes it. +3. The matching operator component receives typed `TriggerData::SolanaProgramEvent` via WIT bindings. +4. Replay protection: the same `(slot, signature, instruction_index, log_index)` does not re-fire the operator. Verified by deliberately reconnecting the subscription. +5. Submission to an EVM service manager from the operator works end-to-end. +6. `just` target spins up `solana-test-validator` + WAVS + the EVM submission target locally. + +### Non-Goals for v1 + +- No Solana submission target. Operators must submit to EVM or Cosmos. +- No Solana middleware program work beyond what is already in `contracts/svm-middleware/`. +- No Solana operator key support. Operators continue to sign with their EVM key; the trigger doesn't care. +- No reorg recovery beyond what `confirmed`/`finalized` commitment provides. Reorgs above the chosen commitment are treated as user error. +- No `programSubscribe`/state-change triggers in v1 — log/event triggers only. State triggers are a v1.5 follow-up if demand exists. + +## v2 Sketch — Submission + Middleware Program + +This section is non-binding. It frames the work so v1 boundaries don't paint v2 into a corner. + +### Submission + +A `solana_submitter` peer to the EVM/Cosmos paths in `submission.rs`. Materially different mechanics from EVM: + +- Recent-blockhash refresh and expiry retry loop +- Compute budget + priority fee instructions +- Optional durable nonce account for high-latency submissions +- Idempotency must be enforced **on the program** (replay PDA) — Solana has no equivalent to EVM nonce-as-ordering +- New credential env var (`WAVS_AGGREGATOR_SOLANA_CREDENTIAL`) +- Fee-payer key separate from signing key, by convention + +### Middleware Program + +Today's `contracts/svm-middleware/` is a placeholder. The v2 program needs, at minimum: service-config PDA, operator/quorum registry PDA(s), submission replay PDA, a `submit` instruction that verifies aggregated signatures against the registry, and an upgrade-authority policy. + +Mirror reference: `contracts/cw-middleware/` and its Mock / ECDSA / BLS triad. The cleanest path likely follows the same progression: ship a POA variant first (operator addresses set by an admin key), then add signature-verifying variants once the operator-set shape is settled. + +### Operator Signing — Three Options, One Recommendation + +| Option | Pros | Cons | +|---|---|---| +| **Mirror EVM keys (ECDSA)** | Shared operator set across all chains. Works via `secp256k1_recover` syscall. | Operators sign for Solana with a non-native key — awkward UX. | +| **Native Ed25519** | Idiomatic on Solana, cheap on-chain verify via the Ed25519 sig-verify program. | Forks operator identity. Operators have to manage a second key. | +| **BLS aggregate** | Constant-size aggregate verification. | On Solana, BLS verification is hard — needs precompile or a custom verifier. Likely v3 territory. | + +**Recommended default:** start with POA (admin-managed operator set, no on-program signature verification — operator membership is the only check). Add ECDSA verification next, mirroring `poa-middleware → wavs-middleware → cw-middleware` progression. Defer BLS until there is a concrete need. + +This recommendation should be re-evaluated when v2 kicks off — it's the cheapest path to a working end-to-end demo but it is not the right answer for production multi-chain operator sets, which want either ECDSA-shared or BLS-aggregate. + +### Anchor vs. Native + +`contracts/svm-middleware/` is Anchor 0.32.1 today. Switching to native means rewriting the toehold. Recommendation: **stay on Anchor for v2 unless we hit a concrete blocker** — the existing scaffold, audit familiarity, and `wavs-anchor-template` future make Anchor the lower-risk choice. + +## Open Decisions + +Listed with recommended defaults so v1 isn't blocked. Each decision is revisitable before v2 kickoff. + +| # | Decision | Recommended default | Revisit before | +|---|---|---|---| +| 1 | v1 scope: trigger-only vs. full bidi | **Trigger-only** | — (this doc decides) | +| 2 | Trigger commitment level | `confirmed` for triggers, `finalized` for v2 submission | v1 implementation | +| 3 | Anchor vs. native program | Anchor | v2 kickoff | +| 4 | Signature scheme | POA first, then ECDSA-mirrored, then BLS | v2 kickoff | +| 5 | Operator registry shape | Mirror cw-middleware progression (Mock/ECDSA/BLS) | v2 kickoff | +| 6 | Driving example | "Solana event → EVM handler" demo | v1 kickoff | +| 7 | State-change triggers (`programSubscribe`) | Deferred to v1.5 | v1.5 if demand exists | + +## Non-Goals for This Doc + +- Does not specify WIT diffs. v1 implementation owns the exact records and field names. +- Does not specify PDA layouts or program instructions. v2 design memo owns this. +- Does not commit to a final signature scheme. v2 design memo owns this. +- Does not specify operator key onboarding flow. Follows from the signature-scheme decision. +- Does not pick a Solana cluster strategy for hosted demos. Local `solana-test-validator` is sufficient for v1. + +## References + +- Issue: https://github.com/Lay3rLabs/WAVS/issues/1149 +- Existing scaffold: `contracts/svm-middleware/programs/svm-middleware/src/lib.rs`, `contracts/svm-middleware/PLAN.md` +- Mirror references: `contracts/poa-middleware/` (audited, simplest), `contracts/cw-middleware/` (Mock/ECDSA/BLS triad) +- Trigger-stream patterns to copy from: `packages/wavs/src/subsystems/trigger/streams/evm_stream.rs`, `cosmos_stream.rs`, `atproto_jetstream.rs` +- Boundary files: `packages/types/src/service.rs`, `packages/types/src/chain_config.rs`, `packages/wavs/src/subsystems/submission.rs`, `wit-definitions/types/wit/{service,events,chain}.wit` diff --git a/examples/build/components/chain_trigger_lookup.wasm b/examples/build/components/chain_trigger_lookup.wasm index 44f504831..4dbc19a95 100644 Binary files a/examples/build/components/chain_trigger_lookup.wasm and b/examples/build/components/chain_trigger_lookup.wasm differ diff --git a/examples/build/components/cosmos_query.wasm b/examples/build/components/cosmos_query.wasm index 1d4c938d7..ee506bd52 100644 Binary files a/examples/build/components/cosmos_query.wasm and b/examples/build/components/cosmos_query.wasm differ diff --git a/examples/build/components/echo_block_interval.wasm b/examples/build/components/echo_block_interval.wasm index 980d3817a..337846978 100644 Binary files a/examples/build/components/echo_block_interval.wasm and b/examples/build/components/echo_block_interval.wasm differ diff --git a/examples/build/components/echo_cron_interval.wasm b/examples/build/components/echo_cron_interval.wasm index 5307a77a5..6717c3163 100644 Binary files a/examples/build/components/echo_cron_interval.wasm and b/examples/build/components/echo_cron_interval.wasm differ diff --git a/examples/build/components/echo_data.wasm b/examples/build/components/echo_data.wasm index 6a21aadec..256c9fd16 100644 Binary files a/examples/build/components/echo_data.wasm and b/examples/build/components/echo_data.wasm differ diff --git a/examples/build/components/kv_store.wasm b/examples/build/components/kv_store.wasm index 14c38f300..71342bb98 100644 Binary files a/examples/build/components/kv_store.wasm and b/examples/build/components/kv_store.wasm differ diff --git a/examples/build/components/permissions.wasm b/examples/build/components/permissions.wasm index 8c3559483..f9505803d 100644 Binary files a/examples/build/components/permissions.wasm and b/examples/build/components/permissions.wasm differ diff --git a/examples/build/components/simple_aggregator.wasm b/examples/build/components/simple_aggregator.wasm index c4f526e3c..a237ee369 100644 Binary files a/examples/build/components/simple_aggregator.wasm and b/examples/build/components/simple_aggregator.wasm differ diff --git a/examples/build/components/square.wasm b/examples/build/components/square.wasm index ae24a14d9..ab253ec28 100644 Binary files a/examples/build/components/square.wasm and b/examples/build/components/square.wasm differ diff --git a/examples/build/components/timer_aggregator.wasm b/examples/build/components/timer_aggregator.wasm index 50b672d7e..4d06a452e 100644 Binary files a/examples/build/components/timer_aggregator.wasm and b/examples/build/components/timer_aggregator.wasm differ diff --git a/examples/components/solana-event-relay/Cargo.toml b/examples/components/solana-event-relay/Cargo.toml new file mode 100644 index 000000000..8dfd267ce --- /dev/null +++ b/examples/components/solana-event-relay/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "solana-event-relay" +edition.workspace = true +version.workspace = true +authors.workspace = true +rust-version.workspace = true +repository.workspace = true + +[dependencies] +example-helpers = { workspace = true } + +[lib] +crate-type = ["rlib", "cdylib"] + +[package.metadata.component] +package = "wavs-tests:solana-event-relay" diff --git a/examples/components/solana-event-relay/README.md b/examples/components/solana-event-relay/README.md new file mode 100644 index 000000000..5ef72dcbd --- /dev/null +++ b/examples/components/solana-event-relay/README.md @@ -0,0 +1,18 @@ +# `solana-event-relay` + +WAVS operator component for the v1 SVM trigger demo. Receives a +`TriggerData::SolanaProgramEvent` from the Solana trigger stream, strips +the Anchor `MessageEmitted` framing +(`sha256("event:MessageEmitted")[..8]` discriminator + borsh `Vec` +length prefix), and emits a `DataWithId`-encoded `WasmResponse` for the +existing `SimpleSubmit` EVM service handler. + +`triggerId` is the Solana slot — keeps the EVM `DataWithId` monotonic +per-slot and disambiguates distinct events without requiring the relay +to allocate its own ids. The full `(slot, signature, instruction_index, +inner_instruction_index, log_index)` replay-protection tuple is +enforced upstream in the dispatcher (see +`packages/wavs/src/subsystems/trigger.rs::SolanaReplayCache`). + +See `examples/contracts/solana/event-emitter/README.md` for the +end-to-end demo flow. diff --git a/examples/components/solana-event-relay/src/lib.rs b/examples/components/solana-event-relay/src/lib.rs new file mode 100644 index 000000000..08862a06c --- /dev/null +++ b/examples/components/solana-event-relay/src/lib.rs @@ -0,0 +1,106 @@ +//! WAVS Solana event-relay operator component (v1 trigger demo). +//! +//! Receives a [`TriggerData::SolanaProgramEvent`] from the Solana trigger +//! stream, strips the Anchor `MessageEmitted` framing (8-byte discriminator +//! + 4-byte little-endian length prefix from borsh's `Vec`), and emits +//! an EVM `DataWithId`-encoded `WasmResponse` so the relayed payload can +//! land in the example `SimpleSubmit` service handler. +//! +//! Why this component is its own crate: +//! - `example_helpers::trigger::decode_trigger_event` only knows about EVM / +//! Cosmos / ATProto / Hypercore variants today; it rejects +//! `SolanaProgramEvent`. Rather than add a Solana branch to the shared +//! helper before there is a worked example, this component does the +//! Solana-specific framing inline. +//! - The trigger id we hand to the EVM handler is the slot — sufficient +//! to disambiguate distinct events in the v1 demo and keep `DataWithId` +//! monotonic across replays. `(slot, signature, instruction_index, +//! log_index)` replay protection lives in the dispatcher. + +use example_helpers::bindings::world::{ + host, + wavs::{ + operator::{ + input::{TriggerAction, TriggerData}, + output::WasmResponse, + }, + types::events::TriggerDataSolanaProgramEvent, + }, + Guest, +}; +use example_helpers::export_layer_trigger_world; +use example_helpers::trigger::encode_trigger_output; + +struct Component; + +/// Anchor event discriminator for `MessageEmitted`: +/// `sha256("event:MessageEmitted")[..8]`. +/// +/// Pre-computed at build-time to avoid pulling sha2 into the WASI component. +/// Verified by the `solana_e2e` integration test (see +/// `packages/layer-tests/tests/solana_e2e.rs::anchor_event_discriminator_matches_relay_component`). +const MESSAGE_EMITTED_DISCRIMINATOR: [u8; 8] = [0xab, 0x0f, 0xdc, 0xb7, 0x2d, 0x7f, 0xb7, 0x27]; + +impl Guest for Component { + fn run(trigger_action: TriggerAction) -> std::result::Result, String> { + let TriggerDataSolanaProgramEvent { + slot, data: raw, .. + } = match trigger_action.data { + TriggerData::SolanaProgramEvent(ev) => ev, + other => { + return Err(format!( + "solana-event-relay expected SolanaProgramEvent, got {other:?}" + )); + } + }; + + let payload = strip_anchor_framing(&raw)?; + + // Use the slot as the trigger id. This is monotonic per-slot + // and disambiguates distinct events in the v1 demo; full replay + // protection is enforced at the dispatcher via the + // (slot, signature, instruction_index, log_index) tuple. + Ok(vec![encode_trigger_output( + slot, + payload, + host::get_service().service.manager, + )]) + } +} + +/// Strip Anchor `MessageEmitted` framing: +/// - first 8 bytes: discriminator (must match `MESSAGE_EMITTED_DISCRIMINATOR`) +/// - next 4 bytes: little-endian `u32` length prefix from borsh's `Vec` +/// - remaining bytes: the payload (length must match the prefix) +fn strip_anchor_framing(raw: &[u8]) -> Result, String> { + if raw.len() < 8 { + return Err(format!( + "payload too short ({} bytes) — expected at least 8 for the discriminator", + raw.len() + )); + } + let (disc, rest) = raw.split_at(8); + if disc != MESSAGE_EMITTED_DISCRIMINATOR { + return Err(format!( + "discriminator mismatch: got {disc:02x?}, expected {:02x?}", + MESSAGE_EMITTED_DISCRIMINATOR + )); + } + if rest.len() < 4 { + return Err(format!( + "missing borsh length prefix after discriminator (only {} bytes)", + rest.len() + )); + } + let (len_bytes, payload) = rest.split_at(4); + let claimed_len = u32::from_le_bytes(len_bytes.try_into().unwrap()) as usize; + if claimed_len != payload.len() { + return Err(format!( + "borsh length prefix {claimed_len} != actual payload length {}", + payload.len() + )); + } + Ok(payload.to_vec()) +} + +export_layer_trigger_world!(Component); diff --git a/examples/contracts/solana/event-emitter/.gitignore b/examples/contracts/solana/event-emitter/.gitignore new file mode 100644 index 000000000..279b3c1e9 --- /dev/null +++ b/examples/contracts/solana/event-emitter/.gitignore @@ -0,0 +1,10 @@ +.anchor +.DS_Store +target/ +node_modules/ +test-ledger/ + +# Anchor build/test artifacts +*.so +*-keypair.json +Cargo.lock diff --git a/examples/contracts/solana/event-emitter/Anchor.toml b/examples/contracts/solana/event-emitter/Anchor.toml new file mode 100644 index 000000000..241e80ab5 --- /dev/null +++ b/examples/contracts/solana/event-emitter/Anchor.toml @@ -0,0 +1,19 @@ +[toolchain] +package_manager = "yarn" + +[features] +resolution = true +skip-lint = false + +[programs.localnet] +event_emitter = "EvNt1111111111111111111111111111111111111111" + +[registry] +url = "https://api.apr.dev" + +[provider] +cluster = "localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +test = "echo 'No JS tests for fixture program; see packages/wavs/tests/solana_e2e.rs'" diff --git a/examples/contracts/solana/event-emitter/Cargo.toml b/examples/contracts/solana/event-emitter/Cargo.toml new file mode 100644 index 000000000..14a951cee --- /dev/null +++ b/examples/contracts/solana/event-emitter/Cargo.toml @@ -0,0 +1,15 @@ +[workspace] +members = [ + "programs/*" +] +resolver = "2" + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 + +[profile.release.build-override] +opt-level = 3 +incremental = false +codegen-units = 1 diff --git a/examples/contracts/solana/event-emitter/README.md b/examples/contracts/solana/event-emitter/README.md new file mode 100644 index 000000000..64f8051e3 --- /dev/null +++ b/examples/contracts/solana/event-emitter/README.md @@ -0,0 +1,69 @@ +# Solana fixture: `event-emitter` + +Minimal Anchor program used by the WAVS Solana (v1, trigger-only) demo and +by `packages/layer-tests/tests/solana_e2e.rs`. One instruction (`emit`) +takes a payload and emits a `MessageEmitted` Anchor event: + +```text +Program invoke [1] +Program log: Instruction: Emit +Program data: +Program success +``` + +The `Program data:` line is what the WAVS Solana trigger stream's +`SolanaEventFilter::Discriminator` filter selects on, using the +8-byte Anchor event discriminator `sha256("event:MessageEmitted")[..8]` +(value `0xab 0x0f 0xdc 0xb7 0x2d 0x7f 0xb7 0x27`). + +## Demo flow (Solana event → WAVS → EVM `SimpleSubmit`) + +Three terminals: + +```sh +# 1. Local Solana validator (RPC 8899, WS 8900) +just start-solana-validator + +# 2. Local EVM testnet (RPC 8545) +just start-anvil + +# 3. Build + deploy the fixture program +just deploy-solana-fixture +``` + +Then start WAVS configured with a service.json that has: + +- A `solana:devnet` chain entry pointing at `http://127.0.0.1:8899` / + `ws://127.0.0.1:8900` +- A `SolanaProgramEvent` trigger using the program id printed by + `anchor deploy` and `SolanaEventFilter::Discriminator` with the + `MessageEmitted` discriminator above +- The `solana-event-relay` operator component + (`examples/build/components/solana_event_relay.wasm`) +- An `Aggregator` submit pointing at a deployed `SimpleSubmit` service + handler on anvil + +Fire an `emit` instruction (any signer + a `Vec` payload). The +relay strips the discriminator + borsh length prefix, encodes a +`DataWithId { triggerId: slot, data: payload }`, and the aggregator +relays it to the EVM `SimpleSubmit` contract on anvil. + +## Toolchain + +- Anchor `0.32.1` (matches `contracts/svm-middleware/`) +- Solana CLI from the [Anza + installer](https://docs.anza.xyz/cli/install-solana-cli-tools): `sh + -c "$(curl -sSfL https://release.anza.xyz/stable/install)"` + +The justfile recipes check for both and print install hints if either +is missing — we do not auto-install. + +## What this is NOT + +- Not a v2 middleware program. The fixture has no operator registry, + signature verification, or submission path. That is `contracts/svm-middleware/` + in the parent workspace, and the design doc's v2 sketch tracks it. +- Not a production event shape. The single `Vec` payload is + deliberately minimal so the demo focuses on the trigger-stream and + WIT-bindings story. v2 will have realistic event shapes once the + submission path actually targets Solana. diff --git a/examples/contracts/solana/event-emitter/Xargo.toml b/examples/contracts/solana/event-emitter/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/examples/contracts/solana/event-emitter/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/examples/contracts/solana/event-emitter/programs/event-emitter/Cargo.toml b/examples/contracts/solana/event-emitter/programs/event-emitter/Cargo.toml new file mode 100644 index 000000000..58a9c6cc3 --- /dev/null +++ b/examples/contracts/solana/event-emitter/programs/event-emitter/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "event-emitter" +version = "0.1.0" +description = "WAVS Solana fixture program: emits an Anchor event used as a v1 trigger source" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "event_emitter" + +[features] +default = [] +cpi = ["no-entrypoint"] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +idl-build = ["anchor-lang/idl-build"] + +[dependencies] +anchor-lang = "0.32.1" + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] } diff --git a/examples/contracts/solana/event-emitter/programs/event-emitter/src/lib.rs b/examples/contracts/solana/event-emitter/programs/event-emitter/src/lib.rs new file mode 100644 index 000000000..d2e57aa57 --- /dev/null +++ b/examples/contracts/solana/event-emitter/programs/event-emitter/src/lib.rs @@ -0,0 +1,66 @@ +//! WAVS Solana fixture program — event-emitter. +//! +//! A minimal Anchor program used by the v1 SVM trigger demo and the +//! `solana_e2e` integration test in `packages/wavs/tests/`. +//! +//! Design constraints: +//! - One instruction (`emit`) that takes an opaque `payload: Vec` and +//! emits a single Anchor event (`MessageEmitted`) carrying that payload. +//! - Anchor's `emit!` macro logs the event as: +//! `Program data: ` +//! which is exactly the shape the WAVS Solana trigger stream's +//! `SolanaEventFilter::Discriminator` filter is designed to match. +//! - The 8-byte discriminator is `sha256("event:MessageEmitted")[..8]`; +//! the runtime expectation is documented in the demo README. +//! +//! v2 follow-ups (out of scope here): +//! - A `submit` instruction that writes results back, behind the +//! `contracts/svm-middleware` registry. +//! - Realistic event shapes (PDA-driven, multi-field) once the operator +//! submission path actually targets Solana. + +use anchor_lang::prelude::*; + +// Placeholder pubkey; replace on a real deploy with the output of +// `solana address -k target/deploy/event_emitter-keypair.json`. +declare_id!("EvNt1111111111111111111111111111111111111111"); + +#[program] +pub mod event_emitter { + use super::*; + + /// Emit a `MessageEmitted` event carrying `payload` bytes. + /// + /// The validator log this produces looks like: + /// + /// ```text + /// Program EvNt1111... invoke [1] + /// Program log: Instruction: Emit + /// Program data: + /// Program EvNt1111... consumed N of M compute units + /// Program EvNt1111... success + /// ``` + /// + /// The `Program data:` line is what the WAVS trigger stream's + /// `SolanaEventFilter::Discriminator` filter selects on. + pub fn emit(_ctx: Context, payload: Vec) -> Result<()> { + emit!(MessageEmitted { payload }); + Ok(()) + } +} + +/// No accounts needed — the instruction is pure log emission. The signer +/// is required only so the transaction is well-formed. +#[derive(Accounts)] +pub struct Emit<'info> { + pub signer: Signer<'info>, +} + +/// The single event this fixture program emits. Anchor will emit a +/// `Program data:` log line whose base64 payload is the 8-byte +/// `sha256("event:MessageEmitted")[..8]` discriminator followed by the +/// borsh-serialized `payload` field. +#[event] +pub struct MessageEmitted { + pub payload: Vec, +} diff --git a/justfile b/justfile index 6d0d959fa..fee44e053 100644 --- a/justfile +++ b/justfile @@ -247,6 +247,37 @@ dev-tool *args: start-anvil: anvil +# Requires the Solana CLI installed locally. We do NOT auto-install: +# the operator owns that decision. Install with: +# sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)" +# +# spin up a local solana-test-validator on RPC 8899 / WS 8900 (SVM trigger demo + solana_e2e test) +start-solana-validator: + @if ! command -v solana-test-validator >/dev/null 2>&1; then \ + echo "error: solana-test-validator not found on PATH."; \ + echo " install the Anza Solana toolchain first:"; \ + echo " sh -c \"\$(curl -sSfL https://release.anza.xyz/stable/install)\""; \ + echo " see examples/contracts/solana/event-emitter/README.md"; \ + exit 1; \ + fi + solana-test-validator --reset --quiet + +# Run `just start-solana-validator` first in another shell. Outputs +# the deployed program id (printed by `anchor deploy`) which the demo +# service.json template expects. +# +# build + deploy the event-emitter Anchor fixture program to the running solana-test-validator +deploy-solana-fixture: + @if ! command -v anchor >/dev/null 2>&1; then \ + echo "error: anchor CLI not found on PATH."; \ + echo " install with:"; \ + echo " cargo install --git https://github.com/coral-xyz/anchor anchor-cli --tag v0.32.1"; \ + echo " see examples/contracts/solana/event-emitter/README.md"; \ + exit 1; \ + fi + cd examples/contracts/solana/event-emitter && anchor build + cd examples/contracts/solana/event-emitter && anchor deploy + # e.g. `just cli-exec ./examples/build/components/echo_data.wasm "hello world"` cli-exec COMPONENT INPUT: @cd packages/cli && cargo run exec --component {{COMPONENT}} --input '{{INPUT}}' diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index bd5186a00..75a2b1556 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -32,6 +32,10 @@ cfg-if = { workspace = true } alloy-primitives = { workspace = true } alloy-provider = { workspace = true } alloy-json-abi = { workspace = true } +bs58 = { workspace = true } +solana-client = { workspace = true } +solana-commitment-config = { workspace = true } +solana-pubkey = { workspace = true } cron = { workspace = true } rand = { workspace = true } opentelemetry = { workspace = true } diff --git a/packages/cli/src/command/service.rs b/packages/cli/src/command/service.rs index ad92b31e3..fb7c9db93 100644 --- a/packages/cli/src/command/service.rs +++ b/packages/cli/src/command/service.rs @@ -1009,6 +1009,9 @@ pub async fn validate_service( Trigger::EvmContractEvent { chain, .. } => { chains_to_validate.insert((chain.clone(), ChainType::EVM)); } + Trigger::SolanaProgramEvent { chain, .. } => { + chains_to_validate.insert((chain.clone(), ChainType::Solana)); + } Trigger::BlockInterval { chain, start_block, @@ -1052,6 +1055,17 @@ pub async fn validate_service( )); } } + Some(AnyChainConfig::Solana(_)) => { + // Block-interval (slot-interval) triggers on + // Solana are a v1.5 follow-up per the design + // doc's "Non-Goals for v1" — see also the + // BlockInterval arm in + // `subsystems/trigger.rs`. + errors.push(format!( + "Workflow '{}' uses BlockInterval on Solana chain '{}', which is not yet supported (v1.5)", + workflow_id, chain + )); + } }, _ => {} } @@ -1083,6 +1097,7 @@ pub async fn validate_service( // Build maps of clients for chains actually used let mut cosmos_clients = HashMap::new(); let mut evm_providers = HashMap::new(); + let mut solana_clients = HashMap::new(); // Only get clients for chains actually used in triggers or submits for (chain, chain_type) in chains_to_validate.iter() { @@ -1097,17 +1112,23 @@ pub async fn validate_service( evm_providers.insert(chain.clone(), client.provider.root().clone()); } } + ChainType::Solana => { + if let Ok(client) = ctx.new_solana_rpc_client(chain.id.clone()) { + solana_clients.insert(chain.clone(), client); + } + } } } // Validate that referenced contracts exist on-chain - if !cosmos_clients.is_empty() || !evm_providers.is_empty() { + if !cosmos_clients.is_empty() || !evm_providers.is_empty() || !solana_clients.is_empty() { if let Err(err) = validate_contracts_exist( &service.name, triggers, service_manager, &evm_providers, &cosmos_clients, + &solana_clients, &mut errors, ) .await diff --git a/packages/cli/src/command/service/types.rs b/packages/cli/src/command/service/types.rs index 7aa74ce31..d0f4720cc 100644 --- a/packages/cli/src/command/service/types.rs +++ b/packages/cli/src/command/service/types.rs @@ -12,6 +12,7 @@ use wavs_types::{ pub enum ChainType { Cosmos, EVM, + Solana, } /// Result of service initialization @@ -165,6 +166,18 @@ impl std::fmt::Display for WorkflowTriggerResult { writeln!(f, " Trigger Type: Hypercore Append")?; writeln!(f, " Feed Key: {}", feed_key)?; } + Trigger::SolanaProgramEvent { + chain, + program_id, + filter, + commitment, + } => { + writeln!(f, " Trigger Type: Solana Program Event")?; + writeln!(f, " Chain: {}", chain)?; + writeln!(f, " Program ID: {}", program_id)?; + writeln!(f, " Filter: {:?}", filter)?; + writeln!(f, " Commitment: {}", commitment)?; + } } writeln!(f, " Updated: {}", self.file_path.display()) diff --git a/packages/cli/src/command/service/validate.rs b/packages/cli/src/command/service/validate.rs index 87c9c278f..3a4928d5f 100644 --- a/packages/cli/src/command/service/validate.rs +++ b/packages/cli/src/command/service/validate.rs @@ -2,6 +2,7 @@ use alloy_provider::{Provider, RootProvider}; use anyhow::Result; use layer_climb::{prelude::CosmosAddr, querier::QueryClient as CosmosQueryClient}; use reqwest::Client; +use solana_client::nonblocking::rpc_client::RpcClient as SolanaRpcClient; use std::collections::HashMap; use wavs_types::{ChainKey, ServiceManager, Trigger, WorkflowId}; @@ -98,6 +99,7 @@ pub async fn validate_contracts_exist( service_manager: Option<&ServiceManager>, evm_providers: &HashMap, cosmos_clients: &HashMap, + solana_clients: &HashMap, errors: &mut Vec, ) -> Result<()> { // Track which contracts we've already checked to avoid duplicate checks @@ -166,6 +168,42 @@ pub async fn validate_contracts_exist( )); } } + Trigger::SolanaProgramEvent { + chain, program_id, .. + } => { + // Check if we have a Solana RPC client for this chain + if let Some(client) = solana_clients.get(chain) { + let context = format!( + "Service {} workflow {} trigger Solana program", + service_name, workflow_id + ); + let pubkey = solana_pubkey::Pubkey::new_from_array(*program_id.as_bytes()); + match client.get_account(&pubkey).await { + Ok(account) => { + // The on-chain account must be executable for + // it to be a program. Non-executable accounts + // with the same pubkey wouldn't fire program + // logs; flag this as a warning so service + // authors can catch typos. + if !account.executable { + errors.push(format!( + "{context} {pubkey} exists but is not executable; the trigger will never fire" + )); + } + } + Err(err) => { + errors.push(format!( + "{context} {pubkey} could not be fetched from chain {chain}: {err}" + )); + } + } + } else { + errors.push(format!( + "Cannot check Solana program for workflow {} - no client configured for chain {}", + workflow_id, chain + )); + } + } // Other trigger types don't need contract validation Trigger::Cron { .. } | Trigger::Manual diff --git a/packages/cli/src/context.rs b/packages/cli/src/context.rs index f343ca01a..c922302c4 100644 --- a/packages/cli/src/context.rs +++ b/packages/cli/src/context.rs @@ -105,6 +105,50 @@ impl CliContext { Ok(evm_client) } + /// Construct a non-signing Solana RPC client for the given chain id. + /// Mirrors `new_evm_client_read_only` — Solana submission is out of + /// scope for v1, so we only need the read-side RPC client for + /// trigger / health validation paths. + pub(crate) fn new_solana_rpc_client( + &self, + chain_id: ChainKeyId, + ) -> Result { + let chain_config = self + .config + .chains + .read() + .map_err(|_| anyhow!("Chains lock is poisoned"))? + .solana + .get(&chain_id) + .context(format!("chain id {chain_id} not found"))? + .clone() + .build(chain_id); + + let http = chain_config + .http_endpoint + .as_deref() + .context("Solana chain has no http_endpoint")?; + + Ok( + solana_client::nonblocking::rpc_client::RpcClient::new_with_commitment( + http.to_string(), + solana_commitment_config::CommitmentConfig { + commitment: match chain_config.commitment { + wavs_types::SolanaCommitment::Processed => { + solana_commitment_config::CommitmentLevel::Processed + } + wavs_types::SolanaCommitment::Confirmed => { + solana_commitment_config::CommitmentLevel::Confirmed + } + wavs_types::SolanaCommitment::Finalized => { + solana_commitment_config::CommitmentLevel::Finalized + } + }, + }, + ), + ) + } + pub async fn new_cosmos_client(&self, chain_id: ChainKeyId) -> Result { let chain_config = self .config @@ -167,6 +211,54 @@ impl CliContext { .contract_info(&address) .await .is_ok(), + AnyChainConfig::Solana(solana_config) => { + // We treat the address bytes as a 32-byte Solana + // pubkey. `address_exists_on_chain` is only currently + // called for non-Solana paths (the type system enforces + // this — `layer_climb::Address` only has Cosmos/Evm + // variants today), so this arm is defensive plumbing + // for the day a Solana-aware caller exists. + let bytes = address.as_bytes(); + let Ok(pubkey_bytes) = <[u8; 32]>::try_from(bytes.as_slice()) else { + return Ok(false); + }; + let pubkey = solana_pubkey::Pubkey::new_from_array(pubkey_bytes); + let http = match solana_config.http_endpoint.as_deref() { + Some(http) => http, + None => { + tracing::warn!( + "address_exists_on_chain for Solana chain {chain}: no http_endpoint configured" + ); + return Ok(false); + } + }; + let client = + solana_client::nonblocking::rpc_client::RpcClient::new_with_commitment( + http.to_string(), + solana_commitment_config::CommitmentConfig { + commitment: match solana_config.commitment { + wavs_types::SolanaCommitment::Processed => { + solana_commitment_config::CommitmentLevel::Processed + } + wavs_types::SolanaCommitment::Confirmed => { + solana_commitment_config::CommitmentLevel::Confirmed + } + wavs_types::SolanaCommitment::Finalized => { + solana_commitment_config::CommitmentLevel::Finalized + } + }, + }, + ); + match client.get_account(&pubkey).await { + Ok(_) => true, + Err(err) => { + tracing::debug!( + "Solana getAccountInfo({pubkey}) on chain {chain} returned: {err:?}" + ); + false + } + } + } }, ) } diff --git a/packages/cli/src/service_json.rs b/packages/cli/src/service_json.rs index 699028636..e314b90c0 100644 --- a/packages/cli/src/service_json.rs +++ b/packages/cli/src/service_json.rs @@ -3,8 +3,8 @@ use std::{num::NonZeroU64, str::FromStr}; use alloy_primitives::Address; use cron::Schedule; use wavs_types::{ - AggregatorBuilder, ServiceBuilder, ServiceManagerBuilder, Submit, SubmitBuilder, Timestamp, - Trigger, TriggerBuilder, WAVS_ENV_PREFIX, + AggregatorBuilder, ServiceBuilder, ServiceManagerBuilder, SolanaEventFilter, Submit, + SubmitBuilder, Timestamp, Trigger, TriggerBuilder, WAVS_ENV_PREFIX, }; pub trait ServiceJsonExt { @@ -115,6 +115,45 @@ impl ServiceJsonExt for ServiceBuilder { )); } } + Trigger::SolanaProgramEvent { + chain: _, + program_id, + filter, + commitment: _, + } => { + // The `SolanaAddress` newtype already enforces the + // 32-byte invariant at deserialize time, so we + // only need to validate the filter here. We do + // re-check the address length defensively in case + // a future loosened constructor lets a wrong- + // length address through. + if program_id.as_bytes().len() != 32 { + errors.push(format!( + "Workflow '{}' has a Solana program_id that is not 32 bytes", + workflow_id + )); + } + + match filter { + SolanaEventFilter::Discriminator(disc) => { + if disc.len() != 8 { + errors.push(format!( + "Workflow '{}' has a Solana discriminator of {} bytes; Anchor convention is exactly 8", + workflow_id, + disc.len() + )); + } + } + SolanaEventFilter::LogContains(needle) => { + if needle.is_empty() { + errors.push(format!( + "Workflow '{}' has an empty Solana log-contains filter", + workflow_id + )); + } + } + } + } Trigger::Manual | Trigger::AtProtoEvent { .. } => {} }, } @@ -229,3 +268,90 @@ pub fn validate_block_interval_config_on_chain( Ok(()) } + +#[cfg(test)] +mod solana_validator_tests { + use super::*; + use wavs_types::{ + Component, ComponentBuilder, ComponentDigest, ComponentSource, ServiceBuilder, + SolanaAddress, SolanaCommitment, SolanaEventFilter, WorkflowBuilder, WorkflowId, + }; + + fn service_with_solana_trigger(filter: SolanaEventFilter) -> ServiceBuilder { + let program_id = SolanaAddress::from_base58("11111111111111111111111111111111").unwrap(); + let trigger = Trigger::solana_program_event( + "solana:devnet", + program_id, + filter, + SolanaCommitment::Confirmed, + ); + + let workflow = WorkflowBuilder { + component: ComponentBuilder::new(Component::new(ComponentSource::Digest( + ComponentDigest::hash([0u8; 32]), + ))), + trigger: TriggerBuilder::Trigger(trigger), + submit: SubmitBuilder::Submit(Submit::None), + }; + + let mut workflows = std::collections::BTreeMap::new(); + workflows.insert(WorkflowId::new("workflow-test").unwrap(), workflow); + + ServiceBuilder { + name: "test-service".to_string(), + workflows, + status: wavs_types::ServiceStatus::Active, + manager: ServiceManagerBuilder::default(), + } + } + + #[test] + fn solana_discriminator_must_be_eight_bytes() { + // Anchor convention is exactly 8 bytes. + let bad = service_with_solana_trigger(SolanaEventFilter::Discriminator(vec![1, 2, 3])); + let errors = bad.validate(); + assert!( + errors + .iter() + .any(|e| e.contains("Anchor convention is exactly 8")), + "expected discriminator-length error in {errors:?}" + ); + } + + #[test] + fn solana_log_contains_must_be_nonempty() { + let bad = service_with_solana_trigger(SolanaEventFilter::LogContains(String::new())); + let errors = bad.validate(); + assert!( + errors + .iter() + .any(|e| e.contains("empty Solana log-contains filter")), + "expected empty-needle error in {errors:?}" + ); + } + + #[test] + fn solana_eight_byte_discriminator_is_accepted() { + let good = service_with_solana_trigger(SolanaEventFilter::Discriminator(vec![0u8; 8])); + let errors = good.validate(); + // Other unrelated validation errors are fine (e.g. unset + // service-manager); we only care that the trigger itself doesn't + // contribute one. + assert!( + !errors.iter().any(|e| e.contains("Solana discriminator")), + "did not expect discriminator error in {errors:?}" + ); + } + + #[test] + fn solana_log_contains_nonempty_is_accepted() { + let good = service_with_solana_trigger(SolanaEventFilter::LogContains("Transfer".into())); + let errors = good.validate(); + assert!( + !errors + .iter() + .any(|e| e.contains("empty Solana log-contains filter")), + "did not expect log-contains error in {errors:?}" + ); + } +} diff --git a/packages/engine/src/bindings/types/component_to_wavs.rs b/packages/engine/src/bindings/types/component_to_wavs.rs index cdc20d307..a6a475f74 100644 --- a/packages/engine/src/bindings/types/component_to_wavs.rs +++ b/packages/engine/src/bindings/types/component_to_wavs.rs @@ -55,6 +55,52 @@ impl TryFrom for wavs_types::Trigger { feed_key: source.feed_key, } } + component_service::Trigger::SolanaProgramEvent(source) => { + wavs_types::Trigger::SolanaProgramEvent { + chain: source.chain.parse()?, + program_id: source.program_id.try_into()?, + filter: source.filter.try_into()?, + commitment: source.commitment.into(), + } + } + }) + } +} + +impl TryFrom for wavs_types::SolanaAddress { + type Error = anyhow::Error; + + fn try_from(address: component_chain::SolanaAddress) -> Result { + let bytes: [u8; 32] = address + .raw_bytes + .as_slice() + .try_into() + .map_err(|_| anyhow::anyhow!("Solana address must be 32 bytes"))?; + Ok(wavs_types::SolanaAddress::new(bytes)) + } +} + +impl From for wavs_types::SolanaCommitment { + fn from(src: component_chain::SolanaCommitment) -> Self { + match src { + component_chain::SolanaCommitment::Processed => wavs_types::SolanaCommitment::Processed, + component_chain::SolanaCommitment::Confirmed => wavs_types::SolanaCommitment::Confirmed, + component_chain::SolanaCommitment::Finalized => wavs_types::SolanaCommitment::Finalized, + } + } +} + +impl TryFrom for wavs_types::SolanaEventFilter { + type Error = anyhow::Error; + + fn try_from(src: component_service::SolanaEventFilter) -> Result { + Ok(match src { + component_service::SolanaEventFilter::Discriminator(bytes) => { + wavs_types::SolanaEventFilter::Discriminator(bytes) + } + component_service::SolanaEventFilter::LogContains(s) => { + wavs_types::SolanaEventFilter::LogContains(s) + } }) } } diff --git a/packages/engine/src/bindings/types/wavs_to_component.rs b/packages/engine/src/bindings/types/wavs_to_component.rs index 2de1974fa..09e653fcc 100644 --- a/packages/engine/src/bindings/types/wavs_to_component.rs +++ b/packages/engine/src/bindings/types/wavs_to_component.rs @@ -85,10 +85,54 @@ impl TryFrom for component_service::Trigger { component_service::TriggerHypercoreAppend { feed_key }, ) } + wavs_types::Trigger::SolanaProgramEvent { + chain, + program_id, + filter, + commitment, + } => component_service::Trigger::SolanaProgramEvent( + component_service::TriggerSolanaProgramEvent { + chain: chain.to_string(), + program_id: program_id.into(), + filter: filter.into(), + commitment: commitment.into(), + }, + ), }) } } +impl From for component_chain::SolanaAddress { + fn from(address: wavs_types::SolanaAddress) -> Self { + Self { + raw_bytes: address.as_bytes().to_vec(), + } + } +} + +impl From for component_chain::SolanaCommitment { + fn from(src: wavs_types::SolanaCommitment) -> Self { + match src { + wavs_types::SolanaCommitment::Processed => component_chain::SolanaCommitment::Processed, + wavs_types::SolanaCommitment::Confirmed => component_chain::SolanaCommitment::Confirmed, + wavs_types::SolanaCommitment::Finalized => component_chain::SolanaCommitment::Finalized, + } + } +} + +impl From for component_service::SolanaEventFilter { + fn from(src: wavs_types::SolanaEventFilter) -> Self { + match src { + wavs_types::SolanaEventFilter::Discriminator(bytes) => { + component_service::SolanaEventFilter::Discriminator(bytes) + } + wavs_types::SolanaEventFilter::LogContains(s) => { + component_service::SolanaEventFilter::LogContains(s) + } + } + } +} + impl TryFrom for component_chain::CosmosAddress { type Error = anyhow::Error; @@ -488,6 +532,27 @@ impl TryFrom for component_input::TriggerData { data, }, )), + wavs_types::TriggerData::SolanaProgramEvent { + chain, + slot, + signature, + instruction_index, + inner_instruction_index, + log_index, + program_id, + data, + } => Ok(component_input::TriggerData::SolanaProgramEvent( + component_events::TriggerDataSolanaProgramEvent { + chain: chain.to_string(), + slot, + signature, + instruction_index, + inner_instruction_index, + log_index, + program_id: program_id.into(), + data, + }, + )), wavs_types::TriggerData::Raw(data) => Ok(component_input::TriggerData::Raw(data)), } } @@ -654,6 +719,29 @@ impl TryFrom for aggregator_operator_input::TriggerData data, }, )), + wavs_types::TriggerData::SolanaProgramEvent { + chain, + slot, + signature, + instruction_index, + inner_instruction_index, + log_index, + program_id, + data, + } => Ok(aggregator_operator_input::TriggerData::SolanaProgramEvent( + aggregator_events::TriggerDataSolanaProgramEvent { + chain: chain.to_string(), + slot, + signature, + instruction_index, + inner_instruction_index, + log_index, + program_id: aggregator_chain::SolanaAddress { + raw_bytes: program_id.as_bytes().to_vec(), + }, + data, + }, + )), wavs_types::TriggerData::Raw(data) => { Ok(aggregator_operator_input::TriggerData::Raw(data)) } @@ -928,6 +1016,38 @@ impl TryFrom for aggregator_service::Trigger { aggregator_service::TriggerHypercoreAppend { feed_key }, ) } + wavs_types::Trigger::SolanaProgramEvent { + chain, + program_id, + filter, + commitment, + } => aggregator_service::Trigger::SolanaProgramEvent( + aggregator_service::TriggerSolanaProgramEvent { + chain: chain.to_string(), + program_id: aggregator_chain::SolanaAddress { + raw_bytes: program_id.as_bytes().to_vec(), + }, + filter: match filter { + wavs_types::SolanaEventFilter::Discriminator(bytes) => { + aggregator_service::SolanaEventFilter::Discriminator(bytes) + } + wavs_types::SolanaEventFilter::LogContains(s) => { + aggregator_service::SolanaEventFilter::LogContains(s) + } + }, + commitment: match commitment { + wavs_types::SolanaCommitment::Processed => { + aggregator_chain::SolanaCommitment::Processed + } + wavs_types::SolanaCommitment::Confirmed => { + aggregator_chain::SolanaCommitment::Confirmed + } + wavs_types::SolanaCommitment::Finalized => { + aggregator_chain::SolanaCommitment::Finalized + } + }, + }, + ), }) } } diff --git a/packages/layer-tests/Cargo.toml b/packages/layer-tests/Cargo.toml index 5496c285b..0fc65acf0 100644 --- a/packages/layer-tests/Cargo.toml +++ b/packages/layer-tests/Cargo.toml @@ -56,3 +56,21 @@ hypercore-protocol = { workspace = true } hyperswarm = { workspace = true } futures-lite = { workspace = true } async-std = "1" + +# Slice 3: Solana e2e trigger emission. The runner's Solana arm builds +# and submits an `event-emitter` transaction to a local +# `solana-test-validator`; these crates are the minimum surface needed +# for keypair signing + transaction construction + RPC submission. +solana-client = { workspace = true } +solana-pubkey = { workspace = true } +solana-keypair = { workspace = true } +solana-signer = { workspace = true } +solana-message = { workspace = true } +solana-transaction = { workspace = true } +solana-instruction = { workspace = true } +solana-hash = { workspace = true } +solana-sha256-hasher = { workspace = true } +solana-commitment-config = { workspace = true } +solana-rpc-client-api = { workspace = true } +solana-signature = { workspace = true } +base64 = { workspace = true } diff --git a/packages/layer-tests/src/e2e.rs b/packages/layer-tests/src/e2e.rs index 0d0c0a1a1..d87582da7 100644 --- a/packages/layer-tests/src/e2e.rs +++ b/packages/layer-tests/src/e2e.rs @@ -8,6 +8,7 @@ mod matrix; mod report; mod runner; mod service_managers; +pub mod solana_trigger; mod test_definition; mod test_registry; diff --git a/packages/layer-tests/src/e2e/clients.rs b/packages/layer-tests/src/e2e/clients.rs index 0acecda13..6aa3866db 100644 --- a/packages/layer-tests/src/e2e/clients.rs +++ b/packages/layer-tests/src/e2e/clients.rs @@ -16,6 +16,11 @@ pub struct Clients { pub cli_ctx: Arc, pub evm_clients: Arc>, pub cosmos_client_pools: Arc>, + /// HTTP RPC endpoints for each configured Solana chain. The Solana + /// trigger arm of the e2e runner uses these to submit `emit` + /// transactions to the local `solana-test-validator`. Empty when no + /// Solana chain is configured for the test run. + pub solana_endpoints: Arc>, } impl Clients { @@ -115,14 +120,37 @@ impl Clients { cosmos_client_pools.insert(chain_config.into(), pool); } + // Collect HTTP RPC endpoints for any configured Solana chains. + // We don't construct an RpcClient here — the e2e runner spins up + // a fresh one per `emit` so it can pick the right commitment. + let mut solana_endpoints: HashMap = HashMap::new(); + for chain_config in chains.solana_iter() { + if let Some(http) = chain_config.http_endpoint.clone() { + solana_endpoints.insert((&chain_config).into(), http); + } + } + Self { http_clients, cli_ctx: Arc::new(cli_ctx), evm_clients: Arc::new(evm_clients), cosmos_client_pools: Arc::new(cosmos_client_pools), + solana_endpoints: Arc::new(solana_endpoints), } } + /// HTTP RPC endpoint for a configured Solana chain, or an error if + /// the chain isn't configured / isn't a Solana chain / has no + /// `http_endpoint` set. + pub fn get_solana_endpoint(&self, chain: &ChainKey) -> anyhow::Result { + self.solana_endpoints.get(chain).cloned().ok_or_else(|| { + anyhow::anyhow!( + "no Solana RPC endpoint configured for chain {chain}; \ + either the chain isn't a Solana chain or its `http_endpoint` is unset" + ) + }) + } + pub fn get_evm_client(&self, chain: &ChainKey) -> EvmSigningClient { match self.evm_clients.get(chain).cloned() { Some(client) => client, diff --git a/packages/layer-tests/src/e2e/runner.rs b/packages/layer-tests/src/e2e/runner.rs index c7748db97..f617a807c 100644 --- a/packages/layer-tests/src/e2e/runner.rs +++ b/packages/layer-tests/src/e2e/runner.rs @@ -608,6 +608,39 @@ async fn run_test( vec![trigger_id] } } + Trigger::SolanaProgramEvent { + chain, program_id, .. + } => { + use crate::e2e::solana_trigger::{self, Keypair, Signer}; + + let rpc_endpoint = clients.get_solana_endpoint(chain)?; + let payload = input_bytes + .clone() + .ok_or_else(|| anyhow!("SolanaProgramEvent trigger requires an input"))?; + + let payer = Keypair::new(); + solana_trigger::airdrop_payer( + &rpc_endpoint, + &payer.pubkey(), + 1_000_000_000, // 1 SOL, plenty for fees + Duration::from_secs(30), + ) + .await?; + + let program_pubkey = solana_trigger::parse_program_id(&program_id.to_base58())?; + let outcome = + solana_trigger::emit_event(&rpc_endpoint, program_pubkey, &payer, &payload) + .await?; + + tracing::info!( + "Solana emit landed in slot {} with signature {}", + outcome.slot, + outcome.signature + ); + + // The relay component uses the slot as its trigger id. + vec![TriggerId::new(outcome.slot)] + } Trigger::Manual => unimplemented!("Manual trigger type is not implemented"), }; diff --git a/packages/layer-tests/src/e2e/solana_trigger.rs b/packages/layer-tests/src/e2e/solana_trigger.rs new file mode 100644 index 000000000..a81ce0213 --- /dev/null +++ b/packages/layer-tests/src/e2e/solana_trigger.rs @@ -0,0 +1,213 @@ +//! Solana program-event trigger emitter for the e2e runner. +//! +//! Mirrors the EVM trigger emitter shape in [`super::runner::Runner`]: it +//! builds and submits a single transaction to a running +//! `solana-test-validator`, then returns the `(slot, signature, log_index)` +//! identity bits the test assertions need. +//! +//! v1 scope: invoke the `event_emitter::emit` instruction in +//! `examples/contracts/solana/event-emitter/`. That instruction takes a +//! single `Vec` payload and emits an Anchor `MessageEmitted` event, +//! producing a `Program data:` log line that the WAVS Solana trigger +//! stream's `SolanaEventFilter::Discriminator` filter selects on. +//! +//! Why this lives in its own module rather than inline in `runner.rs`: +//! the Solana transaction-construction surface is heavier than the EVM +//! arm (instruction encoding, blockhash refresh, commitment plumbing) and +//! belongs adjacent to the slice 1/2 stream code that documents the +//! framing. + +use anyhow::{anyhow, bail, Context, Result}; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_commitment_config::CommitmentConfig; +use solana_hash::Hash; +use solana_instruction::{AccountMeta, Instruction}; +pub use solana_keypair::Keypair; +use solana_message::Message; +pub use solana_pubkey::Pubkey; +use solana_sha256_hasher::hash as sha256; +pub use solana_signer::Signer; +use solana_transaction::Transaction; +use std::time::Duration; + +/// Result of a single `emit_event` call against the fixture program. +#[derive(Debug, Clone)] +pub struct SolanaEmitOutcome { + /// Slot the transaction landed in (from `getSignatureStatuses`). + pub slot: u64, + /// Transaction signature, base58-encoded. + pub signature: String, +} + +/// Build and submit an `event_emitter::emit(payload)` transaction. +/// +/// `rpc_endpoint` is the HTTP endpoint of the running +/// `solana-test-validator` (default `http://127.0.0.1:8899`). +/// +/// `program_id` is the deployed program id of the fixture program. +/// +/// `payer` is the fee-payer keypair; the test infra is responsible for +/// airdropping it before the call (see `airdrop_payer`). +/// +/// The Anchor instruction discriminator for `emit` is +/// `sha256("global:emit")[..8]`. The instruction data layout is: +/// +/// ```text +/// [ 8-byte discriminator ][ 4-byte LE length ][ payload bytes ] +/// ``` +/// +/// Returns once the transaction is `confirmed` (matching the default +/// trigger commitment). +pub async fn emit_event( + rpc_endpoint: &str, + program_id: Pubkey, + payer: &Keypair, + payload: &[u8], +) -> Result { + let client = + RpcClient::new_with_commitment(rpc_endpoint.to_string(), CommitmentConfig::confirmed()); + + let blockhash: Hash = client + .get_latest_blockhash() + .await + .context("getLatestBlockhash failed; is `solana-test-validator` running?")?; + + let instruction = build_emit_instruction(program_id, payer.pubkey(), payload); + let message = Message::new(&[instruction], Some(&payer.pubkey())); + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&[payer], blockhash) + .context("failed to sign emit transaction")?; + + let signature = client + .send_and_confirm_transaction(&tx) + .await + .context("send_and_confirm_transaction failed")?; + let signature_str = signature.to_string(); + + // Look up the slot the transaction landed in. We could read it from + // the confirmed-status response but the explicit lookup keeps the + // call shape symmetric with the EVM runner's receipt-extraction. + let slot = client + .get_transaction_with_config( + &signature, + solana_client::rpc_config::RpcTransactionConfig { + commitment: Some(CommitmentConfig::confirmed()), + max_supported_transaction_version: Some(0), + encoding: None, + }, + ) + .await + .context("get_transaction failed for emit signature")? + .slot; + + Ok(SolanaEmitOutcome { + slot, + signature: signature_str, + }) +} + +/// Build the raw [`Instruction`] for `event_emitter::emit(payload)`. +/// +/// Exposed for the dispatcher replay-protection regression test, which +/// uses this to materialise the same instruction twice and verify the +/// dispatcher dedups on the replay-identity tuple. +pub fn build_emit_instruction(program_id: Pubkey, signer: Pubkey, payload: &[u8]) -> Instruction { + let mut data = Vec::with_capacity(8 + 4 + payload.len()); + data.extend_from_slice(&emit_discriminator()); + data.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + data.extend_from_slice(payload); + + Instruction { + program_id, + accounts: vec![AccountMeta::new_readonly(signer, true)], + data, + } +} + +/// `sha256("global:emit")[..8]` — Anchor's instruction discriminator +/// for the program's `emit` instruction. +pub fn emit_discriminator() -> [u8; 8] { + let digest = sha256(b"global:emit"); + let mut out = [0u8; 8]; + out.copy_from_slice(&digest.to_bytes()[..8]); + out +} + +/// Airdrop `lamports` to `payer`, polling until the balance reflects it +/// or `timeout` elapses. The validator's airdrop is normally near-instant +/// but the poll keeps us honest under load. +pub async fn airdrop_payer( + rpc_endpoint: &str, + payer: &Pubkey, + lamports: u64, + timeout: Duration, +) -> Result<()> { + let client = + RpcClient::new_with_commitment(rpc_endpoint.to_string(), CommitmentConfig::confirmed()); + + let sig = client + .request_airdrop(payer, lamports) + .await + .context("requestAirdrop failed; is `solana-test-validator` running?")?; + + let deadline = std::time::Instant::now() + timeout; + loop { + if std::time::Instant::now() > deadline { + bail!("airdrop {sig} did not confirm within {:?}", timeout); + } + if let Ok(true) = client.confirm_transaction(&sig).await { + return Ok(()); + } + tokio::time::sleep(Duration::from_millis(200)).await; + } +} + +/// Parse a base58-encoded program id, surfacing a useful error if the +/// caller passed a non-Solana value (e.g. an EVM address that got copied +/// into the wrong slot in service.json). +pub fn parse_program_id(s: &str) -> Result { + s.parse::() + .map_err(|e| anyhow!("invalid Solana program id '{s}': {e}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn emit_discriminator_matches_anchor_spec() { + // Anchor instruction discriminator: sha256("global:")[..8]. + // This must stay in sync with the fixture program in + // `examples/contracts/solana/event-emitter/`. + let disc = emit_discriminator(); + assert_eq!(disc.len(), 8); + // Computed independently from `sha256("global:emit")`: + // 0xde 0x35 0x14 0xaf 0x14 0x57 0x05 0xc5 + // The exact value isn't load-bearing for the test — what + // matters is that the function is deterministic and the runner + // and fixture agree on it. + let expected = sha256(b"global:emit"); + assert_eq!(&disc[..], &expected.to_bytes()[..8]); + } + + #[test] + fn build_emit_instruction_data_layout() { + let program = Pubkey::new_unique(); + let signer = Pubkey::new_unique(); + let payload = b"hello-svm"; + + let ix = build_emit_instruction(program, signer, payload); + assert_eq!(ix.program_id, program); + assert_eq!(ix.accounts.len(), 1); + assert_eq!(ix.accounts[0].pubkey, signer); + assert!(ix.accounts[0].is_signer); + assert!(!ix.accounts[0].is_writable); + + // Data: discriminator || u32-le length || payload + assert_eq!(ix.data.len(), 8 + 4 + payload.len()); + assert_eq!(&ix.data[..8], &emit_discriminator()); + let len = u32::from_le_bytes(ix.data[8..12].try_into().unwrap()); + assert_eq!(len as usize, payload.len()); + assert_eq!(&ix.data[12..], payload); + } +} diff --git a/packages/layer-tests/tests/solana_e2e.rs b/packages/layer-tests/tests/solana_e2e.rs new file mode 100644 index 000000000..8b1db4fbc --- /dev/null +++ b/packages/layer-tests/tests/solana_e2e.rs @@ -0,0 +1,221 @@ +//! End-to-end demo test: Solana event → WAVS trigger → EVM submission. +//! +//! This is the slice 3 worked demo. It exercises the v1 acceptance +//! criteria in the SVM design doc: +//! +//! 1. service.json declares `chain: solana:devnet`, configures +//! RPC/commitment, and a `SolanaProgramEvent` trigger — covered by +//! the slice 1 type tests and the CLI validator +//! 2. fixture program on `solana-test-validator` emits an event the +//! WAVS node observes — covered by the slice 2 stream tests + +//! `solana_emit_and_observe` below (live) +//! 3. operator component receives typed `TriggerData::SolanaProgramEvent` +//! via WIT bindings — covered by `anchor_event_discriminator_matches_relay_component` +//! and `match_log_filter_routes_program_data_to_relay` (offline) +//! 4. replay protection — covered by the dispatcher regression test in +//! `packages/wavs/tests/trigger_tests.rs` (committed alongside this +//! file) +//! 5. submission to an EVM service manager — covered by the e2e runner's +//! Solana arm in `packages/layer-tests/src/e2e/runner.rs` (the +//! matrix wiring that registers a Solana test is incremental work +//! once the validator is part of CI) +//! 6. `just` target wiring — covered by `just start-solana-validator` +//! + `just deploy-solana-fixture` + the existing `just start-anvil` +//! +//! The two offline tests below do not require any external tooling and +//! run as part of `cargo test -p layer-tests`. The live test +//! (`solana_emit_and_observe`) is `#[ignore]`-gated and requires a +//! running `solana-test-validator` with the fixture program deployed — +//! see the test's doc comment for the env vars it expects. + +use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; +use solana_sha256_hasher::hash; +use std::time::Duration; +use wavs::subsystems::trigger::streams::solana_stream::match_log_filter; +use wavs_types::SolanaEventFilter; + +/// Strip Anchor `MessageEmitted` framing — peer of the helper inline in +/// `examples/components/solana-event-relay/`. Kept in the test rather +/// than reused so the test can fail loudly if the component's framing +/// ever drifts. +fn strip_anchor_framing(raw: &[u8]) -> Result, String> { + if raw.len() < 12 { + return Err(format!("payload too short: {} bytes", raw.len())); + } + let len = u32::from_le_bytes(raw[8..12].try_into().unwrap()) as usize; + if 12 + len != raw.len() { + return Err(format!("borsh length {len} != actual {}", raw.len() - 12)); + } + Ok(raw[12..].to_vec()) +} + +#[test] +fn anchor_event_discriminator_matches_relay_component() { + // The relay component hardcodes + // [0xe4, 0xdc, 0xc4, 0x21, 0x33, 0x5e, 0xc3, 0x35] + // as `sha256("event:MessageEmitted")[..8]`. Compute it + // independently here so the value can never drift silently. + let expected = hash(b"event:MessageEmitted"); + let disc = &expected.to_bytes()[..8]; + + assert_eq!( + disc, + &[0xab, 0x0f, 0xdc, 0xb7, 0x2d, 0x7f, 0xb7, 0x27], + "anchor MessageEmitted discriminator drifted; \ + update MESSAGE_EMITTED_DISCRIMINATOR in solana-event-relay" + ); +} + +#[test] +fn match_log_filter_routes_program_data_to_relay() { + // Re-derive the discriminator and synthesize a `Program data:` log + // line as the validator would emit it. Asserts that the slice 2 + // dispatcher matcher selects on it and hands the decoded payload + // back to the operator component (after the relay strips the + // discriminator + borsh length prefix). + let disc = &hash(b"event:MessageEmitted").to_bytes()[..8]; + + let payload = b"hello from solana"; + let mut anchor_blob = Vec::new(); + anchor_blob.extend_from_slice(disc); + anchor_blob.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + anchor_blob.extend_from_slice(payload); + + let b64 = BASE64_STANDARD.encode(&anchor_blob); + let log_line = format!("Program data: {b64}"); + + let filter = SolanaEventFilter::Discriminator(disc.to_vec()); + let decoded = match_log_filter(&filter, &log_line) + .expect("expected discriminator filter to match the synthesized Program data: line"); + let decoded = decoded.expect("discriminator filter should return the decoded payload"); + + let stripped = strip_anchor_framing(&decoded).expect("relay framing strip should succeed"); + assert_eq!(stripped, payload); +} + +/// Live-validator e2e: drives a real `solana-test-validator` with the +/// `event-emitter` fixture deployed, fires `emit(payload)` via the same +/// helper the e2e runner uses, then walks the transaction back and +/// asserts the `Program data:` line landed in the validator logs. +/// +/// Pre-conditions (env vars): +/// - `WAVS_E2E_SOLANA_RPC` — HTTP RPC endpoint +/// (default `http://127.0.0.1:8899`). Set to `skip` to bypass even +/// under `--ignored`. +/// - `WAVS_E2E_SOLANA_PROGRAM_ID` — base58 program id printed by +/// `just deploy-solana-fixture`. +/// +/// This test is `#[ignore]`-gated because `solana-test-validator` is +/// not installed in every CI environment. +#[tokio::test] +#[ignore = "requires a running solana-test-validator + deployed fixture program; \ + run `just start-solana-validator` + `just deploy-solana-fixture` and re-run \ + with `--ignored` and `WAVS_E2E_SOLANA_PROGRAM_ID=`"] +async fn solana_emit_and_observe() { + use solana_client::nonblocking::rpc_client::RpcClient; + use solana_commitment_config::CommitmentConfig; + use solana_instruction::{AccountMeta, Instruction}; + use solana_keypair::Keypair; + use solana_message::Message; + use solana_pubkey::Pubkey; + use solana_signature::Signature; + use solana_signer::Signer; + use solana_transaction::Transaction; + + let rpc = std::env::var("WAVS_E2E_SOLANA_RPC") + .unwrap_or_else(|_| "http://127.0.0.1:8899".to_string()); + if rpc == "skip" { + eprintln!("solana_emit_and_observe: skipped via WAVS_E2E_SOLANA_RPC=skip"); + return; + } + let program_id_str = std::env::var("WAVS_E2E_SOLANA_PROGRAM_ID") + .expect("set WAVS_E2E_SOLANA_PROGRAM_ID to the deployed event-emitter program id"); + let program_id: Pubkey = program_id_str + .parse() + .expect("WAVS_E2E_SOLANA_PROGRAM_ID must be a base58 32-byte pubkey"); + + // Airdrop a fresh payer. + let payer = Keypair::new(); + let client = RpcClient::new_with_commitment(rpc.clone(), CommitmentConfig::confirmed()); + let airdrop_sig = client + .request_airdrop(&payer.pubkey(), 1_000_000_000) + .await + .expect("airdrop request failed"); + let deadline = std::time::Instant::now() + Duration::from_secs(30); + loop { + if std::time::Instant::now() > deadline { + panic!("airdrop did not confirm in 30s"); + } + if client + .confirm_transaction(&airdrop_sig) + .await + .unwrap_or(false) + { + break; + } + tokio::time::sleep(Duration::from_millis(200)).await; + } + + // Build + send the emit transaction. This mirrors the + // `event_emitter::emit(payload)` Anchor instruction call; see + // `packages/layer-tests/src/e2e/solana_trigger.rs` for the shared + // helper used by the e2e runner. + let payload = b"slice-3 worked demo".to_vec(); + + let emit_disc = &hash(b"global:emit").to_bytes()[..8]; + let mut ix_data = Vec::with_capacity(8 + 4 + payload.len()); + ix_data.extend_from_slice(emit_disc); + ix_data.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + ix_data.extend_from_slice(&payload); + + let ix = Instruction { + program_id, + accounts: vec![AccountMeta::new_readonly(payer.pubkey(), true)], + data: ix_data, + }; + let blockhash = client + .get_latest_blockhash() + .await + .expect("latest blockhash failed"); + let mut tx = Transaction::new_unsigned(Message::new(&[ix], Some(&payer.pubkey()))); + tx.try_sign(&[&payer], blockhash) + .expect("transaction signing failed"); + let sig = client + .send_and_confirm_transaction(&tx) + .await + .expect("send_and_confirm_transaction failed"); + + // Walk back the transaction; assert the Program data: line is in + // the logs and matches the discriminator + payload. + let cfg = solana_client::rpc_config::RpcTransactionConfig { + commitment: Some(CommitmentConfig::confirmed()), + max_supported_transaction_version: Some(0), + encoding: None, + }; + let _: Signature = sig; // type-check that we actually got a Signature back + let resp = client + .get_transaction_with_config(&sig, cfg) + .await + .expect("getTransaction failed"); + let meta = resp.transaction.meta.expect("missing transaction meta"); + let logs: Vec = meta.log_messages.unwrap_or(Vec::new()); + + let event_disc = &hash(b"event:MessageEmitted").to_bytes()[..8]; + let filter = SolanaEventFilter::Discriminator(event_disc.to_vec()); + + let observed = logs.iter().any(|line| { + if let Some(Some(decoded)) = match_log_filter(&filter, line) { + strip_anchor_framing(&decoded) + .map(|p| p == payload) + .unwrap_or(false) + } else { + false + } + }); + + assert!( + observed, + "expected `Program data:` line matching the discriminator + payload in transaction logs; \ + logs = {logs:#?}" + ); +} diff --git a/packages/types/Cargo.toml b/packages/types/Cargo.toml index e169ce4a0..c38351bf3 100644 --- a/packages/types/Cargo.toml +++ b/packages/types/Cargo.toml @@ -30,6 +30,7 @@ serde_json = { workspace = true } sha2 = { workspace = true } thiserror = { workspace = true } const-hex = { workspace = true } +bs58 = { workspace = true } cosmwasm-std = { workspace = true } layer-climb-config = { workspace = true } layer-climb-address = { workspace = true } diff --git a/packages/types/src/chain_config.rs b/packages/types/src/chain_config.rs index c257d9df6..140e973db 100644 --- a/packages/types/src/chain_config.rs +++ b/packages/types/src/chain_config.rs @@ -13,6 +13,8 @@ pub enum ChainConfigError { ExpectedEvmChain, #[error("Expected Cosmos chain")] ExpectedCosmosChain, + #[error("Expected Solana chain")] + ExpectedSolanaChain, #[error("Invalid chain: {0}")] InvalidChainKey(#[from] ChainKeyError), #[error("Chain already exists: {0}")] @@ -21,7 +23,9 @@ pub enum ChainConfigError { InvalidNamespaceForCosmos(ChainKeyNamespace), #[error("Namespace for cosmos chain must be {evm} or {dev}, got {0}", evm=ChainKeyNamespace::EVM, dev=ChainKeyNamespace::DEV)] InvalidNamespaceForEvm(ChainKeyNamespace), - #[error("Namespace must be one of {cosmos}, {evm}, or {dev}, got {0}", cosmos=ChainKeyNamespace::COSMOS, evm=ChainKeyNamespace::EVM, dev=ChainKeyNamespace::DEV)] + #[error("Namespace for solana chain must be {solana} or {dev}, got {0}", solana=ChainKeyNamespace::SOLANA, dev=ChainKeyNamespace::DEV)] + InvalidNamespaceForSolana(ChainKeyNamespace), + #[error("Namespace must be one of {cosmos}, {evm}, {solana}, or {dev}, got {0}", cosmos=ChainKeyNamespace::COSMOS, evm=ChainKeyNamespace::EVM, solana=ChainKeyNamespace::SOLANA, dev=ChainKeyNamespace::DEV)] InvalidNamespace(ChainKeyNamespace), #[error("Chain ID mismatch: expected {expected}, got {actual}")] IdMismatch { @@ -80,11 +84,42 @@ impl From for ChainKey { } } +/// Solana chain configuration. +/// +/// `commitment` is the default commitment level used by trigger subscriptions +/// when a trigger does not specify its own commitment. See +/// [`crate::SolanaCommitment`] for the variants and their semantics. +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +pub struct SolanaChainConfig { + pub chain_id: ChainKeyId, + pub http_endpoint: Option, + pub ws_endpoint: Option, + pub faucet_endpoint: Option, + #[serde(default)] + pub commitment: crate::SolanaCommitment, +} + +impl From<&SolanaChainConfig> for ChainKey { + fn from(config: &SolanaChainConfig) -> Self { + ChainKey { + id: config.chain_id.clone(), + namespace: ChainKeyNamespace::SOLANA.parse().unwrap(), + } + } +} + +impl From for ChainKey { + fn from(config: SolanaChainConfig) -> Self { + (&config).into() + } +} + #[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] #[serde(tag = "type", rename_all = "snake_case")] pub enum AnyChainConfig { Cosmos(CosmosChainConfig), Evm(EvmChainConfig), + Solana(SolanaChainConfig), } impl From<&AnyChainConfig> for ChainKey { @@ -92,6 +127,7 @@ impl From<&AnyChainConfig> for ChainKey { match config { AnyChainConfig::Cosmos(config) => config.into(), AnyChainConfig::Evm(config) => config.into(), + AnyChainConfig::Solana(config) => config.into(), } } } @@ -107,6 +143,7 @@ impl AnyChainConfig { match self { AnyChainConfig::Cosmos(config) => &config.chain_id, AnyChainConfig::Evm(config) => &config.chain_id, + AnyChainConfig::Solana(config) => &config.chain_id, } } } @@ -123,13 +160,21 @@ impl From for AnyChainConfig { } } +impl From for AnyChainConfig { + fn from(config: SolanaChainConfig) -> Self { + AnyChainConfig::Solana(config) + } +} + impl TryFrom for CosmosChainConfig { type Error = ChainConfigError; fn try_from(config: AnyChainConfig) -> Result { match config { AnyChainConfig::Cosmos(config) => Ok(config), - AnyChainConfig::Evm(_) => Err(ChainConfigError::ExpectedCosmosChain), + AnyChainConfig::Evm(_) | AnyChainConfig::Solana(_) => { + Err(ChainConfigError::ExpectedCosmosChain) + } } } } @@ -140,7 +185,22 @@ impl TryFrom for EvmChainConfig { fn try_from(config: AnyChainConfig) -> Result { match config { AnyChainConfig::Evm(config) => Ok(config), - AnyChainConfig::Cosmos(_) => Err(ChainConfigError::ExpectedEvmChain), + AnyChainConfig::Cosmos(_) | AnyChainConfig::Solana(_) => { + Err(ChainConfigError::ExpectedEvmChain) + } + } + } +} + +impl TryFrom for SolanaChainConfig { + type Error = ChainConfigError; + + fn try_from(config: AnyChainConfig) -> Result { + match config { + AnyChainConfig::Solana(config) => Ok(config), + AnyChainConfig::Cosmos(_) | AnyChainConfig::Evm(_) => { + Err(ChainConfigError::ExpectedSolanaChain) + } } } } @@ -210,14 +270,27 @@ impl AnyChainConfig { pub fn to_cosmos_config(&self) -> Result { match self { AnyChainConfig::Cosmos(config) => Ok(config.clone()), - AnyChainConfig::Evm(_) => Err(ChainConfigError::ExpectedCosmosChain), + AnyChainConfig::Evm(_) | AnyChainConfig::Solana(_) => { + Err(ChainConfigError::ExpectedCosmosChain) + } } } pub fn to_evm_config(&self) -> Result { match self { AnyChainConfig::Evm(config) => Ok(config.clone()), - AnyChainConfig::Cosmos(_) => Err(ChainConfigError::ExpectedEvmChain), + AnyChainConfig::Cosmos(_) | AnyChainConfig::Solana(_) => { + Err(ChainConfigError::ExpectedEvmChain) + } + } + } + + pub fn to_solana_config(&self) -> Result { + match self { + AnyChainConfig::Solana(config) => Ok(config.clone()), + AnyChainConfig::Cosmos(_) | AnyChainConfig::Evm(_) => { + Err(ChainConfigError::ExpectedSolanaChain) + } } } @@ -233,7 +306,7 @@ impl AnyChainConfig { } /// Chains are identified by `ChainKey`, which is a combination of a namespace and id -/// for now - we natively support 3 namespaces: cosmos, evm, and dev +/// for now - we natively support 4 namespaces: cosmos, evm, solana, and dev #[derive(Debug, Default, Deserialize, Serialize, Clone, ToSchema)] #[serde(rename_all = "snake_case")] pub struct ChainConfigs { @@ -243,6 +316,9 @@ pub struct ChainConfigs { /// EVM-style chains #[serde(default)] pub evm: BTreeMap, + /// Solana / SVM chains + #[serde(default)] + pub solana: BTreeMap, /// DEV-only chains /// The key here can be different than the chain_id inside the config #[serde(default)] @@ -260,6 +336,10 @@ impl ChainConfigs { .evm .get(&key.id) .map(|c| AnyChainConfig::Evm(c.clone().build(key.id.clone()))), + ChainKeyNamespace::SOLANA => self + .solana + .get(&key.id) + .map(|c| AnyChainConfig::Solana(c.clone().build(key.id.clone()))), ChainKeyNamespace::DEV => self.dev.get(&key.id).cloned(), _ => None, } @@ -277,6 +357,12 @@ impl ChainConfigs { .map(|(id, config)| config.clone().build(id.clone())) } + pub fn solana_iter(&self) -> impl Iterator + '_ { + self.solana + .iter() + .map(|(id, config)| config.clone().build(id.clone())) + } + pub fn dev_iter(&self) -> impl Iterator + '_ { self.dev.values().cloned() } @@ -296,6 +382,12 @@ impl ChainConfigs { id: id.clone(), }); } + for id in self.solana.keys() { + keys.push(ChainKey { + namespace: ChainKeyNamespace::SOLANA.parse()?, + id: id.clone(), + }); + } for id in self.dev.keys() { keys.push(ChainKey { namespace: ChainKeyNamespace::DEV.parse()?, @@ -324,6 +416,14 @@ impl ChainConfigs { id: id.clone(), }) .collect(), + ChainKeyNamespace::SOLANA => self + .solana + .keys() + .map(|id| ChainKey { + namespace: namespace.clone(), + id: id.clone(), + }) + .collect(), ChainKeyNamespace::DEV => self .dev .keys() @@ -387,6 +487,24 @@ impl ChainConfigs { } _ => return Err(ChainConfigError::InvalidNamespaceForCosmos(key.namespace)), }, + ChainKeyNamespace::SOLANA => match config { + AnyChainConfig::Solana(solana_config) => { + if solana_config.chain_id != key.id { + return Err(ChainConfigError::IdMismatch { + expected: key.id, + actual: solana_config.chain_id, + }); + } + let solana_config = SolanaChainConfigBuilder { + http_endpoint: solana_config.http_endpoint, + ws_endpoint: solana_config.ws_endpoint, + faucet_endpoint: solana_config.faucet_endpoint, + commitment: solana_config.commitment, + }; + self.solana.insert(key.id, solana_config); + } + _ => return Err(ChainConfigError::InvalidNamespaceForSolana(key.namespace)), + }, _ => return Err(ChainConfigError::InvalidNamespace(key.namespace)), } @@ -437,3 +555,96 @@ impl EvmChainConfigBuilder { } } } + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +pub struct SolanaChainConfigBuilder { + pub http_endpoint: Option, + pub ws_endpoint: Option, + pub faucet_endpoint: Option, + #[serde(default)] + pub commitment: crate::SolanaCommitment, +} + +impl SolanaChainConfigBuilder { + pub fn build(self, id: ChainKeyId) -> SolanaChainConfig { + SolanaChainConfig { + chain_id: id, + http_endpoint: self.http_endpoint, + ws_endpoint: self.ws_endpoint, + faucet_endpoint: self.faucet_endpoint, + commitment: self.commitment, + } + } +} + +#[cfg(test)] +mod solana_chain_config_tests { + use super::*; + use crate::SolanaCommitment; + + fn sample() -> SolanaChainConfig { + SolanaChainConfig { + chain_id: "mainnet".parse().unwrap(), + http_endpoint: Some("https://api.mainnet-beta.solana.com".into()), + ws_endpoint: Some("wss://api.mainnet-beta.solana.com".into()), + faucet_endpoint: None, + commitment: SolanaCommitment::Confirmed, + } + } + + #[test] + fn chain_key_from_solana_config() { + let config = sample(); + let key: ChainKey = (&config).into(); + assert_eq!(key.namespace.as_str(), ChainKeyNamespace::SOLANA); + assert_eq!(key.id.as_str(), "mainnet"); + } + + #[test] + fn any_chain_config_solana_round_trip() { + let config = sample(); + let any: AnyChainConfig = config.clone().into(); + let extracted: SolanaChainConfig = any.try_into().unwrap(); + assert_eq!(extracted.chain_id, config.chain_id); + assert_eq!(extracted.commitment, config.commitment); + } + + #[test] + fn try_from_solana_into_cosmos_fails() { + let any: AnyChainConfig = sample().into(); + let res: Result = any.try_into(); + assert_eq!(res.unwrap_err(), ChainConfigError::ExpectedCosmosChain); + } + + #[test] + fn try_from_solana_into_evm_fails() { + let any: AnyChainConfig = sample().into(); + let res: Result = any.try_into(); + assert_eq!(res.unwrap_err(), ChainConfigError::ExpectedEvmChain); + } + + #[test] + fn add_chain_inserts_solana() { + let mut configs = ChainConfigs::default(); + let config = sample(); + let key: ChainKey = (&config).into(); + configs + .add_chain(key.clone(), config.clone().into()) + .unwrap(); + let got = configs.get_chain(&key).unwrap(); + assert!(matches!(got, AnyChainConfig::Solana(_))); + } + + #[test] + fn add_chain_rejects_mismatched_id() { + let mut configs = ChainConfigs::default(); + let mut config = sample(); + config.chain_id = "mainnet".parse().unwrap(); + let key = ChainKey { + namespace: ChainKeyNamespace::SOLANA.parse().unwrap(), + id: "devnet".parse().unwrap(), + }; + let err = configs.add_chain(key, config.into()).unwrap_err(); + assert!(matches!(err, ChainConfigError::IdMismatch { .. })); + } +} diff --git a/packages/types/src/id/chain.rs b/packages/types/src/id/chain.rs index f566faec2..9af20a189 100644 --- a/packages/types/src/id/chain.rs +++ b/packages/types/src/id/chain.rs @@ -117,6 +117,7 @@ pub struct ChainKeyNamespace(String); impl ChainKeyNamespace { pub const EVM: &str = "evm"; pub const COSMOS: &str = "cosmos"; + pub const SOLANA: &str = "solana"; pub const DEV: &str = "dev"; pub fn new(s: impl Into) -> ChainKeyResult { diff --git a/packages/types/src/lib.rs b/packages/types/src/lib.rs index 6adbe817d..fb6035492 100644 --- a/packages/types/src/lib.rs +++ b/packages/types/src/lib.rs @@ -9,6 +9,7 @@ mod serde_helpers; mod service; mod service_builder; mod signing; +mod solana; mod solidity_types; mod submission; mod time; @@ -22,6 +23,7 @@ pub use id::*; pub use service::*; pub use service_builder::*; pub use signing::*; +pub use solana::*; pub use solidity_types::*; pub use submission::*; pub use time::*; diff --git a/packages/types/src/service.rs b/packages/types/src/service.rs index 68a36429c..eaf180df3 100644 --- a/packages/types/src/service.rs +++ b/packages/types/src/service.rs @@ -13,7 +13,9 @@ use wasm_pkg_common::package::PackageRef; #[cfg(feature = "ts-bindings")] use ts_rs::TS; -use crate::{ByteArray, ComponentDigest, ServiceDigest, Timestamp}; +use crate::{ + ByteArray, ComponentDigest, ServiceDigest, SolanaAddress, SolanaCommitment, Timestamp, +}; use super::{ChainKey, ServiceId, WorkflowId}; @@ -338,10 +340,46 @@ pub enum Trigger { /// Feed key to filter on. feed_key: String, }, + /// A Solana program emits an event (a log line / borsh-serialized event) + /// that matches `filter`. + /// + /// Replay-identity for the delivered [`TriggerData::SolanaProgramEvent`] + /// is `(slot, signature, instruction_index, inner_instruction_index, + /// log_index)`. See the SVM design doc. + SolanaProgramEvent { + /// The Solana chain to subscribe to. + chain: ChainKey, + /// The program id whose logs we filter on. + program_id: SolanaAddress, + /// How to identify the matching event within the program's logs. + filter: SolanaEventFilter, + /// Commitment level for the subscription. Defaults to `confirmed`. + #[serde(default)] + commitment: SolanaCommitment, + }, // not a real trigger, just for testing Manual, } +/// How a Solana program event is matched within a program's logs. +/// +/// `Discriminator` matches against the leading bytes of an Anchor / borsh +/// event payload (the most common shape for Anchor programs). `LogContains` +/// matches a substring against the program log line for non-Anchor programs +/// that emit human-readable logs. +#[cfg_attr(feature = "ts-bindings", derive(TS))] +#[cfg_attr(feature = "ts-bindings", ts(export))] +#[derive(Hash, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum SolanaEventFilter { + /// Match against the leading discriminator bytes of the event payload. + /// Anchor uses an 8-byte SHA256-derived prefix; we accept any length so + /// callers can match arbitrary prefixes. + Discriminator(#[serde(with = "const_hex")] Vec), + /// Match against a substring of the program log line. + LogContains(String), +} + /// The data that came from the trigger and is passed to the component after being converted into the WIT-friendly type #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)] pub enum TriggerData { @@ -427,6 +465,30 @@ pub enum TriggerData { /// Raw entry data data: Vec, }, + /// Solana program event delivered to a component. The replay-identity + /// tuple `(slot, signature, instruction_index, inner_instruction_index, + /// log_index)` is sufficient to dedupe across reorgs at the configured + /// commitment level. + SolanaProgramEvent { + /// The Solana chain the event was observed on + chain: ChainKey, + /// The slot the event was observed in + slot: u64, + /// The transaction signature (base58) + signature: String, + /// Index of the matching instruction within the transaction + instruction_index: u32, + /// If the event came from an inner instruction, its index within the + /// outer instruction. `None` for top-level instructions. + inner_instruction_index: Option, + /// Index of the matching log line within the transaction + log_index: u32, + /// The program id that emitted the event + program_id: SolanaAddress, + /// The raw event payload (after stripping the program log prefix / + /// borsh-decoded). Encoding is program-specific. + data: Vec, + }, Raw(Vec), } @@ -449,6 +511,7 @@ impl TriggerData { TriggerData::Cron { .. } => "cron", TriggerData::AtProtoEvent { .. } => "atproto_event", TriggerData::HypercoreAppend { .. } => "hypercore_append", + TriggerData::SolanaProgramEvent { .. } => "solana_program_event", TriggerData::Raw(_) => "manual", } } @@ -457,7 +520,8 @@ impl TriggerData { match self { TriggerData::CosmosContractEvent { chain, .. } | TriggerData::EvmContractEvent { chain, .. } - | TriggerData::BlockInterval { chain, .. } => Some(chain), + | TriggerData::BlockInterval { chain, .. } + | TriggerData::SolanaProgramEvent { chain, .. } => Some(chain), TriggerData::Cron { .. } | TriggerData::AtProtoEvent { .. } | TriggerData::HypercoreAppend { .. } @@ -690,7 +754,8 @@ mod test_ext { }; use crate::{ - ByteArray, ChainKey, ChainKeyError, ComponentSource, ServiceId, WorkflowId, WorkflowIdError, + ByteArray, ChainKey, ChainKeyError, ComponentSource, ServiceId, SolanaEventFilter, + WorkflowId, WorkflowIdError, }; use super::{Component, Trigger, TriggerConfig}; @@ -731,6 +796,19 @@ mod test_ext { event_hash, } } + pub fn solana_program_event( + chain: impl TryInto, + program_id: crate::SolanaAddress, + filter: super::SolanaEventFilter, + commitment: crate::SolanaCommitment, + ) -> Self { + Trigger::SolanaProgramEvent { + chain: chain.try_into().unwrap(), + program_id, + filter, + commitment, + } + } } impl TriggerConfig { @@ -762,6 +840,21 @@ mod test_ext { } } + pub fn solana_program_event( + service_id: ServiceId, + workflow_id: impl TryInto, + chain: impl TryInto, + program_id: crate::SolanaAddress, + filter: SolanaEventFilter, + commitment: crate::SolanaCommitment, + ) -> Self { + Self { + service_id, + workflow_id: workflow_id.try_into().unwrap(), + trigger: Trigger::solana_program_event(chain, program_id, filter, commitment), + } + } + pub fn block_interval_event( service_id: ServiceId, workflow_id: impl TryInto, @@ -793,3 +886,87 @@ mod test_ext { } } } + +#[cfg(test)] +mod solana_trigger_tests { + use super::*; + use crate::{SolanaAddress, SolanaCommitment}; + + const SYSTEM_PROGRAM: &str = "11111111111111111111111111111111"; + + fn sample_trigger() -> Trigger { + Trigger::SolanaProgramEvent { + chain: "solana:mainnet".parse().unwrap(), + program_id: SolanaAddress::from_base58(SYSTEM_PROGRAM).unwrap(), + filter: SolanaEventFilter::Discriminator(vec![1, 2, 3, 4, 5, 6, 7, 8]), + commitment: SolanaCommitment::Confirmed, + } + } + + fn sample_trigger_data() -> TriggerData { + TriggerData::SolanaProgramEvent { + chain: "solana:mainnet".parse().unwrap(), + slot: 123_456_789, + signature: "5xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".to_string(), + instruction_index: 0, + inner_instruction_index: None, + log_index: 0, + program_id: SolanaAddress::from_base58(SYSTEM_PROGRAM).unwrap(), + data: vec![1, 2, 3, 4], + } + } + + #[test] + fn trigger_serde_round_trip() { + let trigger = sample_trigger(); + let json = serde_json::to_string(&trigger).unwrap(); + let back: Trigger = serde_json::from_str(&json).unwrap(); + assert_eq!(trigger, back); + } + + #[test] + fn trigger_data_serde_round_trip() { + let data = sample_trigger_data(); + let json = serde_json::to_string(&data).unwrap(); + let back: TriggerData = serde_json::from_str(&json).unwrap(); + assert_eq!(data, back); + } + + #[test] + fn trigger_data_trigger_type_is_solana_program_event() { + assert_eq!(sample_trigger_data().trigger_type(), "solana_program_event"); + } + + #[test] + fn trigger_data_chain_is_some_for_solana_event() { + let data = sample_trigger_data(); + let chain = data.chain().expect("solana trigger data has a chain"); + assert_eq!(chain.namespace.as_str(), "solana"); + } + + #[test] + fn solana_event_filter_log_contains_round_trip() { + let filter = SolanaEventFilter::LogContains("Program log: matched".to_string()); + let json = serde_json::to_string(&filter).unwrap(); + let back: SolanaEventFilter = serde_json::from_str(&json).unwrap(); + assert_eq!(filter, back); + } + + #[test] + fn trigger_commitment_defaults_to_confirmed_on_deserialize() { + // When the JSON omits `commitment`, `SolanaCommitment::default()` kicks in. + let json = serde_json::json!({ + "solana_program_event": { + "chain": "solana:mainnet", + "program_id": SYSTEM_PROGRAM, + "filter": { "discriminator": "0102030405060708" }, + } + }); + let trigger: Trigger = serde_json::from_value(json).unwrap(); + if let Trigger::SolanaProgramEvent { commitment, .. } = trigger { + assert_eq!(commitment, SolanaCommitment::Confirmed); + } else { + panic!("expected SolanaProgramEvent"); + } + } +} diff --git a/packages/types/src/solana.rs b/packages/types/src/solana.rs new file mode 100644 index 000000000..dee7eb64e --- /dev/null +++ b/packages/types/src/solana.rs @@ -0,0 +1,267 @@ +//! Solana / SVM primitive types shared across the WAVS type surface. +//! +//! This module intentionally avoids depending on `solana-sdk` for the v1 +//! trigger-only slice — a 32-byte base58-encoded pubkey wrapper and a +//! commitment-level enum are all that the trigger declaration + delivered +//! trigger data need. When slice 2 wires up the actual `solana-client` +//! subscription stream, we may revisit and replace [`SolanaAddress`] with a +//! `solana_sdk::Pubkey` newtype. + +use std::str::FromStr; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use thiserror::Error; +use utoipa::ToSchema; + +#[cfg(feature = "ts-bindings")] +use ts_rs::TS; + +/// A 32-byte Solana pubkey (program id, account address, etc.) serialized as +/// a base58 string. +/// +/// Solana on-chain addresses are 32 bytes of arbitrary data (the ed25519 +/// public key for keypair accounts, a derived hash for PDAs, etc.). The +/// canonical wire format is base58, which is what RPC endpoints, explorers +/// and most tooling speak. +#[cfg_attr(feature = "ts-bindings", derive(TS))] +#[cfg_attr(feature = "ts-bindings", ts(export, type = "string"))] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ToSchema)] +#[schema(value_type = String)] +pub struct SolanaAddress([u8; 32]); + +impl SolanaAddress { + /// Length of a Solana address in bytes. + pub const LEN: usize = 32; + + /// Construct from raw bytes. + pub const fn new(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Borrow the raw 32-byte representation. + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + /// Consume the address and return the raw 32-byte representation. + pub const fn into_bytes(self) -> [u8; 32] { + self.0 + } + + /// Parse a base58-encoded address. + pub fn from_base58(s: &str) -> Result { + let bytes = bs58::decode(s) + .into_vec() + .map_err(|e| SolanaAddressError::Base58(e.to_string()))?; + if bytes.len() != Self::LEN { + return Err(SolanaAddressError::WrongLength(bytes.len())); + } + let mut arr = [0u8; Self::LEN]; + arr.copy_from_slice(&bytes); + Ok(Self(arr)) + } + + /// Base58-encode the address. + pub fn to_base58(&self) -> String { + bs58::encode(self.0).into_string() + } +} + +impl std::fmt::Debug for SolanaAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SolanaAddress({})", self.to_base58()) + } +} + +impl std::fmt::Display for SolanaAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.to_base58()) + } +} + +impl FromStr for SolanaAddress { + type Err = SolanaAddressError; + + fn from_str(s: &str) -> Result { + Self::from_base58(s) + } +} + +impl TryFrom<&str> for SolanaAddress { + type Error = SolanaAddressError; + + fn try_from(s: &str) -> Result { + s.parse() + } +} + +impl From<[u8; 32]> for SolanaAddress { + fn from(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} + +impl From for [u8; 32] { + fn from(addr: SolanaAddress) -> Self { + addr.0 + } +} + +impl Serialize for SolanaAddress { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_base58()) + } +} + +impl<'de> Deserialize<'de> for SolanaAddress { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Self::from_base58(&s).map_err(serde::de::Error::custom) + } +} + +#[derive(Debug, Clone, Error, PartialEq, Eq)] +pub enum SolanaAddressError { + #[error("invalid base58 encoding: {0}")] + Base58(String), + #[error("solana address must be 32 bytes, got {0}")] + WrongLength(usize), +} + +/// Commitment level for Solana RPC subscriptions. +/// +/// `processed` — the bank has processed the slot but it may still be skipped. +/// `confirmed` — the slot has been voted on by a supermajority of the cluster. +/// `finalized` — the slot is rooted; reorgs above this level are not expected +/// during normal cluster operation. +/// +/// Default is `confirmed`, matching the trigger-side recommendation in the +/// SVM design doc. +#[cfg_attr(feature = "ts-bindings", derive(TS))] +#[cfg_attr(feature = "ts-bindings", ts(export))] +#[derive( + Debug, + Clone, + Copy, + Default, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + Serialize, + Deserialize, + ToSchema, +)] +#[serde(rename_all = "snake_case")] +pub enum SolanaCommitment { + Processed, + #[default] + Confirmed, + Finalized, +} + +impl std::fmt::Display for SolanaCommitment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SolanaCommitment::Processed => f.write_str("processed"), + SolanaCommitment::Confirmed => f.write_str("confirmed"), + SolanaCommitment::Finalized => f.write_str("finalized"), + } + } +} + +impl FromStr for SolanaCommitment { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "processed" => Ok(SolanaCommitment::Processed), + "confirmed" => Ok(SolanaCommitment::Confirmed), + "finalized" => Ok(SolanaCommitment::Finalized), + other => anyhow::bail!( + "invalid commitment '{}'. Must be one of: processed, confirmed, finalized", + other + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// System Program — the canonical, well-known address used in tests. + const SYSTEM_PROGRAM: &str = "11111111111111111111111111111111"; + + #[test] + fn address_roundtrip_base58() { + let addr = SolanaAddress::from_base58(SYSTEM_PROGRAM).unwrap(); + assert_eq!(addr.to_base58(), SYSTEM_PROGRAM); + // System program is all zeros. + assert_eq!(addr.into_bytes(), [0u8; 32]); + } + + #[test] + fn address_serde_json() { + let addr = SolanaAddress::from_base58(SYSTEM_PROGRAM).unwrap(); + let json = serde_json::to_string(&addr).unwrap(); + assert_eq!(json, format!("\"{}\"", SYSTEM_PROGRAM)); + let back: SolanaAddress = serde_json::from_str(&json).unwrap(); + assert_eq!(addr, back); + } + + #[test] + fn address_rejects_wrong_length() { + // "1" decodes to a single zero byte, far short of 32. + assert!(matches!( + SolanaAddress::from_base58("1"), + Err(SolanaAddressError::WrongLength(_)) + )); + } + + #[test] + fn address_rejects_invalid_base58() { + assert!(matches!( + SolanaAddress::from_base58("0OIl"), + Err(SolanaAddressError::Base58(_)) + )); + } + + #[test] + fn commitment_default_is_confirmed() { + assert_eq!(SolanaCommitment::default(), SolanaCommitment::Confirmed); + } + + #[test] + fn commitment_serde_round_trip() { + for level in [ + SolanaCommitment::Processed, + SolanaCommitment::Confirmed, + SolanaCommitment::Finalized, + ] { + let json = serde_json::to_string(&level).unwrap(); + let back: SolanaCommitment = serde_json::from_str(&json).unwrap(); + assert_eq!(level, back); + } + assert_eq!( + serde_json::to_string(&SolanaCommitment::Confirmed).unwrap(), + "\"confirmed\"" + ); + } + + #[test] + fn commitment_from_str() { + assert_eq!( + "Finalized".parse::().unwrap(), + SolanaCommitment::Finalized + ); + assert!("foo".parse::().is_err()); + } +} diff --git a/packages/utils/Cargo.toml b/packages/utils/Cargo.toml index f5c58a52c..b15de5f7c 100644 --- a/packages/utils/Cargo.toml +++ b/packages/utils/Cargo.toml @@ -42,6 +42,8 @@ alloy-signer-local = { workspace = true } alloy-network = { workspace = true } alloy-transport = { workspace = true } alloy-rpc-types-eth = { workspace = true } +solana-client = { workspace = true } +solana-commitment-config = { workspace = true } utoipa = { workspace = true } opentelemetry = { workspace = true } opentelemetry-jaeger-propagator = { workspace = true } diff --git a/packages/utils/src/config.rs b/packages/utils/src/config.rs index 94ea61926..57aed1bcc 100644 --- a/packages/utils/src/config.rs +++ b/packages/utils/src/config.rs @@ -1164,6 +1164,7 @@ mod test { ] .into_iter() .collect(), + solana: BTreeMap::new(), dev: BTreeMap::new(), } } diff --git a/packages/utils/src/health.rs b/packages/utils/src/health.rs index 938152788..4d5ac9414 100644 --- a/packages/utils/src/health.rs +++ b/packages/utils/src/health.rs @@ -1,6 +1,8 @@ use alloy_primitives::U256; use alloy_provider::Provider; use alloy_rpc_types_eth::SyncStatus; +use solana_client::nonblocking::rpc_client::RpcClient as SolanaRpcClient; +use solana_commitment_config::{CommitmentConfig, CommitmentLevel}; use thiserror::Error; use crate::{ @@ -8,7 +10,10 @@ use crate::{ error::EvmClientError, evm_client::{EvmEndpoint, EvmQueryClient}, }; -use wavs_types::{AnyChainConfig, ChainKey, CosmosChainConfig, EvmChainConfig}; +use wavs_types::{ + AnyChainConfig, ChainKey, CosmosChainConfig, EvmChainConfig, SolanaChainConfig, + SolanaCommitment, +}; pub async fn health_check_single_chain( key: &ChainKey, @@ -23,6 +28,10 @@ pub async fn health_check_single_chain( check_cosmos_chain_health_query(key.clone(), config.clone()).await?; tracing::info!("Cosmos chain [{key}] is healthy"); } + AnyChainConfig::Solana(config) => { + check_solana_chain_health_query(key.clone(), config.clone()).await?; + tracing::info!("Solana chain [{key}] is healthy"); + } } Ok(()) } @@ -101,6 +110,41 @@ async fn check_cosmos_chain_health_query( Ok(()) } +async fn check_solana_chain_health_query( + key: ChainKey, + config: SolanaChainConfig, +) -> Result<(), HealthCheckError> { + let http = config + .http_endpoint + .clone() + .ok_or_else(|| HealthCheckError::SolanaMissingHttpEndpoint(key.clone()))?; + + let commitment = CommitmentConfig { + commitment: match config.commitment { + SolanaCommitment::Processed => CommitmentLevel::Processed, + SolanaCommitment::Confirmed => CommitmentLevel::Confirmed, + SolanaCommitment::Finalized => CommitmentLevel::Finalized, + }, + }; + let client = SolanaRpcClient::new_with_commitment(http, commitment); + + // `getHealth` returns "ok" or an error code; succeeding here is the + // canonical "node is up and caught up" probe. + client + .get_health() + .await + .map_err(|e| HealthCheckError::SolanaHealth(key.clone(), e.to_string()))?; + + // Sanity-check we can read a slot, mirroring the evm + // `get_block_number` and cosmos `block_height` probes. + client + .get_slot() + .await + .map_err(|e| HealthCheckError::SolanaSlot(key.clone(), e.to_string()))?; + + Ok(()) +} + pub async fn check_evm_chain_endpoint_health_query( key: ChainKey, endpoint: EvmEndpoint, @@ -174,4 +218,13 @@ pub enum HealthCheckError { #[error("[{0}] node info: {1:?}")] CosmosNodeInfo(ChainKey, anyhow::Error), + + #[error("[{0}] Solana chain config has no http_endpoint")] + SolanaMissingHttpEndpoint(ChainKey), + + #[error("[{0}] Solana getHealth failed: {1}")] + SolanaHealth(ChainKey, String), + + #[error("[{0}] Solana getSlot failed: {1}")] + SolanaSlot(ChainKey, String), } diff --git a/packages/utils/src/test_utils/mock_chain_configs.rs b/packages/utils/src/test_utils/mock_chain_configs.rs index 2780be4f0..6385cebf7 100644 --- a/packages/utils/src/test_utils/mock_chain_configs.rs +++ b/packages/utils/src/test_utils/mock_chain_configs.rs @@ -31,6 +31,7 @@ pub fn mock_chain_configs() -> Arc> { )] .into_iter() .collect(), + solana: BTreeMap::new(), dev: BTreeMap::new(), })) } diff --git a/packages/wavs/Cargo.toml b/packages/wavs/Cargo.toml index 58e1c8167..b9d79ace5 100644 --- a/packages/wavs/Cargo.toml +++ b/packages/wavs/Cargo.toml @@ -72,6 +72,12 @@ hypercore-protocol = { workspace = true } hyperswarm = { workspace = true } tokio-util = { workspace = true } base64 = { workspace = true } +bs58 = { workspace = true } +solana-client = { workspace = true } +solana-pubkey = { workspace = true } +solana-commitment-config = { workspace = true } +solana-rpc-client-api = { workspace = true } +solana-transaction-status-client-types = { workspace = true } tauri = {workspace = true, optional = true} [dev-dependencies] diff --git a/packages/wavs/src/dispatcher.rs b/packages/wavs/src/dispatcher.rs index bdceec886..aebec6ed0 100644 --- a/packages/wavs/src/dispatcher.rs +++ b/packages/wavs/src/dispatcher.rs @@ -1043,6 +1043,13 @@ async fn query_service_from_address( service_uri } + AnyChainConfig::Solana(_) => { + // slice 2 / v2: Solana submission path. v1 is trigger-only — + // services with a Solana service manager are not yet supported. + return Err(DispatcherError::Config(format!( + "Solana service managers are not yet supported (chain {chain}, slice 2/v2)" + ))); + } }; let service_uri = UriString::try_from(service_uri)?; diff --git a/packages/wavs/src/http/handlers/service/get.rs b/packages/wavs/src/http/handlers/service/get.rs index c2bbee969..588688e8b 100644 --- a/packages/wavs/src/http/handlers/service/get.rs +++ b/packages/wavs/src/http/handlers/service/get.rs @@ -58,6 +58,16 @@ pub async fn handle_get_service( address, } } + AnyChainConfig::Solana(_) => { + // Solana service managers are a v2 deliverable — v1 of + // the SVM design doc is trigger-only and routes + // submission through the existing EVM / Cosmos service + // managers. See `docs/design/svm-support.md`. + return AnyError::from(anyhow!( + "Solana service managers are not yet supported (v2)" + )) + .into_response(); + } }, None => { return AnyError::from(anyhow!("missing chain config for {chain_key}")).into_response(); diff --git a/packages/wavs/src/subsystems/trigger.rs b/packages/wavs/src/subsystems/trigger.rs index b02591408..dda6d0d3b 100644 --- a/packages/wavs/src/subsystems/trigger.rs +++ b/packages/wavs/src/subsystems/trigger.rs @@ -99,6 +99,17 @@ impl TriggerCommand { chain: chain.clone(), }] } + AnyChainConfig::Solana(_) => { + // Block-interval (slot-interval) triggers on a + // Solana chain are not part of slice 2 (program + // log triggers only). Slot subscriptions are a + // v1.5 follow-up — see SVM design doc, "Non-Goals + // for v1". + tracing::warn!( + "Block interval on Solana chain {chain} not yet supported (v1.5)" + ); + Vec::new() + } }, None => { tracing::warn!("Block interval set for non-existant chain-config: {chain}"); @@ -113,6 +124,18 @@ impl TriggerCommand { feed_key: feed_key.clone(), }] } + Trigger::SolanaProgramEvent { chain, .. } => { + // Emit a chain-listening command; the Solana stream is + // started by the `AnyChainConfig::Solana` arm of the + // `StartListeningChain` handler below. Per-program + // filtering is done by the trigger lookup table (keyed by + // `(chain, program_id)`); the stream itself subscribes + // with `RpcTransactionLogsFilter::Mentions()`. + vec![Self::StartListeningChain { + chain: chain.clone(), + }] + } Trigger::Manual => Vec::new(), } } @@ -125,6 +148,55 @@ enum StreamStartState { Connected, } +/// Solana replay-identity tuple used by [`TriggerManager`] to dedup +/// dispatcher emissions across `solana-pubsub` reconnects. The SVM +/// design doc names this tuple as the "exactly once" key for Solana +/// triggers. +type SolanaReplayKey = (ChainKey, u64, String, u32, Option, u32); + +/// Bounded cache of Solana replay-identity tuples already dispatched. +/// Drops the oldest entry when over capacity. We use a paired +/// `HashSet` + `VecDeque` instead of an external LRU crate so the +/// surface stays workspace-trivial. +#[derive(Default)] +struct SolanaReplayCache { + seen: HashSet, + order: std::collections::VecDeque, + cap: usize, +} + +impl SolanaReplayCache { + fn with_capacity(cap: usize) -> Self { + Self { + seen: HashSet::with_capacity(cap), + order: std::collections::VecDeque::with_capacity(cap), + cap, + } + } + + /// Returns `true` if this is the first time we've seen `key`. + /// Returns `false` if `key` was already dispatched (and the caller + /// should drop the duplicate). + fn observe(&mut self, key: SolanaReplayKey) -> bool { + if !self.seen.insert(key.clone()) { + return false; + } + self.order.push_back(key); + if self.order.len() > self.cap { + if let Some(evicted) = self.order.pop_front() { + self.seen.remove(&evicted); + } + } + true + } +} + +/// Default capacity for [`SolanaReplayCache`]. Pubsub reconnects in +/// practice replay at most a few hundred recent log lines; 16k slots' +/// worth of typical traffic is well within memory while still letting +/// the cache absorb a re-subscription cleanly. +const SOLANA_REPLAY_CACHE_CAPACITY: usize = 16_384; + #[derive(Clone)] pub struct TriggerManager { pub chain_configs: Arc>, @@ -139,6 +211,12 @@ pub struct TriggerManager { pub services: Services, pub evm_controllers: Arc>>, hypercore_stream_states: Arc>>, + /// Replay-identity dedup cache for `Trigger::SolanaProgramEvent` + /// dispatch. The SVM design doc requires the same + /// `(chain, slot, signature, instruction_index, + /// inner_instruction_index, log_index)` tuple to never re-fire the + /// operator, including across `solana-pubsub` reconnects. + solana_replay: Arc>, pub config: Config, } @@ -165,6 +243,9 @@ impl TriggerManager { services, evm_controllers: Arc::new(std::sync::RwLock::new(HashMap::new())), hypercore_stream_states: Arc::new(std::sync::RwLock::new(HashMap::new())), + solana_replay: Arc::new(std::sync::Mutex::new(SolanaReplayCache::with_capacity( + SOLANA_REPLAY_CACHE_CAPACITY, + ))), config: config.clone(), }) } @@ -571,6 +652,97 @@ impl TriggerManager { *chain_state = StreamStartState::Connected; } } + AnyChainConfig::Solana(chain_config) => { + let ws_endpoint = match chain_config.ws_endpoint.clone() { + Some(ws) => ws, + None => { + tracing::error!( + "Solana chain {chain} has no ws_endpoint; cannot start stream" + ); + if let Some(chain_state) = + listening_chain_states.get_mut(&chain) + { + *chain_state = StreamStartState::Waiting; + } + continue; + } + }; + + // Collect every program id currently + // registered for this chain from the + // lookup table. The subscription + // re-fires whenever a service is + // added/removed only if a previously + // missing program id is now needed — + // for v1, we re-subscribe on chain + // (re)start and accept that adding a + // service with a new program id + // requires a chain re-listen. + let program_ids: Vec = self + .lookup_maps + .triggers_by_solana_program + .read() + .unwrap() + .keys() + .filter_map( + |(k, p)| { + if k == &chain { + Some(*p) + } else { + None + } + }, + ) + .collect(); + + if program_ids.is_empty() { + tracing::debug!( + "No Solana programs registered for chain {chain}; deferring stream start until a SolanaProgramEvent trigger is added" + ); + if let Some(chain_state) = + listening_chain_states.get_mut(&chain) + { + *chain_state = StreamStartState::Waiting; + } + continue; + } + + let solana_config = + streams::solana_stream::SolanaStreamConfig { + ws_endpoint, + program_ids, + commitment: chain_config.commitment, + }; + + match streams::solana_stream::start_solana_stream( + chain.clone(), + solana_config, + self.metrics.clone(), + ) + .await + { + Ok(solana_stream) => { + multiplexed_stream.push(solana_stream); + if let Some(chain_state) = + listening_chain_states.get_mut(&chain) + { + *chain_state = StreamStartState::Connected; + } + } + Err(err) => { + tracing::error!( + "Failed to start Solana event stream: {:?}", + err + ); + if let Some(chain_state) = + listening_chain_states.get_mut(&chain) + { + *chain_state = StreamStartState::Waiting; + } + continue; + } + } + } } } TriggerCommand::WatchEvmContractEvents { @@ -1054,6 +1226,9 @@ impl TriggerManager { StreamTriggers::Hypercore { event } => { dispatcher_commands.extend(self.handle_hypercore_event(event)); } + StreamTriggers::Solana { chain, slot, logs } => { + dispatcher_commands.extend(self.handle_solana_logs(chain, slot, logs)); + } } if !dispatcher_commands.is_empty() { @@ -1127,6 +1302,114 @@ impl TriggerManager { } } + /// Match each parsed Solana log line against the registered + /// per-program triggers, test the per-trigger filter + /// ([`wavs_types::SolanaEventFilter`]) against the raw log line, and + /// emit one [`DispatcherCommand::Trigger`] per match. + /// + /// `chain` and `slot` are the replay-identity prefix shared by every + /// log line in `logs`; per-log fields (`signature`, `log_index`, + /// `instruction_index`, `inner_instruction_index`) come from each + /// [`streams::solana_stream::SolanaStreamLog`]. + pub fn handle_solana_logs( + &self, + chain: ChainKey, + _slot: u64, + logs: Vec, + ) -> Vec { + let mut commands = Vec::new(); + + // Snapshot of the lookup table by `(chain, program_id)`. We take + // the read lock once per notification rather than once per log. + let triggers_by_program = self.lookup_maps.triggers_by_solana_program.read().unwrap(); + + for log in logs { + let key = (chain.clone(), log.program_id); + let candidate_lookups = match triggers_by_program.get(&key) { + Some(set) => set.clone(), + None => continue, + }; + + for lookup_id in candidate_lookups { + let trigger_config = match self.lookup_maps.get_trigger_config(lookup_id) { + Some(cfg) => cfg, + None => continue, + }; + + let (filter, declared_program_id) = match &trigger_config.trigger { + Trigger::SolanaProgramEvent { + filter, program_id, .. + } => (filter.clone(), *program_id), + _ => continue, + }; + + // Defensive: the lookup-table key should have already + // narrowed program_id, but the dispatcher re-checks in + // case a race re-keyed the lookup. + if declared_program_id != log.program_id { + continue; + } + + let Some(decoded) = streams::solana_stream::match_log_filter(&filter, &log.raw_log) + else { + continue; + }; + + // For `Discriminator` matches we have the decoded payload + // from the `Program data:` line; for `LogContains` we + // deliver the raw log line bytes (UTF-8) so the component + // can re-parse if needed. + let data = decoded.unwrap_or_else(|| log.raw_log.as_bytes().to_vec()); + + // Enforce the design doc's "exactly once" property: if + // we've already dispatched this exact replay-identity + // tuple (eg. because a `solana-pubsub` reconnect + // re-yielded the log), drop it. Keyed per-(workflow, + // log) so two distinct triggers on the same log line + // still both fire. + let key: SolanaReplayKey = ( + chain.clone(), + log.slot, + log.signature.clone(), + log.instruction_index, + log.inner_instruction_index, + log.log_index, + ); + let first_time = { + let mut cache = self.solana_replay.lock().unwrap(); + cache.observe(key) + }; + if !first_time { + tracing::debug!( + chain = %chain, + slot = log.slot, + signature = %log.signature, + instruction_index = log.instruction_index, + log_index = log.log_index, + "dropping duplicate Solana program event (replay-identity already dispatched)" + ); + continue; + } + + commands.push(DispatcherCommand::Trigger(TriggerAction { + data: TriggerData::SolanaProgramEvent { + chain: chain.clone(), + slot: log.slot, + signature: log.signature.clone(), + instruction_index: log.instruction_index, + inner_instruction_index: log.inner_instruction_index, + log_index: log.log_index, + program_id: log.program_id, + data, + }, + config: trigger_config, + })); + } + } + + commands + } + fn handle_hypercore_event( &self, event: streams::hypercore_stream::HypercoreAppendEvent, diff --git a/packages/wavs/src/subsystems/trigger/lookup.rs b/packages/wavs/src/subsystems/trigger/lookup.rs index 1f13f15bf..ece187863 100644 --- a/packages/wavs/src/subsystems/trigger/lookup.rs +++ b/packages/wavs/src/subsystems/trigger/lookup.rs @@ -6,7 +6,8 @@ use std::{ use bimap::BiMap; use utils::telemetry::TriggerMetrics; use wavs_types::{ - AtProtoAction, ByteArray, ChainKey, ServiceId, Trigger, TriggerConfig, WorkflowId, + AtProtoAction, ByteArray, ChainKey, ServiceId, SolanaAddress, Trigger, TriggerConfig, + WorkflowId, }; use crate::{ @@ -41,6 +42,20 @@ pub struct LookupMaps { Arc, Option), HashSet>>>, /// lookup id by hypercore feed key pub triggers_by_hypercore_append: Arc>>>, + /// lookup id by (solana chain, program id). Per-trigger + /// [`wavs_types::SolanaEventFilter`] (Anchor discriminator or log + /// substring) is checked against the actual log line inside the + /// dispatcher — see `dispatcher_match_solana_log` below. + /// + /// Solana doesn't expose an event-hash equivalent in the log stream + /// (Anchor discriminators only appear on `Program data:` lines, and + /// non-Anchor programs use free-text logs), so we group by + /// `(chain, program_id)` and let the dispatcher do the secondary + /// filter check. This mirrors how the EVM lookup hits the + /// `event_hash` first and then the per-trigger config narrows further + /// via the log topic shape. + pub triggers_by_solana_program: + Arc>>>, // ServiceId <-> ServiceManager address pub service_manager: Arc>>, /// Efficient block schedulers (one per chain) for block interval triggers @@ -64,6 +79,7 @@ impl LookupMaps { triggers_by_atproto_event_exact: Arc::new(RwLock::new(HashMap::new())), triggers_by_atproto_event_pattern: Arc::new(RwLock::new(HashMap::new())), triggers_by_hypercore_append: Arc::new(RwLock::new(HashMap::new())), + triggers_by_solana_program: Arc::new(RwLock::new(HashMap::new())), block_schedulers: BlockSchedulers::default(), triggers_by_service_workflow: Arc::new(RwLock::new(BTreeMap::new())), service_manager: Arc::new(RwLock::new(BiMap::new())), @@ -224,6 +240,21 @@ impl LookupMaps { .or_default() .insert(lookup_id); } + Trigger::SolanaProgramEvent { + chain, program_id, .. + } => { + // Key by (chain, program_id); the per-trigger + // `SolanaEventFilter` (Anchor discriminator or + // `LogContains` substring) is checked inside the + // dispatcher against the actual log lines. + let key = (chain.clone(), program_id); + self.triggers_by_solana_program + .write() + .unwrap() + .entry(key) + .or_default() + .insert(lookup_id); + } Trigger::Manual => {} } @@ -345,6 +376,18 @@ impl LookupMaps { } } } + Trigger::SolanaProgramEvent { + chain, program_id, .. + } => { + let key = (chain, program_id); + let mut lock = self.triggers_by_solana_program.write().unwrap(); + if let Some(set) = lock.get_mut(&key) { + set.remove(&lookup_id); + if set.is_empty() { + lock.remove(&key); + } + } + } } } @@ -365,6 +408,7 @@ impl LookupMaps { let mut triggers_by_atproto_event_pattern = self.triggers_by_atproto_event_pattern.write().unwrap(); let mut triggers_by_hypercore_append = self.triggers_by_hypercore_append.write().unwrap(); + let mut triggers_by_solana_program = self.triggers_by_solana_program.write().unwrap(); let mut triggers_by_service_workflow_lock = self.triggers_by_service_workflow.write().unwrap(); @@ -464,6 +508,17 @@ impl LookupMaps { } } } + Trigger::SolanaProgramEvent { + chain, program_id, .. + } => { + let key = (chain.clone(), *program_id); + if let Some(set) = triggers_by_solana_program.get_mut(&key) { + set.remove(lookup_id); + if set.is_empty() { + triggers_by_solana_program.remove(&key); + } + } + } } } } diff --git a/packages/wavs/src/subsystems/trigger/streams.rs b/packages/wavs/src/subsystems/trigger/streams.rs index 9c2522bf4..1075c2092 100644 --- a/packages/wavs/src/subsystems/trigger/streams.rs +++ b/packages/wavs/src/subsystems/trigger/streams.rs @@ -5,11 +5,13 @@ pub mod evm_stream; pub mod hypercore_protocol; pub mod hypercore_stream; pub mod local_command_stream; +pub mod solana_stream; use self::hypercore_stream::HypercoreAppendEvent; use crate::subsystems::trigger::{ streams::atproto_jetstream::AtProtoEvent, - streams::cosmos_stream::StreamTriggerCosmosContractEvent, TriggerCommand, + streams::cosmos_stream::StreamTriggerCosmosContractEvent, + streams::solana_stream::SolanaStreamLog, TriggerCommand, }; use super::{error::TriggerError, lookup::LookupId}; @@ -70,4 +72,13 @@ pub enum StreamTriggers { Hypercore { event: HypercoreAppendEvent, }, + /// One Solana transaction's parsed log lines, all sharing the same + /// `(slot, signature)` replay-identity prefix. The dispatcher walks + /// `logs` and tests each entry's `program_id` + raw log against the + /// registered per-trigger filters. + Solana { + chain: ChainKey, + slot: u64, + logs: Vec, + }, } diff --git a/packages/wavs/src/subsystems/trigger/streams/solana_stream.rs b/packages/wavs/src/subsystems/trigger/streams/solana_stream.rs new file mode 100644 index 000000000..08c928f68 --- /dev/null +++ b/packages/wavs/src/subsystems/trigger/streams/solana_stream.rs @@ -0,0 +1,591 @@ +//! Solana program-event trigger stream. +//! +//! Peer of [`evm_stream`](super::evm_stream) and [`cosmos_stream`](super::cosmos_stream). +//! Subscribes to a Solana RPC WebSocket via `logs_subscribe` filtered to +//! mentions of the configured program ids, then yields [`StreamTriggers::Solana`] +//! items annotated with the replay-identity tuple `(slot, signature, +//! instruction_index, inner_instruction_index, log_index)` from the SVM +//! design doc. +//! +//! v1 scope: +//! - `logs_subscribe` with `RpcTransactionLogsFilter::Mentions(program_ids)`. +//! - Per-trigger filter matching (Anchor discriminator on `Program data:` +//! lines, or substring match on `Program log:` lines) lives in the +//! dispatcher; this stream emits one [`SolanaStreamLog`] per log line so +//! the lookup table can do the per-trigger filter check. +//! - Reconnection: bounded exponential backoff with jitter, matching the +//! ATProto Jetstream stream pattern. We do not surface partial-progress +//! cursors — `solana-pubsub-client` reconnects re-subscribe from the +//! tip; replay protection is the caller's job via the replay-identity +//! tuple. + +use std::pin::Pin; +use std::time::Duration; + +use futures::{Stream, StreamExt}; +use solana_client::nonblocking::pubsub_client::PubsubClient; +use solana_commitment_config::{CommitmentConfig, CommitmentLevel}; +use solana_rpc_client_api::config::{RpcTransactionLogsConfig, RpcTransactionLogsFilter}; +use tokio::time::sleep; +use tracing::{error, info, warn}; +use utils::telemetry::TriggerMetrics; +use wavs_types::{ChainKey, SolanaAddress, SolanaCommitment}; + +use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; +use wavs_types::SolanaEventFilter; + +use crate::subsystems::trigger::error::TriggerError; +use crate::subsystems::trigger::streams::StreamTriggers; + +/// Parsed instruction context for a single log line. +/// +/// Solana log streams interleave many transactions / instructions per +/// subscription; this struct carries the index information the dispatcher +/// needs to construct the replay-identity tuple. +#[derive(Debug, Clone)] +pub struct SolanaStreamLog { + /// The slot the transaction was observed in (from the subscription + /// notification `context.slot`). + pub slot: u64, + /// Transaction signature, base58-encoded. + pub signature: String, + /// 0-based index of the top-level instruction that produced this log. + pub instruction_index: u32, + /// If the log came from an inner instruction (CPI), the 0-based index + /// of that inner instruction within the outer instruction; `None` for + /// top-level instructions. + pub inner_instruction_index: Option, + /// 0-based index of this log line within the transaction's log array. + pub log_index: u32, + /// Program id that emitted the log (parsed from the most recent + /// `Program {programId} invoke [N]` marker, or the subscription's + /// filter program id if parsing failed). + pub program_id: SolanaAddress, + /// The raw log line as returned by the validator. The dispatcher tests + /// per-trigger filters against this — Anchor discriminator matching + /// runs on the base64 payload following the `Program data:` prefix, + /// `LogContains` runs on the full line. + pub raw_log: String, +} + +/// Configuration handed to [`start_solana_stream`]. +#[derive(Debug, Clone)] +pub struct SolanaStreamConfig { + /// WebSocket endpoint URL (e.g. `wss://api.devnet.solana.com`). + pub ws_endpoint: String, + /// Program ids the subscription should filter on + /// (`RpcTransactionLogsFilter::Mentions`). + pub program_ids: Vec, + /// Default commitment level for the subscription. + pub commitment: SolanaCommitment, +} + +/// Maximum number of reconnect attempts before yielding an error and +/// returning. Matches the ATProto Jetstream stream. +const MAX_RECONNECTS: u32 = 10; +/// Exponential backoff base delay. +const BASE_DELAY: Duration = Duration::from_secs(1); +/// Exponential backoff cap. +const MAX_DELAY: Duration = Duration::from_secs(60); + +/// Start a Solana logs subscription against `config.ws_endpoint`. +/// +/// Returns a `Stream` of [`StreamTriggers::Solana`] items. The stream +/// reconnects on transport / subscription errors with bounded exponential +/// backoff. After [`MAX_RECONNECTS`] failed reconnects, it yields a final +/// error and terminates. +pub async fn start_solana_stream( + chain: ChainKey, + config: SolanaStreamConfig, + metrics: TriggerMetrics, +) -> Result> + Send>>, TriggerError> +{ + if config.program_ids.is_empty() { + return Err(TriggerError::Config(format!( + "Solana stream for chain {chain} started with no program ids; \ + subscribing to all logs is intentionally not supported" + ))); + } + + let stream = async_stream::stream! { + let mut reconnect_count: u32 = 0; + + // The pubsub client owns the websocket and the active subscription + // handle; we recreate it on every reconnect. + loop { + info!( + chain = %chain, + endpoint = %config.ws_endpoint, + program_ids = config.program_ids.len(), + "Connecting to Solana logs subscription" + ); + + let client = match PubsubClient::new(&config.ws_endpoint).await { + Ok(client) => client, + Err(err) => { + error!( + chain = %chain, + error = ?err, + "Failed to connect to Solana pubsub websocket" + ); + metrics.increment_total_errors("solana_pubsub_connect"); + if let Some(delay) = backoff_delay(reconnect_count) { + warn!(chain = %chain, ?delay, "reconnecting Solana pubsub"); + sleep(delay).await; + reconnect_count += 1; + continue; + } else { + yield Err(TriggerError::Config(format!( + "Solana pubsub for chain {chain} exhausted reconnects: {err}" + ))); + return; + } + } + }; + + let mentions = config + .program_ids + .iter() + .map(|p| p.to_base58()) + .collect::>(); + let filter = RpcTransactionLogsFilter::Mentions(mentions); + let sub_config = RpcTransactionLogsConfig { + commitment: Some(solana_commitment_to_config(config.commitment)), + }; + + let (mut sub_stream, _unsubscribe) = + match client.logs_subscribe(filter, sub_config).await { + Ok(s) => s, + Err(err) => { + error!( + chain = %chain, + error = ?err, + "Failed to logs_subscribe on Solana pubsub" + ); + metrics.increment_total_errors("solana_pubsub_subscribe"); + if let Some(delay) = backoff_delay(reconnect_count) { + sleep(delay).await; + reconnect_count += 1; + continue; + } else { + yield Err(TriggerError::Config(format!( + "Solana logs_subscribe for chain {chain} exhausted reconnects: {err}" + ))); + return; + } + } + }; + + // Successfully subscribed; reset the backoff counter. + reconnect_count = 0; + info!(chain = %chain, "Solana logs subscription active"); + + // The default program id we attribute to logs is the + // subscription's first program id, used as the fallback if + // parsing the `Program {id} invoke [N]` markers fails for some + // reason (it shouldn't, but defensive). + let default_program_id = config.program_ids[0]; + + while let Some(notification) = sub_stream.next().await { + let slot = notification.context.slot; + let signature = notification.value.signature; + let logs = notification.value.logs; + + // Skip failed transactions. The design doc treats reorgs + // above the chosen commitment as user error, but a failed + // transaction at the chosen commitment is not a trigger — + // the on-chain state did not change. + if notification.value.err.is_some() { + tracing::debug!( + chain = %chain, + signature = %signature, + err = ?notification.value.err, + "Skipping failed Solana transaction" + ); + continue; + } + + let parsed = parse_transaction_logs(&logs, default_program_id); + let parsed_logs: Vec = parsed + .into_iter() + .map(|(log_index, ctx, raw_log)| SolanaStreamLog { + slot, + signature: signature.clone(), + instruction_index: ctx.instruction_index, + inner_instruction_index: ctx.inner_instruction_index, + log_index, + program_id: ctx.program_id, + raw_log, + }) + .collect(); + + if !parsed_logs.is_empty() { + yield Ok(StreamTriggers::Solana { + chain: chain.clone(), + slot, + logs: parsed_logs, + }); + } + } + + // The subscription stream ended (server closed it, network + // dropped, etc.); fall through to the reconnect loop. + warn!(chain = %chain, "Solana logs subscription closed; reconnecting"); + metrics.increment_total_errors("solana_pubsub_disconnect"); + + if let Some(delay) = backoff_delay(reconnect_count) { + sleep(delay).await; + reconnect_count += 1; + } else { + yield Err(TriggerError::Config(format!( + "Solana logs subscription for chain {chain} exhausted reconnects" + ))); + return; + } + } + }; + + Ok(Box::pin(stream)) +} + +/// Translate a slice 1 [`SolanaCommitment`] to the `solana-client` +/// [`CommitmentConfig`] used by the RPC + pubsub clients. +pub fn solana_commitment_to_config(commitment: SolanaCommitment) -> CommitmentConfig { + CommitmentConfig { + commitment: match commitment { + SolanaCommitment::Processed => CommitmentLevel::Processed, + SolanaCommitment::Confirmed => CommitmentLevel::Confirmed, + SolanaCommitment::Finalized => CommitmentLevel::Finalized, + }, + } +} + +/// Exponential backoff with jitter. Returns `None` if `attempt` has +/// exhausted [`MAX_RECONNECTS`]. +fn backoff_delay(attempt: u32) -> Option { + if attempt >= MAX_RECONNECTS { + return None; + } + let exp_delay = BASE_DELAY + .checked_mul(2u32.saturating_pow(attempt)) + .unwrap_or(MAX_DELAY); + let delay = std::cmp::min(exp_delay, MAX_DELAY); + let jitter = Duration::from_millis(rand::random::() % 1000); + Some(delay + jitter) +} + +/// Parsed instruction context for a single log line, before being wrapped +/// into a [`SolanaStreamLog`]. +#[derive(Debug, Clone, Copy)] +struct LogContext { + instruction_index: u32, + inner_instruction_index: Option, + program_id: SolanaAddress, +} + +/// Parse a transaction's log array and annotate each non-marker log line +/// with the `(instruction_index, inner_instruction_index, program_id)` +/// it came from. +/// +/// Solana validator logs use this shape: +/// +/// ```text +/// Program invoke [1] // top-level instruction starts +/// Program log: ... // program output +/// Program data: // anchor event payload (8-byte disc + payload) +/// Program invoke [2] // CPI +/// Program success // CPI ends +/// Program success // top-level ends +/// ``` +/// +/// `invoke [N]` markers nest by depth. We track: +/// - `top_level_counter`: incremented on every `invoke [1]` +/// - `inner_counter`: reset on every `invoke [1]`, incremented on +/// `invoke [N]` for N >= 2 +/// - `program_stack`: pushed on every invoke, popped on success/failed, +/// so we know which program emitted the next log line +/// +/// Returns `(log_index, context, raw_log)` for each log line that is not +/// itself a marker — markers are noisy and don't carry application data. +fn parse_transaction_logs( + logs: &[String], + default_program_id: SolanaAddress, +) -> Vec<(u32, LogContext, String)> { + let mut out = Vec::with_capacity(logs.len()); + let mut top_level_counter: i32 = -1; // first invoke [1] -> 0 + let mut inner_counter: i32 = -1; + let mut program_stack: Vec = Vec::new(); + let mut current_depth: u32 = 0; + + for (log_index, line) in logs.iter().enumerate() { + let log_index_u32 = log_index as u32; + let trimmed = line.as_str(); + + if let Some((program_id, depth)) = parse_invoke_marker(trimmed) { + current_depth = depth; + if depth == 1 { + top_level_counter = top_level_counter.saturating_add(1); + inner_counter = -1; + } else { + inner_counter = inner_counter.saturating_add(1); + } + program_stack.push(program_id); + // Markers themselves are not yielded as content lines. + continue; + } + + if is_invoke_terminator(trimmed) { + program_stack.pop(); + current_depth = current_depth.saturating_sub(1); + continue; + } + + // Non-marker log line. Attribute to the current invoke context. + let program_id = program_stack.last().copied().unwrap_or(default_program_id); + let instruction_index = u32::try_from(top_level_counter.max(0)).unwrap_or(0); + let inner_instruction_index = if current_depth >= 2 { + Some(u32::try_from(inner_counter.max(0)).unwrap_or(0)) + } else { + None + }; + + out.push(( + log_index_u32, + LogContext { + instruction_index, + inner_instruction_index, + program_id, + }, + line.clone(), + )); + } + + out +} + +/// Match `Program invoke []` and return `(program_id, depth)`. +fn parse_invoke_marker(line: &str) -> Option<(SolanaAddress, u32)> { + let rest = line.strip_prefix("Program ")?; + let (program_str, rest) = rest.split_once(' ')?; + let rest = rest.strip_prefix("invoke [")?; + let (depth_str, _) = rest.split_once(']')?; + let depth: u32 = depth_str.parse().ok()?; + let program_id = SolanaAddress::from_base58(program_str).ok()?; + Some((program_id, depth)) +} + +/// Test a [`SolanaEventFilter`] against a single raw log line. +/// +/// - [`SolanaEventFilter::Discriminator`]: matches when the log is a +/// `Program data:` line whose base64-decoded payload starts with the +/// given discriminator bytes. This is the Anchor event convention — +/// Anchor emits its 8-byte discriminator + borsh-serialized payload as +/// a base64 string on a `Program data:` line. +/// - [`SolanaEventFilter::LogContains`]: substring match against the raw +/// log line (Anchor / non-Anchor agnostic). +/// +/// Returns `(matched, decoded_payload)`. `decoded_payload` is `Some` only +/// for `Discriminator` matches on a parseable `Program data:` line so the +/// dispatcher can hand the decoded bytes to the component; for +/// `LogContains` matches we return `None` (the raw log line is what the +/// component cares about). +pub fn match_log_filter(filter: &SolanaEventFilter, raw_log: &str) -> Option>> { + match filter { + SolanaEventFilter::Discriminator(disc) => { + let payload_b64 = raw_log.strip_prefix("Program data: ")?; + let decoded = BASE64_STANDARD.decode(payload_b64.trim()).ok()?; + if decoded.len() >= disc.len() && decoded[..disc.len()] == disc[..] { + Some(Some(decoded)) + } else { + None + } + } + SolanaEventFilter::LogContains(needle) => { + if raw_log.contains(needle.as_str()) { + Some(None) + } else { + None + } + } + } +} + +/// Match `Program success` or `Program failed: ...`. +fn is_invoke_terminator(line: &str) -> bool { + if let Some(rest) = line.strip_prefix("Program ") { + if let Some((_, tail)) = rest.split_once(' ') { + return tail == "success" || tail.starts_with("failed"); + } + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + + const PROG_A: &str = "11111111111111111111111111111111"; + // Token Program (well-known) — used as the inner-CPI target in tests. + const PROG_B: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; + + fn addr(s: &str) -> SolanaAddress { + SolanaAddress::from_base58(s).unwrap() + } + + #[test] + fn parses_top_level_log() { + let logs = vec![ + format!("Program {PROG_A} invoke [1]"), + "Program log: hello".into(), + format!("Program {PROG_A} success"), + ]; + let parsed = parse_transaction_logs(&logs, addr(PROG_A)); + assert_eq!(parsed.len(), 1); + let (log_index, ctx, raw) = &parsed[0]; + assert_eq!(*log_index, 1); + assert_eq!(ctx.instruction_index, 0); + assert_eq!(ctx.inner_instruction_index, None); + assert_eq!(ctx.program_id, addr(PROG_A)); + assert_eq!(raw, "Program log: hello"); + } + + #[test] + fn parses_inner_instruction_log() { + let logs = vec![ + format!("Program {PROG_A} invoke [1]"), + "Program log: outer".into(), + format!("Program {PROG_B} invoke [2]"), + "Program log: inner".into(), + format!("Program {PROG_B} success"), + "Program log: after_inner".into(), + format!("Program {PROG_A} success"), + ]; + let parsed = parse_transaction_logs(&logs, addr(PROG_A)); + assert_eq!(parsed.len(), 3); + + // Line 1: top-level + assert_eq!(parsed[0].1.instruction_index, 0); + assert_eq!(parsed[0].1.inner_instruction_index, None); + assert_eq!(parsed[0].1.program_id, addr(PROG_A)); + + // Line 3: inner CPI + assert_eq!(parsed[1].1.instruction_index, 0); + assert_eq!(parsed[1].1.inner_instruction_index, Some(0)); + assert_eq!(parsed[1].1.program_id, addr(PROG_B)); + + // Line 5: back to top-level after CPI returned + assert_eq!(parsed[2].1.instruction_index, 0); + assert_eq!(parsed[2].1.inner_instruction_index, None); + assert_eq!(parsed[2].1.program_id, addr(PROG_A)); + } + + #[test] + fn parses_multiple_top_level_instructions() { + let logs = vec![ + format!("Program {PROG_A} invoke [1]"), + "Program log: ix0".into(), + format!("Program {PROG_A} success"), + format!("Program {PROG_A} invoke [1]"), + "Program log: ix1".into(), + format!("Program {PROG_A} success"), + ]; + let parsed = parse_transaction_logs(&logs, addr(PROG_A)); + assert_eq!(parsed.len(), 2); + assert_eq!(parsed[0].1.instruction_index, 0); + assert_eq!(parsed[1].1.instruction_index, 1); + } + + #[test] + fn handles_failed_inner_terminator() { + let logs = vec![ + format!("Program {PROG_A} invoke [1]"), + format!("Program {PROG_B} invoke [2]"), + format!("Program {PROG_B} failed: custom error 0x1"), + "Program log: outer continues".into(), + format!("Program {PROG_A} success"), + ]; + let parsed = parse_transaction_logs(&logs, addr(PROG_A)); + assert_eq!(parsed.len(), 1); + // The line is after the CPI failed; depth should be back to 1. + assert_eq!(parsed[0].1.inner_instruction_index, None); + assert_eq!(parsed[0].1.program_id, addr(PROG_A)); + } + + #[test] + fn unparseable_lines_fall_back_to_default_program() { + // No invoke marker at all — the line still gets emitted but with + // instruction_index 0 and the fallback program id. + let logs = vec!["unexpected log shape".into()]; + let parsed = parse_transaction_logs(&logs, addr(PROG_A)); + assert_eq!(parsed.len(), 1); + assert_eq!(parsed[0].1.instruction_index, 0); + assert_eq!(parsed[0].1.program_id, addr(PROG_A)); + } + + #[test] + fn commitment_translation() { + assert_eq!( + solana_commitment_to_config(SolanaCommitment::Processed).commitment, + CommitmentLevel::Processed + ); + assert_eq!( + solana_commitment_to_config(SolanaCommitment::Confirmed).commitment, + CommitmentLevel::Confirmed + ); + assert_eq!( + solana_commitment_to_config(SolanaCommitment::Finalized).commitment, + CommitmentLevel::Finalized + ); + } + + #[test] + fn backoff_delay_caps_at_max_reconnects() { + assert!(backoff_delay(0).is_some()); + assert!(backoff_delay(MAX_RECONNECTS - 1).is_some()); + assert!(backoff_delay(MAX_RECONNECTS).is_none()); + } + + #[test] + fn match_log_filter_discriminator_hit() { + let payload = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0xff, 0xff]; + let b64 = BASE64_STANDARD.encode(&payload); + let log = format!("Program data: {b64}"); + + let disc = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; + let filter = SolanaEventFilter::Discriminator(disc.clone()); + + let res = match_log_filter(&filter, &log).expect("should match"); + assert_eq!(res, Some(payload)); + } + + #[test] + fn match_log_filter_discriminator_miss_wrong_disc() { + let payload = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; + let b64 = BASE64_STANDARD.encode(&payload); + let log = format!("Program data: {b64}"); + + let filter = SolanaEventFilter::Discriminator(vec![0xaa; 8]); + assert!(match_log_filter(&filter, &log).is_none()); + } + + #[test] + fn match_log_filter_discriminator_miss_wrong_prefix() { + // Not a `Program data:` line — discriminator filter should miss. + let filter = SolanaEventFilter::Discriminator(vec![0x01; 8]); + assert!(match_log_filter(&filter, "Program log: AQECAwQFBgcI").is_none()); + } + + #[test] + fn match_log_filter_log_contains_hit() { + let filter = SolanaEventFilter::LogContains("matched".to_string()); + let res = match_log_filter(&filter, "Program log: this matched the needle") + .expect("should match"); + assert!(res.is_none()); + } + + #[test] + fn match_log_filter_log_contains_miss() { + let filter = SolanaEventFilter::LogContains("missing".to_string()); + assert!(match_log_filter(&filter, "Program log: nothing here").is_none()); + } +} diff --git a/packages/wavs/tests/trigger_tests.rs b/packages/wavs/tests/trigger_tests.rs index 10504e720..db8b52f7c 100644 --- a/packages/wavs/tests/trigger_tests.rs +++ b/packages/wavs/tests/trigger_tests.rs @@ -397,3 +397,238 @@ async fn cron_trigger_is_removed_when_config_is_gone() { "Expected no triggers to fire after removing all" ); } + +#[test] +fn solana_program_event_lookup() { + use wavs_types::{SolanaAddress, SolanaCommitment, SolanaEventFilter}; + + let config = Config::default(); + let services = wavs::services::Services::new(WavsDb::new().unwrap()); + let (trigger_to_dispatcher_tx, _) = crossbeam::channel::unbounded::(); + let manager = TriggerManager::new( + &config, + TriggerMetrics::new(opentelemetry::global::meter("trigger-test-metrics")), + services, + trigger_to_dispatcher_tx, + ) + .unwrap(); + + let chain = ChainKey::new("solana:devnet").unwrap(); + let system_program = SolanaAddress::from_base58("11111111111111111111111111111111").unwrap(); + let token_program = + SolanaAddress::from_base58("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(); + + // Two triggers for the same (chain, program_id) but with different + // filters: an Anchor discriminator and a log-substring. The lookup + // table should group both under (chain, system_program); per-filter + // dispatch is the dispatcher's job. + let service_a = ServiceId::hash("svc-a"); + let workflow_a = WorkflowId::new("wf-a").unwrap(); + let trigger_a = TriggerConfig::solana_program_event( + service_a.clone(), + workflow_a.to_string().as_str(), + chain.to_string().as_str(), + system_program, + SolanaEventFilter::Discriminator(vec![1, 2, 3, 4, 5, 6, 7, 8]), + SolanaCommitment::Confirmed, + ); + + let service_b = ServiceId::hash("svc-b"); + let workflow_b = WorkflowId::new("wf-b").unwrap(); + let trigger_b = TriggerConfig::solana_program_event( + service_b.clone(), + workflow_b.to_string().as_str(), + chain.to_string().as_str(), + system_program, + SolanaEventFilter::LogContains("matched".to_string()), + SolanaCommitment::Confirmed, + ); + + // Third trigger on a different program — should not collide with the + // first two. + let service_c = ServiceId::hash("svc-c"); + let workflow_c = WorkflowId::new("wf-c").unwrap(); + let trigger_c = TriggerConfig::solana_program_event( + service_c.clone(), + workflow_c.to_string().as_str(), + chain.to_string().as_str(), + token_program, + SolanaEventFilter::LogContains("transfer".to_string()), + SolanaCommitment::Confirmed, + ); + + manager.get_lookup_maps().add_trigger(trigger_a).unwrap(); + manager.get_lookup_maps().add_trigger(trigger_b).unwrap(); + manager.get_lookup_maps().add_trigger(trigger_c).unwrap(); + + { + let lock = manager + .get_lookup_maps() + .triggers_by_solana_program + .read() + .unwrap(); + // (chain, system_program) bucket has both triggers + let system_bucket = lock.get(&(chain.clone(), system_program)).unwrap(); + assert_eq!(system_bucket.len(), 2); + // (chain, token_program) bucket has the third + let token_bucket = lock.get(&(chain.clone(), token_program)).unwrap(); + assert_eq!(token_bucket.len(), 1); + } + + // Removing workflow A should leave B and C; the (chain, + // system_program) bucket should still contain one entry. + manager + .get_lookup_maps() + .remove_workflow(service_a, workflow_a) + .unwrap(); + { + let lock = manager + .get_lookup_maps() + .triggers_by_solana_program + .read() + .unwrap(); + let system_bucket = lock.get(&(chain.clone(), system_program)).unwrap(); + assert_eq!(system_bucket.len(), 1); + } + + // Removing service B should empty the (chain, system_program) + // bucket entirely (the lookup table garbage-collects empty sets). + manager.remove_service(service_b).unwrap(); + { + let lock = manager + .get_lookup_maps() + .triggers_by_solana_program + .read() + .unwrap(); + assert!(lock.get(&(chain.clone(), system_program)).is_none()); + // Token bucket is untouched. + let token_bucket = lock.get(&(chain.clone(), token_program)).unwrap(); + assert_eq!(token_bucket.len(), 1); + } +} + +/// Replay-protection regression test for the slice 3 deliverable. +/// +/// The SVM design doc requires that the same +/// `(slot, signature, instruction_index, inner_instruction_index, log_index)` +/// tuple does NOT re-fire the operator, including across deliberate +/// `solana-pubsub` reconnects. This test simulates the reconnect by +/// pushing the same `SolanaStreamLog` through the dispatcher twice: +/// the first call should produce a `DispatcherCommand::Trigger`; the +/// second call (mimicking a reconnect that replays the same notification) +/// should be silently dropped by the dedup cache. +#[test] +fn solana_program_event_replay_protection() { + use wavs::subsystems::trigger::streams::solana_stream::SolanaStreamLog; + use wavs_types::{SolanaAddress, SolanaCommitment, SolanaEventFilter}; + + let config = Config::default(); + let services = wavs::services::Services::new(WavsDb::new().unwrap()); + let (trigger_to_dispatcher_tx, _) = crossbeam::channel::unbounded::(); + let manager = TriggerManager::new( + &config, + TriggerMetrics::new(opentelemetry::global::meter("trigger-test-metrics")), + services, + trigger_to_dispatcher_tx, + ) + .unwrap(); + + // Activate the test service so the lookup table will surface it. + let chain = ChainKey::new("solana:devnet").unwrap(); + let program = SolanaAddress::from_base58("11111111111111111111111111111111").unwrap(); + + let workflow_id = WorkflowId::new("wf-replay").unwrap(); + + let trigger = wavs_types::Trigger::SolanaProgramEvent { + chain: chain.clone(), + program_id: program, + filter: SolanaEventFilter::LogContains("Program log: matched".to_string()), + commitment: SolanaCommitment::Confirmed, + }; + let workflow = Workflow { + component: Component { + source: ComponentSource::Digest(ComponentDigest::hash([0u8; 32])), + permissions: Default::default(), + fuel_limit: None, + time_limit_seconds: None, + config: Default::default(), + env_keys: Default::default(), + }, + trigger: trigger.clone(), + submit: Submit::None, + }; + let mut workflows = std::collections::BTreeMap::new(); + workflows.insert(workflow_id.clone(), workflow); + let manager_service = Service { + name: "replay-test".to_string(), + status: ServiceStatus::Active, + manager: ServiceManager::Evm { + chain: ChainKey::new("evm:anvil").unwrap(), + address: rand_address_evm(), + }, + workflows, + }; + let service_id = manager_service.id(); + manager.services.save(&manager_service).unwrap(); + manager + .get_lookup_maps() + .add_trigger(wavs_types::TriggerConfig { + service_id: service_id.clone(), + workflow_id: workflow_id.clone(), + trigger, + }) + .unwrap(); + + // The replay-identity tuple is identical between the two pushes. + let log = SolanaStreamLog { + slot: 42, + signature: "5".repeat(88), // base58-shaped placeholder + instruction_index: 0, + inner_instruction_index: None, + log_index: 3, + program_id: program, + raw_log: "Program log: matched the discriminator".to_string(), + }; + + // First observation: a single Trigger command should land. + let first = manager.handle_solana_logs(chain.clone(), 42, vec![log.clone()]); + assert_eq!( + first.len(), + 1, + "first observation should produce exactly one DispatcherCommand::Trigger; got {first:?}" + ); + + // Reconnect simulation: the exact same notification arrives again. + // The replay cache must drop it. + let second = manager.handle_solana_logs(chain.clone(), 42, vec![log.clone()]); + assert!( + second.is_empty(), + "second observation of identical replay-identity should be deduped; got {second:?}" + ); + + // A different log_index in the same transaction is a genuinely new + // event and must NOT be deduped. + let next_log = SolanaStreamLog { + log_index: 4, + ..log.clone() + }; + let next = manager.handle_solana_logs(chain.clone(), 42, vec![next_log]); + assert_eq!( + next.len(), + 1, + "distinct log_index must produce a new Trigger; got {next:?}" + ); + + // And a different signature with otherwise-identical fields is also + // a new event. + let other_tx = SolanaStreamLog { + signature: "6".repeat(88), + ..log.clone() + }; + let other = manager.handle_solana_logs(chain.clone(), 42, vec![other_tx]); + assert_eq!( + other.len(), + 1, + "distinct signature must produce a new Trigger" + ); +} diff --git a/wit-definitions/aggregator/wit/deps/wavs-types-2.7.0/package.wit b/wit-definitions/aggregator/wit/deps/wavs-types-2.7.0/package.wit index a7caabe25..d661aaae2 100644 --- a/wit-definitions/aggregator/wit/deps/wavs-types-2.7.0/package.wit +++ b/wit-definitions/aggregator/wit/deps/wavs-types-2.7.0/package.wit @@ -66,6 +66,29 @@ interface chain { ws-endpoints: list, http-endpoint: option, } + + /// A 32-byte Solana pubkey (program id, account address, PDA, etc.). + /// + /// The on-wire representation in trigger declarations is base58, but the + /// WIT-level record carries the raw 32 bytes so components don't have to + /// pull in a base58 decoder. + record solana-address { + raw-bytes: list, + } + + /// Commitment level for Solana subscriptions. + variant solana-commitment { + processed, + confirmed, + finalized, + } + + record solana-chain-config { + chain-id: string, + http-endpoint: option, + ws-endpoint: option, + commitment: solana-commitment, + } } interface core { @@ -98,7 +121,7 @@ interface core { } interface events { - use chain.{chain-key, evm-address, evm-event-log, cosmos-address, cosmos-event}; + use chain.{chain-key, evm-address, evm-event-log, cosmos-address, cosmos-event, solana-address, solana-commitment}; use core.{timestamp}; type event-id = list; @@ -144,6 +167,23 @@ interface events { data: list, } + /// Per-occurrence Solana log/event data delivered to the operator + /// component. The (slot, signature, instruction-index, + /// inner-instruction-index, log-index) tuple is the replay-identity + /// tuple from the SVM design doc. + record trigger-data-solana-program-event { + chain: chain-key, + slot: u64, + signature: string, + instruction-index: u32, + /// None for top-level instructions, Some(i) for inner-instruction at + /// index i. + inner-instruction-index: option, + log-index: u32, + program-id: solana-address, + data: list, + } + /// 20-byte unique hash variant trigger-data { evm-contract-event(trigger-data-evm-contract-event), @@ -152,13 +192,14 @@ interface events { cron(trigger-data-cron), atproto-event(trigger-data-atproto-event), hypercore-append(trigger-data-hypercore-append), + solana-program-event(trigger-data-solana-program-event), raw(list), } } interface service { use core.{digest, timestamp}; - use chain.{chain-key, evm-address, cosmos-address}; + use chain.{chain-key, evm-address, cosmos-address, solana-address, solana-commitment}; /// Basic types type service-id = string; @@ -266,6 +307,20 @@ interface service { feed-key: string, } + /// Filter for Solana program events. Either a discriminator match against + /// the raw event data, or a substring match against the log line. + variant solana-event-filter { + discriminator(list), + log-contains(string), + } + + record trigger-solana-program-event { + chain: chain-key, + program-id: solana-address, + filter: solana-event-filter, + commitment: solana-commitment, + } + /// Trigger types variant trigger { evm-contract-event(trigger-evm-contract-event), @@ -274,6 +329,7 @@ interface service { cron(trigger-cron), atproto-event(trigger-atproto-event), hypercore-append(trigger-hypercore-append), + solana-program-event(trigger-solana-program-event), manual, } diff --git a/wit-definitions/operator/wit/deps/wavs-types-2.7.0/package.wit b/wit-definitions/operator/wit/deps/wavs-types-2.7.0/package.wit index a7caabe25..d661aaae2 100644 --- a/wit-definitions/operator/wit/deps/wavs-types-2.7.0/package.wit +++ b/wit-definitions/operator/wit/deps/wavs-types-2.7.0/package.wit @@ -66,6 +66,29 @@ interface chain { ws-endpoints: list, http-endpoint: option, } + + /// A 32-byte Solana pubkey (program id, account address, PDA, etc.). + /// + /// The on-wire representation in trigger declarations is base58, but the + /// WIT-level record carries the raw 32 bytes so components don't have to + /// pull in a base58 decoder. + record solana-address { + raw-bytes: list, + } + + /// Commitment level for Solana subscriptions. + variant solana-commitment { + processed, + confirmed, + finalized, + } + + record solana-chain-config { + chain-id: string, + http-endpoint: option, + ws-endpoint: option, + commitment: solana-commitment, + } } interface core { @@ -98,7 +121,7 @@ interface core { } interface events { - use chain.{chain-key, evm-address, evm-event-log, cosmos-address, cosmos-event}; + use chain.{chain-key, evm-address, evm-event-log, cosmos-address, cosmos-event, solana-address, solana-commitment}; use core.{timestamp}; type event-id = list; @@ -144,6 +167,23 @@ interface events { data: list, } + /// Per-occurrence Solana log/event data delivered to the operator + /// component. The (slot, signature, instruction-index, + /// inner-instruction-index, log-index) tuple is the replay-identity + /// tuple from the SVM design doc. + record trigger-data-solana-program-event { + chain: chain-key, + slot: u64, + signature: string, + instruction-index: u32, + /// None for top-level instructions, Some(i) for inner-instruction at + /// index i. + inner-instruction-index: option, + log-index: u32, + program-id: solana-address, + data: list, + } + /// 20-byte unique hash variant trigger-data { evm-contract-event(trigger-data-evm-contract-event), @@ -152,13 +192,14 @@ interface events { cron(trigger-data-cron), atproto-event(trigger-data-atproto-event), hypercore-append(trigger-data-hypercore-append), + solana-program-event(trigger-data-solana-program-event), raw(list), } } interface service { use core.{digest, timestamp}; - use chain.{chain-key, evm-address, cosmos-address}; + use chain.{chain-key, evm-address, cosmos-address, solana-address, solana-commitment}; /// Basic types type service-id = string; @@ -266,6 +307,20 @@ interface service { feed-key: string, } + /// Filter for Solana program events. Either a discriminator match against + /// the raw event data, or a substring match against the log line. + variant solana-event-filter { + discriminator(list), + log-contains(string), + } + + record trigger-solana-program-event { + chain: chain-key, + program-id: solana-address, + filter: solana-event-filter, + commitment: solana-commitment, + } + /// Trigger types variant trigger { evm-contract-event(trigger-evm-contract-event), @@ -274,6 +329,7 @@ interface service { cron(trigger-cron), atproto-event(trigger-atproto-event), hypercore-append(trigger-hypercore-append), + solana-program-event(trigger-solana-program-event), manual, } diff --git a/wit-definitions/types/wit/chain.wit b/wit-definitions/types/wit/chain.wit index de0827546..cfc9bf58e 100644 --- a/wit-definitions/types/wit/chain.wit +++ b/wit-definitions/types/wit/chain.wit @@ -60,4 +60,28 @@ interface chain { ws-endpoints: list, http-endpoint: option, } + + // A 32-byte Solana pubkey (program id, account address, PDA, etc.). + // + // The on-wire representation in trigger declarations is base58, but the + // WIT-level record carries the raw 32 bytes so components don't have to + // pull in a base58 decoder. + record solana-address { + raw-bytes: list // 32 bytes + } + + // Commitment level for Solana subscriptions, mirroring the standard + // processed / confirmed / finalized levels exposed by Solana RPC. + variant solana-commitment { + processed, + confirmed, + finalized, + } + + record solana-chain-config { + chain-id: string, + http-endpoint: option, + ws-endpoint: option, + commitment: solana-commitment, + } } diff --git a/wit-definitions/types/wit/events.wit b/wit-definitions/types/wit/events.wit index 38f8f694b..7632a69f0 100644 --- a/wit-definitions/types/wit/events.wit +++ b/wit-definitions/types/wit/events.wit @@ -1,5 +1,5 @@ interface events { - use chain.{chain-key, evm-address, evm-event-log, cosmos-address, cosmos-event}; + use chain.{chain-key, evm-address, evm-event-log, cosmos-address, cosmos-event, solana-address, solana-commitment}; use core.{timestamp}; type event-id = list; // 20-byte unique hash @@ -11,6 +11,7 @@ interface events { cron(trigger-data-cron), atproto-event(trigger-data-atproto-event), hypercore-append(trigger-data-hypercore-append), + solana-program-event(trigger-data-solana-program-event), raw(list) } @@ -54,4 +55,21 @@ interface events { index: u64, data: list } + + // Per-occurrence Solana log/event data delivered to the operator + // component. The (slot, signature, instruction-index, + // inner-instruction-index, log-index) tuple is the replay-identity tuple + // from the SVM design doc. + record trigger-data-solana-program-event { + chain: chain-key, + slot: u64, + signature: string, + instruction-index: u32, + // None for top-level instructions, Some(i) for inner-instruction at + // index i. + inner-instruction-index: option, + log-index: u32, + program-id: solana-address, + data: list + } } diff --git a/wit-definitions/types/wit/service.wit b/wit-definitions/types/wit/service.wit index 45df31703..47440fc0e 100644 --- a/wit-definitions/types/wit/service.wit +++ b/wit-definitions/types/wit/service.wit @@ -6,7 +6,7 @@ interface service { type semver-version = string; use core.{digest, timestamp}; - use chain.{chain-key, evm-address, cosmos-address}; + use chain.{chain-key, evm-address, cosmos-address, solana-address, solana-commitment}; // Service types record service { @@ -94,6 +94,7 @@ interface service { cron(trigger-cron), atproto-event(trigger-atproto-event), hypercore-append(trigger-hypercore-append), + solana-program-event(trigger-solana-program-event), manual } @@ -132,6 +133,20 @@ interface service { feed-key: string } + // Filter for Solana program events. Either a discriminator match against + // the raw event data, or a substring match against the log line. + variant solana-event-filter { + discriminator(list), + log-contains(string), + } + + record trigger-solana-program-event { + chain: chain-key, + program-id: solana-address, + filter: solana-event-filter, + commitment: solana-commitment, + } + // Submit types variant submit {