From 6ce43ee0a664b2dd15bd7fe4573d367220a6f2ca Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Mon, 13 Jul 2026 03:10:34 -0400 Subject: [PATCH 1/8] build: use local quicknode-sdk with payment features for development Points the SDK dependency at the local checkout's crypto-micropayment branch and enables the payments, payments-svm, and payments-tempo features. Dev-only: the release swaps back to a published crates.io version once the SDK ships this feature. --- Cargo.lock | 2528 ++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 9 +- 2 files changed, 2453 insertions(+), 84 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2b8a411..98fbe70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,290 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloy-consensus" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ff0c4adba2abdcd9fb5829ae5f4394c06f8585ed283a9ba79aa33763c802e1" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-trie", + "alloy-tx-macros", + "auto_impl", + "borsh", + "c-kzg", + "derive_more", + "either", + "k256 0.13.4", + "once_cell", + "secp256k1 0.30.0", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-eip2124" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "crc", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-eip2930" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "serde", +] + +[[package]] +name = "alloy-eip7702" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "k256 0.13.4", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-eip7928" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b12337f74cbfa451cb04dac173974814a6ff463079e1793aa09600ba8813ab" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "once_cell", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-eips" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4c0453065b9206acc0f869a258dc8dcbbd595e144b4446f2c493a24a814d1f" +dependencies = [ + "alloy-eip2124", + "alloy-eip2930", + "alloy-eip7702", + "alloy-eip7928", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "auto_impl", + "borsh", + "c-kzg", + "derive_more", + "either", + "serde", + "serde_with", + "sha2 0.10.9", +] + +[[package]] +name = "alloy-json-abi" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c36c9d7f9021601b04bfef14a4b64849f6d73116a4e91e071d7fbfe10247901" +dependencies = [ + "alloy-primitives", + "alloy-sol-type-parser", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-primitives" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4885c1409b6936c4898e646ef58baf6ec54edaf6d8179f79df805a7b85b7cf3e" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "foldhash 0.2.0", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "itoa", + "k256 0.13.4", + "keccak-asm", + "paste", + "proptest", + "rand 0.9.4", + "rapidhash", + "ruint", + "rustc-hash", + "secp256k1 0.31.1", + "serde", + "sha3 0.11.0", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24671b1f62edcf0f9b62994c7bf72cd621a04a4b99f5020ece1a647b40e2f103" +dependencies = [ + "alloy-rlp-derive", + "arrayvec", + "bytes", +] + +[[package]] +name = "alloy-rlp-derive" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d4311c03125e8a18296504560b9de3d75ecbd0dcda7f71e6cf2a196d57e6fba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "alloy-serde" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e97b3e0b9f816b25083045dcfa69431bd059a078e828e4d82d296d1949b96c" +dependencies = [ + "alloy-primitives", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-sol-macro" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840128ed2b2971d6d4668a553fe403a82683d3acc646c73e75887e7157408033" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63ec265e5d65d725175f6ca7711c970824c90ef9c0d1f1973711d4150ee612dd" +dependencies = [ + "alloy-json-abi", + "alloy-sol-macro-input", + "const-hex", + "heck", + "indexmap 2.14.0", + "proc-macro-error2", + "proc-macro2", + "quote", + "sha3 0.11.0", + "syn 2.0.117", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-macro-input" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89bf01077f18650876cfa682eb1f949967b5cde03f1a51c955c469d2c9b4aa67" +dependencies = [ + "alloy-json-abi", + "const-hex", + "dunce", + "heck", + "macro-string", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.117", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-type-parser" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "857b470ecdd2ed38beaf82ad1a38c516a8ff75266750f38b9eeed001d575241b" +dependencies = [ + "serde", + "winnow 1.0.3", +] + +[[package]] +name = "alloy-sol-types" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384cf252de0db2dec52821eac037a7f57e2aa33fe5b900ce6fe39973402341f1" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-macro", +] + +[[package]] +name = "alloy-trie" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f14b5d9b2c2173980202c6ff470d96e7c5e202c65a9f67884ad565226df7fbb" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "derive_more", + "nybbles", + "serde", + "smallvec", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "alloy-tx-macros" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406bc1183f6843e0aba09f7b3365e828b597213d60793ba5cb41befc863e3a78" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "1.0.0" @@ -67,12 +351,281 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" +dependencies = [ + "ark-ff-asm 0.6.0", + "ark-ff-macros 0.6.0", + "ark-serialize 0.6.0", + "ark-std 0.6.0", + "digest 0.10.7", + "educe", + "num-bigint", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-ff-asm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint", + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-ff-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" +dependencies = [ + "ark-serialize-derive", + "ark-std 0.6.0", + "digest 0.10.7", + "num-bigint", + "serde_with", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand 0.8.7", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.7", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.7", +] + +[[package]] +name = "ark-std" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" +dependencies = [ + "num-traits", + "rand 0.8.7", +] + [[package]] name = "arraydeque" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -106,7 +659,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -115,6 +668,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -143,12 +707,82 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2d6094e2a1ba3c93b5a596fe5a10d1a10c3c6e06785cde89f693a044c01aa40" +dependencies = [ + "bitcoin-internals", +] + +[[package]] +name = "bitcoin-internals" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a30a22d1f112dde8e16be7b45c63645dc165cef254f835b3e1e9553e485cfa64" +dependencies = [ + "hex-conservative 0.3.2", +] + +[[package]] +name = "bitcoin-io" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +dependencies = [ + "bitcoin-io", + "hex-conservative 0.2.2", +] + [[package]] name = "bitflags" version = "2.11.1" @@ -158,6 +792,27 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -167,6 +822,60 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "borsh" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bstr" version = "1.12.1" @@ -184,11 +893,41 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "c-kzg" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d04308254695569fdb9bfe3bacc1c91837a670d0806605eb82d63748fbd3a6" +dependencies = [ + "blst", + "cc", + "glob", + "hex", + "libc", + "once_cell", + "serde", +] [[package]] name = "cc" @@ -214,6 +953,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "clap" version = "4.6.1" @@ -255,7 +1006,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -273,6 +1024,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "colorchoice" version = "1.0.5" @@ -307,7 +1064,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f316c6237b2d38be61949ecd15268a4c6ca32570079394a2444d9ce2c72a72d8" dependencies = [ "async-trait", - "convert_case", + "convert_case 0.6.0", "json5", "pathdiff", "ron", @@ -344,6 +1101,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "const-hex" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "proptest", + "serde_core", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-random" version = "0.1.18" @@ -364,6 +1145,27 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "convert_case" version = "0.6.0" @@ -373,6 +1175,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -389,6 +1200,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -398,6 +1215,30 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crossterm" version = "0.29.0" @@ -427,6 +1268,34 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "getrandom 0.4.2", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -437,6 +1306,101 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.2", + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version 0.4.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", + "zeroize", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + [[package]] name = "deadpool" version = "0.12.3" @@ -455,6 +1419,26 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid 0.10.2", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -462,6 +1446,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.117", + "unicode-xid", ] [[package]] @@ -483,14 +1502,37 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -501,7 +1543,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -529,18 +1571,167 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] -name = "encode_unicode" -version = "1.0.0" +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.10", + "digest 0.10.7", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", +] + +[[package]] +name = "ecdsa" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" +dependencies = [ + "der 0.8.1", + "digest 0.11.3", + "elliptic-curve 0.14.1", + "rfc6979 0.6.0", + "signature 3.0.0", + "spki 0.8.0", + "zeroize", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.2.0", +] + +[[package]] +name = "ed25519-consensus" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +dependencies = [ + "curve25519-dalek-ng", + "hex", + "rand_core 0.6.4", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.5", + "digest 0.10.7", + "ff 0.13.1", + "generic-array", + "group 0.13.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.3", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct 1.0.0", + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "digest 0.11.3", + "ff 0.14.0", + "group 0.14.0", + "hybrid-array", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sec1 0.8.1", + "subtle", + "zeroize", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-ordinalize" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +checksum = "07f808d588c10e464ea6f7d3eaed500049eff30aaac103460f61828c2d65b3eb" +dependencies = [ + "enum-ordinalize-derive", +] [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "enum-ordinalize-derive" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "42e528e2d34ba8a67a1a650b86beae8ef69fc5fdb638016f386b973226590432" dependencies = [ - "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -576,12 +1767,72 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "fastrlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.7", + "rustc-hex", + "static_assertions", +] + [[package]] name = "float-cmp" version = "0.10.0" @@ -624,6 +1875,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.32" @@ -680,7 +1937,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -720,6 +1977,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -758,10 +2016,39 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "h2" version = "0.4.14" @@ -774,13 +2061,19 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -810,6 +2103,11 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", + "serde", + "serde_core", +] [[package]] name = "hashlink" @@ -832,6 +2130,48 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-conservative" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830e599c2904b08f0834ee6337d8fe8f0ed4a63b5d9e7a7f49c0ffa06d08d360" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "http" version = "1.4.1" @@ -883,6 +2223,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + [[package]] name = "hyper" version = "1.10.0" @@ -943,6 +2294,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -1031,6 +2406,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1052,6 +2433,37 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -1089,6 +2501,24 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1120,9 +2550,9 @@ checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ "proc-macro2", "quote", - "rustc_version", + "rustc_version 0.4.1", "simd_cesu8", - "syn", + "syn 2.0.117", ] [[package]] @@ -1141,7 +2571,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1177,6 +2607,78 @@ dependencies = [ "serde", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "once_cell", + "sha2 0.10.9", +] + +[[package]] +name = "k256" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f50113171a713f4a4231ef82eb26703607139b35dcb56241f0ceab2ae1f7d8" +dependencies = [ + "cpubits", + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", + "primeorder 0.14.0", + "sha2 0.11.0", + "signature 3.0.0", + "wnaf", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "keccak-asm" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5dc2c0d691cbf7595cde551ced329cca99c2387c2cbc97754c5d0cd045d3ee" +dependencies = [ + "digest 0.10.7", + "sha3-asm", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "lazy_static" version = "1.5.0" @@ -1195,6 +2697,12 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libyml" version = "0.0.5" @@ -1244,6 +2752,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "macro-string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "memchr" version = "2.8.1" @@ -1267,12 +2786,31 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1280,6 +2818,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1292,6 +2831,20 @@ dependencies = [ "libc", ] +[[package]] +name = "nybbles" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d49ff0c0d00d4a502b39df9af3a525e1efeb14b9dabb5bb83335284c1309210" +dependencies = [ + "alloy-rlp", + "cfg-if", + "proptest", + "ruint", + "serde", + "smallvec", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1304,20 +2857,66 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", + "sha2 0.10.9", +] + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] [[package]] -name = "ordered-multimap" -version = "0.7.3" +name = "parity-scale-codec-derive" +version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" dependencies = [ - "dlv-list", - "hashbrown 0.14.5", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -1343,6 +2942,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.3" @@ -1385,7 +2990,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1395,7 +3000,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -1404,6 +3009,26 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.1", + "spki 0.8.0", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -1465,7 +3090,85 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve 0.13.8", +] + +[[package]] +name = "primeorder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve 0.14.1", + "once_cell", + "primefield", + "serdect", + "wnaf", +] + +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -1477,6 +3180,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quicknode-cli" version = "0.5.0" @@ -1496,7 +3224,7 @@ dependencies = [ "serde", "serde_json", "serde_yml", - "sha2", + "sha2 0.10.9", "tempfile", "thiserror 1.0.69", "time", @@ -1510,14 +3238,24 @@ dependencies = [ [[package]] name = "quicknode-sdk" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d7fd1e431f62f6f82adca349756b31a07e082446c0664a81530e4a791c62ca" dependencies = [ + "alloy-consensus", + "alloy-primitives", + "alloy-rlp", + "base64", + "bs58", "config", + "ed25519-dalek", + "hex", + "k256 0.14.0", + "rand 0.8.7", "reqwest 0.13.4", "secrecy", "serde", "serde_json", + "sha2 0.10.9", + "sha3 0.10.9", + "tempo-primitives", "thiserror 1.0.69", "tokio", "url", @@ -1553,7 +3291,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand", + "rand 0.9.4", "ring", "rustc-hash", "rustls", @@ -1600,14 +3338,42 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.5", + "serde", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -1617,7 +3383,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -1627,6 +3402,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", + "serde", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rapidhash" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e" +dependencies = [ + "rustversion", ] [[package]] @@ -1638,6 +3438,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "regex" version = "1.12.3" @@ -1738,6 +3558,26 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint 0.7.5", + "hmac 0.13.0", +] + [[package]] name = "ring" version = "0.17.14" @@ -1752,6 +3592,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + [[package]] name = "ron" version = "0.12.1" @@ -1766,6 +3616,41 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ruint" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45caf26f647c19115bf9c453c70ffe4a4a3a6390dceebd942610584f99b8ddce" +dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "ark-ff 0.5.0", + "ark-ff 0.6.0", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types", + "proptest", + "rand 0.8.7", + "rand 0.9.4", + "rlp", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + [[package]] name = "rust-ini" version = "0.21.3" @@ -1782,13 +3667,28 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver", + "semver 1.0.28", ] [[package]] @@ -1873,47 +3773,151 @@ version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.10", + "generic-array", + "pkcs8 0.10.2", + "subtle", + "zeroize", ] [[package]] -name = "rustversion" -version = "1.0.22" +name = "sec1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct 1.0.0", + "ctutils", + "der 0.8.1", + "hybrid-array", + "subtle", + "zeroize", +] [[package]] -name = "ryu" -version = "1.0.23" +name = "secp256k1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.7", + "secp256k1-sys 0.10.1", +] [[package]] -name = "same-file" -version = "1.0.6" +name = "secp256k1" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" dependencies = [ - "winapi-util", + "bitcoin_hashes", + "rand 0.9.4", + "secp256k1-sys 0.11.0", ] [[package]] -name = "schannel" -version = "0.1.29" +name = "secp256k1-sys" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" dependencies = [ - "windows-sys 0.61.2", + "cc", ] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "secp256k1-sys" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" +dependencies = [ + "cc", +] [[package]] name = "secrecy" @@ -1947,12 +3951,30 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + [[package]] name = "serde" version = "1.0.228" @@ -1992,7 +4014,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2001,7 +4023,7 @@ version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ - "indexmap", + "indexmap 2.14.0", "itoa", "memchr", "serde", @@ -2039,13 +4061,45 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_yml" version = "0.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" dependencies = [ - "indexmap", + "indexmap 2.14.0", "itoa", "libyml", "memchr", @@ -2054,6 +4108,29 @@ dependencies = [ "version_check", ] +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct 1.0.0", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.9" @@ -2061,8 +4138,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak 0.1.6", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", +] + +[[package]] +name = "sha3-asm" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6287fd675f713484342a89cbf0a386abef5f15919cfad607e5e1f19e1e15331" +dependencies = [ + "cc", + "cfg-if", ] [[package]] @@ -2077,13 +4195,33 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + [[package]] name = "simd_cesu8" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" dependencies = [ - "rustc_version", + "rustc_version 0.4.1", "simdutf8", ] @@ -2121,12 +4259,38 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.1", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -2139,6 +4303,23 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -2150,6 +4331,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn-solidity" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec005042c7d952febc1a3ef5b0f6674e9054aa836877a31c90b20e25b3d31744" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -2167,9 +4360,15 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.27.0" @@ -2183,6 +4382,37 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tempo-contracts" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a163690a85ff88c8cb98b42490fa95a33b114d6d90c653b7be2c9f794f7b2ab9" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", +] + +[[package]] +name = "tempo-primitives" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63f9919c82a4c68531609cd015994e1a81c48d4c84eeaa92a9d86651a0e6ec41" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "base64", + "derive_more", + "ed25519-consensus", + "once_cell", + "p256", + "serde", + "serde_json", + "sha2 0.10.9", + "tempo-contracts", +] + [[package]] name = "terminal_size" version = "0.4.4" @@ -2225,7 +4455,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2236,7 +4466,16 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", ] [[package]] @@ -2327,7 +4566,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2362,7 +4601,7 @@ dependencies = [ "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", - "toml_edit", + "toml_edit 0.22.27", ] [[package]] @@ -2402,7 +4641,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -2410,6 +4649,18 @@ dependencies = [ "winnow 0.7.15", ] +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + [[package]] name = "toml_parser" version = "1.1.2+spec-1.1.0" @@ -2431,7 +4682,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f89570c1a68d73941f728cca32a4345b2ffca36667ad921af336c60309a3e7e" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde", "serde_json", "thiserror 2.0.18", @@ -2525,6 +4776,24 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -2579,6 +4848,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" @@ -2679,7 +4954,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -2709,7 +4984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -2722,8 +4997,8 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap", - "semver", + "indexmap 2.14.0", + "semver 1.0.28", ] [[package]] @@ -2786,12 +5061,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2958,9 +5286,9 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap", + "indexmap 2.14.0", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -2976,7 +5304,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -2989,7 +5317,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -3008,9 +5336,9 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.14.0", "log", - "semver", + "semver 1.0.28", "serde", "serde_derive", "serde_json", @@ -3018,12 +5346,32 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wnaf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff 0.14.0", + "group 0.14.0", + "hybrid-array", +] + [[package]] name = "writeable" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yaml-rust2" version = "0.11.0" @@ -3054,7 +5402,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -3075,7 +5423,7 @@ checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3095,7 +5443,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -3104,6 +5452,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "zerotrie" @@ -3135,7 +5497,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5738ac6..25a9589 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,14 @@ name = "qn" path = "src/lib.rs" [dependencies] -quicknode-sdk = "0.7" +# DEV ONLY: path dep on the SDK's unreleased crypto-micropayment branch. +# Swap back to a published crates.io version (with the same features) before +# any release tag — the release is gated on the SDK publishing this feature. +quicknode-sdk = { path = "../sdk/crates/core", features = [ + "payments", # x402/EVM + "payments-svm", # + x402/Solana + "payments-tempo", # + MPP/Tempo +] } clap = { version = "4", features = ["derive", "env", "wrap_help"] } clap_complete = "4" tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } From 688022b594b07ea3c72db3192b3f4ec7058c87af Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Mon, 13 Jul 2026 03:12:11 -0400 Subject: [PATCH 2/8] feat(config): add [rpc.payment] section for the paid RPC lane Parameter defaults (key_file, max_amount, pay_network, asset, svm_rpc_url) for qn rpc call --x402/--mpp. The section only supplies values; activation stays with the per-invocation scheme flag. The raw private key never lives in config.toml: key_file points at a file, and an inline key = ... is captured by a trap field so payment resolution can reject it with an actionable error instead of serde silently ignoring it. max_amount accepts a TOML string or integer. 7 new tests. --- src/config.rs | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/src/config.rs b/src/config.rs index 2e2dc45..d9de3b2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -64,6 +64,62 @@ pub struct ApiSection { pub struct RpcSection { #[serde(default)] pub endpoint_url: Option, + #[serde(default)] + pub payment: PaymentSection, +} + +/// `[rpc.payment]` section: parameter defaults for the crypto-micropayment +/// lane of `qn rpc call` (`--x402`/`--mpp`). This section supplies values; +/// it never activates payment — only the per-invocation scheme flag does. +/// Per-call flags override every field here. +/// +/// There is deliberately no `scheme` key (the flag is the scheme) and no way +/// to store the raw private key: `key_file` points at a file holding the key, +/// keeping the key itself out of the most commonly shared/pasted file we own. +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct PaymentSection { + /// Path to a file containing the raw payment private key (EVM/Tempo hex, + /// Solana base58). Never the key itself. + #[serde(default)] + pub key_file: Option, + /// Trap field: an inline raw key is rejected at payment-resolution time + /// with an error pointing at `key_file`. Without this field serde would + /// silently ignore `key = "..."` and the user would think it took effect. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub key: Option, + /// Default spend ceiling per paid call, in integer base units of `asset`. + /// Accepts a TOML string or integer. + #[serde(default, deserialize_with = "de_opt_string_or_int")] + pub max_amount: Option, + /// CAIP-2 id of the chain payments settle on, e.g. `eip155:84532`. + /// Independent of `--network` (the chain the RPC call queries). + #[serde(default)] + pub pay_network: Option, + /// Token to pay with: EVM contract address or Solana mint. + #[serde(default)] + pub asset: Option, + /// Explicit Solana RPC URL for x402/Solana payment builds. + #[serde(default)] + pub svm_rpc_url: Option, +} + +/// Deserializes an optional TOML string-or-integer into `Option`, so +/// `max_amount = 10000` and `max_amount = "10000"` both work. (Base units can +/// exceed i64 for high-decimal assets, hence the canonical string form.) +fn de_opt_string_or_int<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let v = Option::::deserialize(deserializer)?; + match v { + None => Ok(None), + Some(toml::Value::String(s)) => Ok(Some(s)), + Some(toml::Value::Integer(i)) => Ok(Some(i.to_string())), + Some(other) => Err(serde::de::Error::custom(format!( + "expected a string or integer, got {}", + other.type_str() + ))), + } } #[derive(Debug, Default, Serialize, Deserialize)] @@ -728,6 +784,89 @@ mod tests { assert_eq!(cfg.rpc.endpoint_url.as_deref(), Some("https://x/rpc")); } + #[test] + fn rpc_payment_section_round_trips_through_config_file() { + let dir = tempdir().unwrap(); + let path = dir.path().join("config.toml"); + std::fs::write( + &path, + "[rpc.payment]\n\ + key_file = \"/keys/payer.key\"\n\ + max_amount = \"10000\"\n\ + pay_network = \"eip155:84532\"\n\ + asset = \"0xabc\"\n\ + svm_rpc_url = \"https://solana.example/rpc\"\n", + ) + .unwrap(); + let cfg = load_from(&path).unwrap().unwrap(); + let p = &cfg.rpc.payment; + assert_eq!(p.key_file.as_deref(), Some(Path::new("/keys/payer.key"))); + assert_eq!(p.max_amount.as_deref(), Some("10000")); + assert_eq!(p.pay_network.as_deref(), Some("eip155:84532")); + assert_eq!(p.asset.as_deref(), Some("0xabc")); + assert_eq!(p.svm_rpc_url.as_deref(), Some("https://solana.example/rpc")); + assert!(p.key.is_none()); + } + + #[test] + fn rpc_payment_section_is_optional() { + let dir = tempdir().unwrap(); + let path = dir.path().join("config.toml"); + std::fs::write(&path, "[rpc]\nendpoint_url = \"https://x/rpc\"\n").unwrap(); + let cfg = load_from(&path).unwrap().unwrap(); + assert!(cfg.rpc.payment.key_file.is_none()); + assert!(cfg.rpc.payment.max_amount.is_none()); + } + + #[test] + fn rpc_payment_max_amount_accepts_toml_integer() { + let dir = tempdir().unwrap(); + let path = dir.path().join("config.toml"); + std::fs::write(&path, "[rpc.payment]\nmax_amount = 10000\n").unwrap(); + let cfg = load_from(&path).unwrap().unwrap(); + assert_eq!(cfg.rpc.payment.max_amount.as_deref(), Some("10000")); + } + + #[test] + fn rpc_payment_max_amount_rejects_other_types() { + let dir = tempdir().unwrap(); + let path = dir.path().join("config.toml"); + std::fs::write(&path, "[rpc.payment]\nmax_amount = 1.5\n").unwrap(); + let err = load_from(&path).unwrap_err(); + assert!(matches!(err, CliError::BadConfig { .. }), "got: {err:?}"); + } + + #[test] + fn rpc_payment_inline_key_parses_into_trap_field() { + // An inline raw key must not break config parsing (unrelated commands + // still run); the payment lane rejects it with an actionable error at + // resolution time instead. + let dir = tempdir().unwrap(); + let path = dir.path().join("config.toml"); + std::fs::write(&path, "[rpc.payment]\nkey = \"0xdeadbeef\"\n").unwrap(); + let cfg = load_from(&path).unwrap().unwrap(); + assert!(cfg.rpc.payment.key.is_some()); + } + + #[test] + fn save_api_key_preserves_rpc_payment_section() { + let dir = tempdir().unwrap(); + let path = dir.path().join("config.toml"); + std::fs::write( + &path, + "[rpc.payment]\nkey_file = \"/keys/payer.key\"\nmax_amount = \"10000\"\n", + ) + .unwrap(); + save_api_key(&path, "new-key").unwrap(); + let cfg = load_from(&path).unwrap().unwrap(); + assert_eq!(cfg.api.key.as_deref(), Some("new-key")); + assert_eq!( + cfg.rpc.payment.key_file.as_deref(), + Some(Path::new("/keys/payer.key")) + ); + assert_eq!(cfg.rpc.payment.max_amount.as_deref(), Some("10000")); + } + #[test] fn output_section_is_optional() { let dir = tempdir().unwrap(); From bc01c72f1745f2f92c51922cb5d28e5f36316c34 Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Mon, 13 Jul 2026 03:14:23 -0400 Subject: [PATCH 3/8] feat(errors): map payment errors to exit codes with wallet-check guidance PaymentUnsupported and PaymentRejected map to exit 2 (the gateway refused); PaymentIndeterminate and the new PaymentMaybeCharged wrapper map to exit 3 (request sent, outcome unknown), so scripts can distinguish safe-to-retry from check-your-wallet-first. Render arms state whether anything was charged and warn against blind re-runs; gateway bodies appear only under --verbose. 6 new tests. --- src/errors.rs | 130 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 2 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 66af44d..b8bd29a 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -40,6 +40,16 @@ pub enum CliError { #[error(transparent)] Sdk(#[from] SdkError), + /// A paid RPC call failed in a way where the payment may already have + /// settled (e.g. the gateway's post-payment response could not be + /// interpreted). Kept separate from `Sdk` so it maps to exit 3 and renders + /// the check-your-wallet guidance; the paid lane must never auto-retry it. + #[error( + "the paid request's outcome is unknown — the payment may have been settled; \ + check your wallet before retrying" + )] + PaymentMaybeCharged(#[source] SdkError), + #[error(transparent)] Io(#[from] std::io::Error), @@ -55,17 +65,23 @@ pub enum CliError { /// - 0: success (never produced here) /// - 1: generic CLI failure (arg parse, IO, decode). clap usage errors are /// mapped to 1 in main.rs too, so 2 always and only means an API error. -/// - 2: SdkError::Api (server returned a non-2xx) -/// - 3: SdkError::Http (network failure) +/// - 2: SdkError::Api (server returned a non-2xx); also a payment the gateway +/// refused or never matched (PaymentRejected / PaymentUnsupported) +/// - 3: SdkError::Http (network failure); also an unknown payment outcome +/// (PaymentIndeterminate / PaymentMaybeCharged — request sent, response +/// lost, the caller may have been charged) /// - 4: NoApiKey / BadConfig /// - 5: user cancelled or needs --yes pub fn exit_code_for(err: &CliError) -> i32 { match err { CliError::NoApiKey | CliError::BadConfig { .. } | CliError::ConfigWrite { .. } => 4, CliError::Cancelled | CliError::NeedsConfirmation => 5, + CliError::PaymentMaybeCharged(_) => 3, CliError::Sdk(sdk) => match sdk { SdkError::Api { .. } => 2, SdkError::Http(_) => 3, + SdkError::PaymentUnsupported { .. } | SdkError::PaymentRejected { .. } => 2, + SdkError::PaymentIndeterminate => 3, _ => 1, }, _ => 1, @@ -85,6 +101,40 @@ pub fn render(err: &CliError, verbose: bool) -> String { /// Like [`render`] but uses the supplied argv values for did-you-mean lookup. pub fn render_with_argv(err: &CliError, verbose: bool, argv: &[String]) -> String { match err { + CliError::Sdk(SdkError::PaymentUnsupported { offered }) => { + format!( + "Error: no offered payment option matched your configuration \ + (check --pay-network, --asset, and --max-amount). Nothing was charged.\n\ + Gateway offered: {offered}" + ) + } + CliError::Sdk(SdkError::PaymentRejected { status, body }) => { + let msg = format!( + "Error: the gateway rejected the submitted payment (HTTP {status}). \ + A signed payment was sent — check your wallet before retrying." + ); + if verbose && !body.is_empty() { + format!("{msg}\n{body}") + } else { + msg + } + } + CliError::Sdk(SdkError::PaymentIndeterminate) => { + "Error: the paid request was sent but its response was lost — the request \ + may have been settled; check your wallet before retrying. Do not blindly \ + re-run this command." + .to_string() + } + CliError::PaymentMaybeCharged(source) => { + let msg = "Error: the paid request returned an unexpected response — the \ + request may have been settled; check your wallet before retrying. \ + Do not blindly re-run this command."; + if verbose { + format!("{msg}\n{source}") + } else { + format!("{msg} Re-run with --verbose for the response detail.") + } + } CliError::Sdk(SdkError::Api { status, body }) => { render_api_error(status.as_u16(), body, verbose, argv) } @@ -464,6 +514,82 @@ mod tests { assert_eq!(exit_code_for(&CliError::Cancelled), 5); } + // ---- payment errors ---- + + fn decode_err() -> SdkError { + SdkError::Decode { + source: serde_json::from_str::("not json").unwrap_err(), + body: "gateway oops".to_string(), + } + } + + #[test] + fn exit_code_payment_refusals_are_2() { + // The gateway said no and nothing settled (unsupported) or the refusal + // is terminal (rejected): the API-error bucket. + let unsupported = CliError::Sdk(SdkError::PaymentUnsupported { + offered: "eip155:84532/0xabc amount 999999".to_string(), + }); + let rejected = CliError::Sdk(SdkError::PaymentRejected { + status: 402, + body: "invalid signature".to_string(), + }); + assert_eq!(exit_code_for(&unsupported), 2); + assert_eq!(exit_code_for(&rejected), 2); + } + + #[test] + fn exit_code_unknown_payment_outcome_is_3() { + // Request sent, outcome unknown: the transport-ambiguity bucket, so + // scripts can distinguish "safe to retry" (2) from "check wallet" (3). + let indeterminate = CliError::Sdk(SdkError::PaymentIndeterminate); + let maybe_charged = CliError::PaymentMaybeCharged(decode_err()); + assert_eq!(exit_code_for(&indeterminate), 3); + assert_eq!(exit_code_for(&maybe_charged), 3); + } + + #[test] + fn renders_payment_unsupported_as_not_charged() { + let err = CliError::Sdk(SdkError::PaymentUnsupported { + offered: "eip155:84532/0xabc amount 999999".to_string(), + }); + let msg = render(&err, false); + assert!(msg.contains("Nothing was charged"), "got: {msg}"); + assert!(msg.contains("999999"), "got: {msg}"); + assert!(msg.contains("--max-amount"), "got: {msg}"); + } + + #[test] + fn renders_payment_rejected_with_wallet_warning() { + let err = CliError::Sdk(SdkError::PaymentRejected { + status: 402, + body: "invalid signature".to_string(), + }); + let msg = render(&err, false); + assert!(msg.contains("402"), "got: {msg}"); + assert!(msg.contains("check your wallet"), "got: {msg}"); + assert!(!msg.contains("invalid signature"), "got: {msg}"); + let verbose = render(&err, true); + assert!(verbose.contains("invalid signature"), "got: {verbose}"); + } + + #[test] + fn renders_payment_indeterminate_as_possibly_settled() { + let msg = render(&CliError::Sdk(SdkError::PaymentIndeterminate), false); + assert!(msg.contains("may have been settled"), "got: {msg}"); + assert!(msg.contains("check your wallet"), "got: {msg}"); + } + + #[test] + fn renders_payment_maybe_charged_with_source_when_verbose() { + let err = CliError::PaymentMaybeCharged(decode_err()); + let msg = render(&err, false); + assert!(msg.contains("may have been settled"), "got: {msg}"); + assert!(!msg.contains("gateway oops"), "got: {msg}"); + let verbose = render(&err, true); + assert!(verbose.contains("gateway oops"), "got: {verbose}"); + } + #[test] fn renders_401_as_unauthorized() { let msg = render(&api_err(401), false); From d1627efb4683c77cc38353a9296589ed7554473c Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Mon, 13 Jul 2026 03:15:47 -0400 Subject: [PATCH 4/8] feat(context): add keyless Ctx constructor for the paid RPC lane Ctx::from_global_keyless_payment builds a keyless SDK carrying only the payment config, so paid calls work with no API key configured. It skips API-key resolution, the token seed, [rpc] endpoint_url, and --base-url sub-client overrides (the paid lane's test hook is PaymentConfig.base_url_override). The User-Agent install is factored into a helper shared with the keyed path. --- src/context.rs | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/context.rs b/src/context.rs index 4d460c7..cdf8ea2 100644 --- a/src/context.rs +++ b/src/context.rs @@ -114,13 +114,20 @@ pub fn user_agent() -> String { /// SDK's supported way to replace its auto-generated `User-Agent`. pub fn sdk_config(api_key: String) -> SdkFullConfig { let mut full = SdkFullConfig::from_api_key(api_key); + apply_user_agent(&mut full); + full +} + +/// Installs the CLI `User-Agent` header on `full`. Shared by the keyed +/// ([`sdk_config`]) and keyless ([`Ctx::from_global_keyless_payment`]) +/// construction paths. +fn apply_user_agent(full: &mut SdkFullConfig) { let mut headers = std::collections::HashMap::new(); headers.insert("User-Agent".to_string(), user_agent()); full.http = Some(HttpConfig { headers: Some(headers), ..Default::default() }); - full } /// Points every sub-client at a custom host, suffixing each with its own base @@ -187,6 +194,46 @@ impl Ctx { Self::build(global, seed, config_endpoint_url) } + /// Keyless construction for the crypto-micropayment lane of `qn rpc call` + /// (`--x402`/`--mpp`): no API key is resolved or required, so it works on + /// a machine that has never run `qn auth login`. Only the RPC payment lane + /// is usable — every keyed sub-client would 401. + /// + /// Deliberately NOT applied here: + /// - the token cache seed and `[rpc] endpoint_url` (either would conflict + /// with the payment lane — the SDK rejects a custom URL + payment); + /// - `--base-url` sub-client overrides (the paid lane's test hook rides in + /// `PaymentConfig.base_url_override`, set by the caller; no control-plane + /// sub-client is used). + pub fn from_global_keyless_payment( + global: GlobalArgs, + payment: quicknode_sdk::PaymentConfig, + ) -> Result { + let stdout_is_tty = std::io::stdout().is_terminal(); + let (format, wide) = global.resolve_output(stdout_is_tty); + + let mut full = SdkFullConfig::keyless(); + apply_user_agent(&mut full); + full.rpc = Some(RpcConfig { + payment: Some(payment), + ..Default::default() + }); + + let sdk = QuicknodeSdk::new(&full)?; + let out = OutputCtx::detect_with( + format, + global.no_color, + global.quiet, + global.verbose, + wide, + stdout_is_tty, + std::env::var_os("NO_COLOR"), + std::env::var("TERM").ok(), + ); + + Ok(Self { sdk, out, global }) + } + fn build( global: GlobalArgs, rpc_seed: Option, From c2aeb02b39bba0e762136006b8d2c9a1f1fe38a0 Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Mon, 13 Jul 2026 03:20:38 -0400 Subject: [PATCH 5/8] feat(rpc): crypto-micropayment lane for qn rpc call (--x402/--mpp) Adds per-request stablecoin payment to qn rpc call via the SDK's 402 -> sign -> resend handshake. Activation is explicit and per invocation: --x402 or --mpp both turns the lane on and selects the scheme; [rpc.payment] config only supplies parameter defaults. The lane lives in src/commands/rpc/payment.rs (rpc.rs became a directory module) and branches off before the default lane's machinery, so the token cache, Tooling Access enable/recovery, networks map, and retrying() are structurally unreachable: paid calls are never auto-retried, and a lost or uninterpretable post-payment response maps to PaymentMaybeCharged (exit 3, check-your-wallet guidance). Key resolution: --payment-key-file > QN_PAYMENT_KEY > key_file in config; the raw key is never a flag value, never inline in config, and never logged. The spend ceiling (--max-amount, base units) has no built-in default and is integer-validated before any request. --receipt opts stdout into {result, payment_receipt} (settlement tx hash on MPP, null on x402); the default paid output shape is identical to an unpaid call. 20 new unit tests. --- src/commands/{rpc.rs => rpc/mod.rs} | 85 ++++- src/commands/rpc/payment.rs | 559 ++++++++++++++++++++++++++++ 2 files changed, 637 insertions(+), 7 deletions(-) rename src/commands/{rpc.rs => rpc/mod.rs} (85%) create mode 100644 src/commands/rpc/payment.rs diff --git a/src/commands/rpc.rs b/src/commands/rpc/mod.rs similarity index 85% rename from src/commands/rpc.rs rename to src/commands/rpc/mod.rs index ec02a53..126672a 100644 --- a/src/commands/rpc.rs +++ b/src/commands/rpc/mod.rs @@ -18,6 +18,13 @@ //! config) is a separate lane: the SDK sends the call straight to that URL with //! no JWT minted or attached. That lane never touches the token cache or the //! Tooling Access enable/probe recovery. +//! +//! The crypto-micropayment lane (`--x402`/`--mpp`) is a third lane, in +//! `payment.rs`: keyless, paid per request, and structurally separate — it +//! branches off before any of this module's token-cache or Tooling Access +//! machinery runs. + +mod payment; use std::io::Read; use std::path::{Path, PathBuf}; @@ -49,8 +56,14 @@ pub enum RpcCmd { qn rpc call eth_blockNumber --endpoint-url https://my-endpoint.example/rpc\n \ qn rpc call eth_call --params-file params.json\n \ echo '[...]' | qn rpc call eth_call -\n \ - cat params.json | qn rpc call eth_call -f -")] - Call(CallArgs), + cat params.json | qn rpc call eth_call -f -\n\n\ + Paid (crypto micropayment, no API key; params from [rpc.payment] in config):\n \ + qn rpc call eth_blockNumber --network base-sepolia --x402\n \ + qn rpc call eth_blockNumber --network base-sepolia --x402 \\\n \ + --payment-key-file ~/.keys/payer --pay-network eip155:84532 \\\n \ + --asset 0x036C... --max-amount 10000\n \ + qn rpc call eth_blockNumber --network tempo-testnet --mpp --receipt")] + Call(Box), /// List the endpoint's available network keys (no RPC call). #[command(visible_alias = "ls")] @@ -58,7 +71,10 @@ pub enum RpcCmd { } #[derive(Debug, ClapArgs)] -#[command(group(ArgGroup::new("params_source").args(["params", "params_file"])))] +#[command( + group(ArgGroup::new("params_source").args(["params", "params_file"])), + group(ArgGroup::new("payment").args(["x402", "mpp"])), +)] pub struct CallArgs { /// The JSON-RPC method, e.g. `eth_blockNumber`. #[arg(value_name = "METHOD")] @@ -90,11 +106,59 @@ pub struct CallArgs { /// exclusive with `--network` (a custom URL is not multichain-routed). #[arg(long, conflicts_with = "network", value_name = "URL")] pub endpoint_url: Option, + + /// Pay for this call per request with the x402 protocol (EVM or Solana + /// stablecoin) instead of the account's API key. Moves real funds; use a + /// dedicated, minimally funded wallet. Requires --network (the query + /// chain, as the payment gateway's path slug — e.g. `base-sepolia`). + #[arg(long, conflicts_with = "endpoint_url", help_heading = "Payment")] + pub x402: bool, + + /// Pay for this call per request with MPP (Tempo). Mutually exclusive + /// with --x402; same rules otherwise. + #[arg(long, conflicts_with = "endpoint_url", help_heading = "Payment")] + pub mpp: bool, + + /// File containing the raw payment private key (EVM/Tempo hex, Solana + /// base58); pass `-` to read it from stdin. Never accepts the key itself. + /// Precedence: this flag > QN_PAYMENT_KEY env var (raw key) > `key_file` + /// under [rpc.payment] in config. + #[arg(long, value_name = "PATH", requires = "payment", help_heading = "Payment")] + pub payment_key_file: Option, + + /// Spend ceiling per call, in integer base units of the asset (e.g. + /// 10000 = 0.01 USDC). No built-in default: flag > `max_amount` under + /// [rpc.payment]. Offered payments above the ceiling are never signed. + #[arg(long, value_name = "BASE_UNITS", requires = "payment", help_heading = "Payment")] + pub max_amount: Option, + + /// CAIP-2 id of the chain you PAY on (e.g. `eip155:84532`), independent + /// of --network (the chain you query). Falls back to `pay_network` under + /// [rpc.payment]. + #[arg(long, value_name = "CAIP2", requires = "payment", help_heading = "Payment")] + pub pay_network: Option, + + /// Token to pay with: EVM contract address or Solana mint. Falls back to + /// `asset` under [rpc.payment]. + #[arg(long, value_name = "ADDRESS", requires = "payment", help_heading = "Payment")] + pub asset: Option, + + /// Explicit Solana RPC URL for building x402/Solana payments. Falls back + /// to `svm_rpc_url` under [rpc.payment], then a public Solana RPC (which + /// rate-limits aggressively — set this at any real volume). + #[arg(long, value_name = "URL", requires = "payment", help_heading = "Payment")] + pub svm_rpc_url: Option, + + /// Wrap stdout as {"result": ..., "payment_receipt": ...}. The receipt is + /// non-null only on MPP (the settlement transaction hash); null on x402. + /// Payment happens either way — this only changes the output. + #[arg(long, requires = "payment", help_heading = "Payment")] + pub receipt: bool, } pub async fn run(args: Args, global: GlobalArgs) -> Result<(), CliError> { match args.cmd { - RpcCmd::Call(call) => run_call(call, global).await, + RpcCmd::Call(call) => run_call(*call, global).await, RpcCmd::ListNetworks => run_list_networks(global).await, } } @@ -114,6 +178,13 @@ async fn run_list_networks(global: GlobalArgs) -> Result<(), CliError> { } async fn run_call(args: CallArgs, global: GlobalArgs) -> Result<(), CliError> { + // The crypto-micropayment lane branches off before any token-cache or + // Tooling Access work: it is keyless, never retried, and never touches + // this function's caches or recovery paths. + if args.x402 || args.mpp { + return payment::run_paid_call(args, global).await; + } + let params = parse_params(args.params.as_deref(), args.params_file.as_deref())?; // A custom URL (per-call flag, else the `[rpc] endpoint_url` config default) @@ -516,7 +587,7 @@ async fn maybe_enable(ctx: &Ctx) -> Result<(), CliError> { /// as JSON. `None`/`None` → no params (sends `[]`). Either source accepts `-` to /// read from stdin. The clap `ArgGroup` guarantees at most one is set. An empty /// value (after trimming) is treated as no params. -fn parse_params(arg: Option<&str>, file: Option<&Path>) -> Result, CliError> { +pub(super) fn parse_params(arg: Option<&str>, file: Option<&Path>) -> Result, CliError> { let raw = match (arg, file) { (None, None) => return Ok(None), (Some("-"), _) => read_stdin("params")?, @@ -539,7 +610,7 @@ fn parse_params(arg: Option<&str>, file: Option<&Path>) -> Result, } /// Read all of stdin as a UTF-8 string, labeling errors with `what`. -fn read_stdin(what: &str) -> Result { +pub(super) fn read_stdin(what: &str) -> Result { let mut buf = String::new(); std::io::stdin() .read_to_string(&mut buf) @@ -552,7 +623,7 @@ fn read_stdin(what: &str) -> Result { /// raw `--format` flag, not the TTY-aware resolved default). `json`/`yaml`/`toon` /// render as requested. `table`/`md` have no columns here, so they fall back to /// JSON — and only that explicit case prints a one-line note on stderr. -fn emit_result(ctx: &Ctx, result: &Value) -> Result<(), CliError> { +pub(super) fn emit_result(ctx: &Ctx, result: &Value) -> Result<(), CliError> { match ctx.global.format { None | Some(Format::Json) => { println!( diff --git a/src/commands/rpc/payment.rs b/src/commands/rpc/payment.rs new file mode 100644 index 0000000..dd17877 --- /dev/null +++ b/src/commands/rpc/payment.rs @@ -0,0 +1,559 @@ +//! The crypto-micropayment lane of `qn rpc call` (`--x402`/`--mpp`): pay per +//! RPC request with a stablecoin instead of an account API key, via the SDK's +//! 402 → sign → resend handshake against Quicknode's payment gateways. +//! +//! This lane is deliberately structural, not conditional: `run_call` branches +//! here before any of the default lane's machinery, so the token cache, the +//! Tooling Access enable/probe recovery, the networks map, and `retrying()` +//! are unreachable. Paid calls are NEVER auto-retried — a lost response after +//! the payment was submitted (`PaymentIndeterminate`, or an uninterpretable +//! post-payment body) means the caller may already have been charged. +//! +//! Everything the lane needs is resolved before any network I/O: the private +//! key (flag file/stdin > `QN_PAYMENT_KEY` env > `key_file` in config — never +//! a raw key on the command line or inline in config), the spend ceiling, the +//! pay network, and the asset. The key lives only inside the SDK's +//! `PaymentConfig` (which redacts it in Debug) and is never logged or echoed. + +use std::path::Path; + +use quicknode_sdk::errors::SdkError; +use quicknode_sdk::PaymentConfig; + +use crate::config::{self, PaymentSection}; +use crate::context::{Ctx, GlobalArgs}; +use crate::errors::CliError; + +use super::CallArgs; + +/// Entry point from `run_call` once `--x402`/`--mpp` is present. +pub(super) async fn run_paid_call(args: CallArgs, global: GlobalArgs) -> Result<(), CliError> { + // Both the key and the params may come from stdin; there is only one stdin. + let params_use_stdin = matches!(args.params.as_deref(), Some("-")) + || matches!(&args.params_file, Some(p) if p.as_os_str() == "-"); + let key_use_stdin = matches!(&args.payment_key_file, Some(p) if p.as_os_str() == "-"); + if params_use_stdin && key_use_stdin { + return Err(CliError::Arg( + "cannot read both the params and the payment key from stdin; \ + put one of them in a file" + .to_string(), + )); + } + + // Config parameter defaults. Unlike the default lane's endpoint_url load, + // a broken config file is a hard error here (exit 4): the user probably + // relies on [rpc.payment] values we could not read. + let section = load_payment_section(&global)?; + + // The single documented env read, injected into the resolver so it stays + // deterministic under test (no process-env mutation). + let env_key = std::env::var("QN_PAYMENT_KEY").ok(); + + let (payment, network, key_file_warning) = + resolve_payment_config(&args, §ion, env_key, global.base_url.clone())?; + + let params = super::parse_params(args.params.as_deref(), args.params_file.as_deref())?; + + let ctx = Ctx::from_global_keyless_payment(global, payment)?; + if let Some(w) = key_file_warning { + ctx.out.warn(&w); + } + + // ONE attempt, no retrying(): a retried paid call risks a double charge. + // call_with_receipt is the single code path; the receipt is dropped unless + // --receipt asked for it. + let resp = ctx + .sdk + .rpc + .call_with_receipt(&args.method, params, Some(network), None) + .await + .map_err(map_paid_error)?; + + if args.receipt { + // The receipt is data: it goes to stdout, opted into explicitly since + // it changes the output shape. `null` on x402 (no settlement + // reference exists in that protocol). + let receipt = resp.payment_receipt.map(|r| { + serde_json::json!({ + "method": r.method, + "status": r.status, + "timestamp": r.timestamp, + "reference": r.reference, + }) + }); + super::emit_result( + &ctx, + &serde_json::json!({ + "result": resp.result, + "payment_receipt": receipt, + }), + ) + } else { + // Identical output shape to an unpaid call. + super::emit_result(&ctx, &resp.result) + } +} + +/// Loads `[rpc.payment]` from the resolved config file. A missing file is an +/// empty section; an unreadable/invalid file is a hard error, since payment +/// parameters the user set there would otherwise be silently ignored. +fn load_payment_section(global: &GlobalArgs) -> Result { + let Some(path) = global.resolve_config_path() else { + return Ok(PaymentSection::default()); + }; + Ok(config::load_from(&path)? + .map(|cfg| cfg.rpc.payment) + .unwrap_or_default()) +} + +/// Resolves the full payment configuration from flags, the injected env value, +/// and the config section — entirely before any network I/O, so every missing +/// or malformed input fails fast with an actionable message and zero requests +/// sent. Returns the SDK config, the query network, and an optional key-file +/// permissions warning for the caller to print once output exists. +fn resolve_payment_config( + args: &CallArgs, + section: &PaymentSection, + env_key: Option, + base_url_override: Option, +) -> Result<(PaymentConfig, String, Option), CliError> { + // Scheme comes from which flag is set; clap's ArgGroup guarantees at most + // one, and run_paid_call only runs when one is present. + let scheme = if args.x402 { "x402" } else { "mpp" }; + + // The query chain. Required, but enforced here rather than via clap so the + // error can explain the query-chain vs pay-chain distinction. + let Some(network) = args.network.clone() else { + return Err(CliError::Arg(format!( + "--{scheme} requires --network: the chain the call queries, as the \ + payment gateway's path slug (e.g. --network base-sepolia). This is \ + separate from --pay-network, the chain the payment settles on." + ))); + }; + + // An inline raw key in config is never accepted — it belongs in a file. + if section.key.is_some() { + return Err(CliError::Arg( + "[rpc.payment] does not accept an inline `key`; store the key in a \ + file and set `key_file = \"\"` instead (the config file is \ + too easily shared to hold a raw wallet key)" + .to_string(), + )); + } + + let (key, key_file_warning) = + resolve_key(args.payment_key_file.as_deref(), env_key, section.key_file.as_deref())?; + + let max_amount = args + .max_amount + .clone() + .or_else(|| section.max_amount.clone()) + .ok_or_else(|| { + CliError::Arg( + "no spend ceiling set. Pass --max-amount or set \ + `max_amount` under [rpc.payment]. This is the most a single \ + call may pay, in integer base units of the asset (e.g. \ + 10000 = 0.01 USDC)" + .to_string(), + ) + })?; + if max_amount.parse::().is_err() { + return Err(CliError::Arg(format!( + "--max-amount must be a non-negative integer in the asset's base \ + units (e.g. 10000 = 0.01 USDC), got '{max_amount}'" + ))); + } + + let pay_network = args + .pay_network + .clone() + .or_else(|| section.pay_network.clone()) + .ok_or_else(|| { + CliError::Arg( + "no pay network set. Pass --pay-network (e.g. \ + eip155:84532) or set `pay_network` under [rpc.payment]" + .to_string(), + ) + })?; + + let asset = args + .asset + .clone() + .or_else(|| section.asset.clone()) + .ok_or_else(|| { + CliError::Arg( + "no payment asset set. Pass --asset
(the token \ + contract or mint to pay with) or set `asset` under \ + [rpc.payment]" + .to_string(), + ) + })?; + + let svm_rpc_url = match args.svm_rpc_url.clone().or_else(|| section.svm_rpc_url.clone()) { + Some(u) => Some(crate::context::validate_endpoint_url(&u)?), + None => None, + }; + + Ok(( + PaymentConfig { + scheme: scheme.to_string(), + key, + pay_network, + asset, + max_amount, + svm_rpc_url, + base_url_override, + }, + network, + key_file_warning, + )) +} + +/// Resolves the raw private key: `--payment-key-file` (a path, or `-` for +/// stdin) > env `QN_PAYMENT_KEY` > config `key_file` path. The raw key is +/// never a flag value. Returns the key and an optional permissions warning +/// (group/world-readable key file). Error messages name the path, never the +/// file contents. +fn resolve_key( + flag_file: Option<&Path>, + env_key: Option, + config_file: Option<&Path>, +) -> Result<(String, Option), CliError> { + if let Some(path) = flag_file { + if path.as_os_str() == "-" { + let key = super::read_stdin("the payment key")?; + return checked_key(key, "stdin").map(|k| (k, None)); + } + return read_key_file(path); + } + if let Some(key) = env_key { + return checked_key(key, "QN_PAYMENT_KEY").map(|k| (k, None)); + } + if let Some(path) = config_file { + return read_key_file(path); + } + Err(CliError::Arg( + "no payment key found. Pass --payment-key-file (or '-' for \ + stdin), set QN_PAYMENT_KEY, or set `key_file` under [rpc.payment]" + .to_string(), + )) +} + +/// Reads and validates a key file, plus an ssh-style permissions warning when +/// the file is group- or world-readable. +fn read_key_file(path: &Path) -> Result<(String, Option), CliError> { + let raw = std::fs::read_to_string(path).map_err(|e| { + CliError::Arg(format!( + "could not read payment key file '{}': {e}", + path.display() + )) + })?; + let key = checked_key(raw, &format!("'{}'", path.display()))?; + + #[cfg(unix)] + let warning = { + use std::os::unix::fs::PermissionsExt; + std::fs::metadata(path) + .ok() + .map(|m| m.permissions().mode()) + .filter(|mode| mode & 0o077 != 0) + .map(|_| { + format!( + "⚠ payment key file '{}' is readable by other users; \ + consider `chmod 600`", + path.display() + ) + }) + }; + #[cfg(not(unix))] + let warning = None; + + Ok((key, warning)) +} + +/// Trims and rejects an empty key. `source` names where the key came from +/// (a path, `stdin`, or the env var) — never its contents. +fn checked_key(raw: String, source: &str) -> Result { + let trimmed = raw.trim(); + if trimmed.is_empty() { + return Err(CliError::Arg(format!( + "the payment key from {source} is empty" + ))); + } + Ok(trimmed.to_string()) +} + +/// Maps a paid-call failure. A `Decode` error on the paid lane means the +/// gateway's post-payment response could not be interpreted — the payment may +/// already have settled, so it gets the same never-blindly-retry treatment as +/// `PaymentIndeterminate` (exit 3) instead of rendering as a generic decode +/// failure. Everything else passes through to the normal SDK mapping. +fn map_paid_error(e: SdkError) -> CliError { + match e { + SdkError::Decode { .. } => CliError::PaymentMaybeCharged(e), + other => other.into(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn paid_args(x402: bool) -> CallArgs { + CallArgs { + method: "eth_blockNumber".to_string(), + params: None, + params_file: None, + network: Some("base-sepolia".to_string()), + endpoint_url: None, + x402, + mpp: !x402, + payment_key_file: None, + max_amount: Some("10000".to_string()), + pay_network: Some("eip155:84532".to_string()), + asset: Some("0xabc".to_string()), + svm_rpc_url: None, + receipt: false, + } + } + + fn empty_section() -> PaymentSection { + PaymentSection::default() + } + + fn key_file_with(contents: &str) -> tempfile::NamedTempFile { + use std::io::Write; + let mut f = tempfile::NamedTempFile::new().unwrap(); + f.write_all(contents.as_bytes()).unwrap(); + f.flush().unwrap(); + f + } + + #[test] + fn resolves_full_config_from_flags_and_env_key() { + let args = paid_args(true); + let (cfg, network, _) = + resolve_payment_config(&args, &empty_section(), Some("0xkey".to_string()), None) + .unwrap(); + assert_eq!(cfg.scheme, "x402"); + assert_eq!(cfg.key, "0xkey"); + assert_eq!(cfg.pay_network, "eip155:84532"); + assert_eq!(cfg.asset, "0xabc"); + assert_eq!(cfg.max_amount, "10000"); + assert_eq!(network, "base-sepolia"); + } + + #[test] + fn mpp_flag_selects_mpp_scheme() { + let args = paid_args(false); + let (cfg, _, _) = + resolve_payment_config(&args, &empty_section(), Some("k".to_string()), None).unwrap(); + assert_eq!(cfg.scheme, "mpp"); + } + + #[test] + fn missing_network_names_both_flags() { + let mut args = paid_args(true); + args.network = None; + let err = resolve_payment_config(&args, &empty_section(), Some("k".to_string()), None) + .unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("--network"), "got: {msg}"); + assert!(msg.contains("--pay-network"), "got: {msg}"); + } + + #[test] + fn flag_key_file_beats_env_key() { + let f = key_file_with("0xfromfile\n"); + let mut args = paid_args(true); + args.payment_key_file = Some(f.path().to_path_buf()); + let (cfg, _, _) = + resolve_payment_config(&args, &empty_section(), Some("0xfromenv".to_string()), None) + .unwrap(); + assert_eq!(cfg.key, "0xfromfile"); // and trimmed + } + + #[test] + fn env_key_beats_config_key_file() { + let f = key_file_with("0xfromconfig"); + let mut section = empty_section(); + section.key_file = Some(f.path().to_path_buf()); + let args = paid_args(true); + let (cfg, _, _) = + resolve_payment_config(&args, §ion, Some("0xfromenv".to_string()), None).unwrap(); + assert_eq!(cfg.key, "0xfromenv"); + } + + #[test] + fn config_key_file_used_when_nothing_else() { + let f = key_file_with("0xfromconfig"); + let mut section = empty_section(); + section.key_file = Some(f.path().to_path_buf()); + let args = paid_args(true); + let (cfg, _, _) = resolve_payment_config(&args, §ion, None, None).unwrap(); + assert_eq!(cfg.key, "0xfromconfig"); + } + + #[test] + fn no_key_anywhere_is_actionable() { + let args = paid_args(true); + let err = resolve_payment_config(&args, &empty_section(), None, None).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("--payment-key-file"), "got: {msg}"); + assert!(msg.contains("QN_PAYMENT_KEY"), "got: {msg}"); + assert!(msg.contains("key_file"), "got: {msg}"); + } + + #[test] + fn unreadable_key_file_names_the_path() { + let mut args = paid_args(true); + args.payment_key_file = Some("/does/not/exist.key".into()); + let err = resolve_payment_config(&args, &empty_section(), None, None).unwrap_err(); + assert!(err.to_string().contains("/does/not/exist.key")); + } + + #[test] + fn empty_key_file_is_rejected_without_leaking_contents() { + let f = key_file_with(" \n"); + let mut args = paid_args(true); + args.payment_key_file = Some(f.path().to_path_buf()); + let err = resolve_payment_config(&args, &empty_section(), None, None).unwrap_err(); + assert!(err.to_string().contains("empty"), "got: {err}"); + } + + #[test] + fn inline_config_key_is_rejected_with_key_file_pointer() { + let mut section = empty_section(); + section.key = Some(toml::Value::String("0xraw".to_string())); + let args = paid_args(true); + let err = + resolve_payment_config(&args, §ion, Some("k".to_string()), None).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("key_file"), "got: {msg}"); + assert!(!msg.contains("0xraw"), "must not echo the key: {msg}"); + } + + #[test] + fn missing_max_amount_is_actionable() { + let mut args = paid_args(true); + args.max_amount = None; + let err = resolve_payment_config(&args, &empty_section(), Some("k".to_string()), None) + .unwrap_err(); + assert!(err.to_string().contains("--max-amount"), "got: {err}"); + } + + #[test] + fn non_integer_max_amount_is_rejected() { + for bad in ["1.5", "abc", "-1", "1_000"] { + let mut args = paid_args(true); + args.max_amount = Some(bad.to_string()); + let err = resolve_payment_config(&args, &empty_section(), Some("k".to_string()), None) + .unwrap_err(); + assert!( + err.to_string().contains("base units"), + "for {bad}: {err}" + ); + } + } + + #[test] + fn flag_max_amount_beats_config() { + let mut section = empty_section(); + section.max_amount = Some("999999".to_string()); + let args = paid_args(true); // flag says 10000 + let (cfg, _, _) = + resolve_payment_config(&args, §ion, Some("k".to_string()), None).unwrap(); + assert_eq!(cfg.max_amount, "10000"); + } + + #[test] + fn config_fills_all_missing_params() { + let f = key_file_with("0xk"); + let section = PaymentSection { + key_file: Some(f.path().to_path_buf()), + key: None, + max_amount: Some("5000".to_string()), + pay_network: Some("eip155:42431".to_string()), + asset: Some("0xdef".to_string()), + svm_rpc_url: None, + }; + let mut args = paid_args(false); + args.max_amount = None; + args.pay_network = None; + args.asset = None; + let (cfg, _, _) = resolve_payment_config(&args, §ion, None, None).unwrap(); + assert_eq!(cfg.scheme, "mpp"); + assert_eq!(cfg.max_amount, "5000"); + assert_eq!(cfg.pay_network, "eip155:42431"); + assert_eq!(cfg.asset, "0xdef"); + } + + #[test] + fn base_url_override_is_threaded_through() { + let args = paid_args(true); + let (cfg, _, _) = resolve_payment_config( + &args, + &empty_section(), + Some("k".to_string()), + Some("http://127.0.0.1:9999".to_string()), + ) + .unwrap(); + assert_eq!( + cfg.base_url_override.as_deref(), + Some("http://127.0.0.1:9999") + ); + } + + #[test] + fn invalid_svm_rpc_url_is_rejected() { + let mut args = paid_args(true); + args.svm_rpc_url = Some("ftp://nope".to_string()); + let err = resolve_payment_config(&args, &empty_section(), Some("k".to_string()), None) + .unwrap_err(); + assert!(err.to_string().contains("scheme"), "got: {err}"); + } + + #[cfg(unix)] + #[test] + fn world_readable_key_file_produces_warning() { + use std::os::unix::fs::PermissionsExt; + let f = key_file_with("0xk"); + std::fs::set_permissions(f.path(), std::fs::Permissions::from_mode(0o644)).unwrap(); + let (_, warning) = read_key_file(f.path()).unwrap(); + assert!(warning.is_some()); + let w = warning.unwrap(); + assert!(w.contains("chmod 600"), "got: {w}"); + assert!(!w.contains("0xk"), "must not leak contents: {w}"); + } + + #[cfg(unix)] + #[test] + fn private_key_file_produces_no_warning() { + use std::os::unix::fs::PermissionsExt; + let f = key_file_with("0xk"); + std::fs::set_permissions(f.path(), std::fs::Permissions::from_mode(0o600)).unwrap(); + let (_, warning) = read_key_file(f.path()).unwrap(); + assert!(warning.is_none(), "got: {warning:?}"); + } + + #[test] + fn decode_error_maps_to_payment_maybe_charged() { + let decode = SdkError::Decode { + source: serde_json::from_str::("x").unwrap_err(), + body: "gateway 5xx html".to_string(), + }; + let mapped = map_paid_error(decode); + assert!(matches!(mapped, CliError::PaymentMaybeCharged(_))); + assert_eq!(crate::errors::exit_code_for(&mapped), 3); + } + + #[test] + fn rejected_error_passes_through_to_exit_2() { + let rejected = SdkError::PaymentRejected { + status: 402, + body: "bad sig".to_string(), + }; + let mapped = map_paid_error(rejected); + assert_eq!(crate::errors::exit_code_for(&mapped), 2); + } +} From b46e57d6ca5f55ef1972820bc4f2b718d72c02f7 Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Mon, 13 Jul 2026 03:25:43 -0400 Subject: [PATCH 6/8] test(rpc): integration coverage for the crypto-micropayment lane 18 tests against a wiremock gateway using the SDK's 402-handshake shapes (x402 menu + signed resend; MPP WWW-Authenticate challenge + Payment-Receipt header) and public fixtures (anvil key #0, Base Sepolia test USDC). Safety-critical assertions: - lane isolation: a paid call sends exactly the two gateway POSTs, never touches /v0 control-plane routes, and writes no tokens.toml - config presence never auto-activates payment (gateway .expect(0)) - the paid lane ignores --retries (one request, ever) - an over-cap offer is refused after one request, before signing - every pre-flight failure (missing network/key/cap, non-integer cap, inline config key, double-stdin) exits before any request - --receipt wraps stdout with the MPP settlement reference and null on x402; without it the paid output is the bare result; the raw key never appears on either stream (subprocess assertions) Also applies rustfmt to the new payment modules. --- src/commands/rpc/mod.rs | 40 +- src/commands/rpc/payment.rs | 21 +- tests/rpc_payment.rs | 791 ++++++++++++++++++++++++++++++++++++ 3 files changed, 837 insertions(+), 15 deletions(-) create mode 100644 tests/rpc_payment.rs diff --git a/src/commands/rpc/mod.rs b/src/commands/rpc/mod.rs index 126672a..e43053f 100644 --- a/src/commands/rpc/mod.rs +++ b/src/commands/rpc/mod.rs @@ -123,30 +123,55 @@ pub struct CallArgs { /// base58); pass `-` to read it from stdin. Never accepts the key itself. /// Precedence: this flag > QN_PAYMENT_KEY env var (raw key) > `key_file` /// under [rpc.payment] in config. - #[arg(long, value_name = "PATH", requires = "payment", help_heading = "Payment")] + #[arg( + long, + value_name = "PATH", + requires = "payment", + help_heading = "Payment" + )] pub payment_key_file: Option, /// Spend ceiling per call, in integer base units of the asset (e.g. /// 10000 = 0.01 USDC). No built-in default: flag > `max_amount` under /// [rpc.payment]. Offered payments above the ceiling are never signed. - #[arg(long, value_name = "BASE_UNITS", requires = "payment", help_heading = "Payment")] + #[arg( + long, + value_name = "BASE_UNITS", + requires = "payment", + help_heading = "Payment" + )] pub max_amount: Option, /// CAIP-2 id of the chain you PAY on (e.g. `eip155:84532`), independent /// of --network (the chain you query). Falls back to `pay_network` under /// [rpc.payment]. - #[arg(long, value_name = "CAIP2", requires = "payment", help_heading = "Payment")] + #[arg( + long, + value_name = "CAIP2", + requires = "payment", + help_heading = "Payment" + )] pub pay_network: Option, /// Token to pay with: EVM contract address or Solana mint. Falls back to /// `asset` under [rpc.payment]. - #[arg(long, value_name = "ADDRESS", requires = "payment", help_heading = "Payment")] + #[arg( + long, + value_name = "ADDRESS", + requires = "payment", + help_heading = "Payment" + )] pub asset: Option, /// Explicit Solana RPC URL for building x402/Solana payments. Falls back /// to `svm_rpc_url` under [rpc.payment], then a public Solana RPC (which /// rate-limits aggressively — set this at any real volume). - #[arg(long, value_name = "URL", requires = "payment", help_heading = "Payment")] + #[arg( + long, + value_name = "URL", + requires = "payment", + help_heading = "Payment" + )] pub svm_rpc_url: Option, /// Wrap stdout as {"result": ..., "payment_receipt": ...}. The receipt is @@ -587,7 +612,10 @@ async fn maybe_enable(ctx: &Ctx) -> Result<(), CliError> { /// as JSON. `None`/`None` → no params (sends `[]`). Either source accepts `-` to /// read from stdin. The clap `ArgGroup` guarantees at most one is set. An empty /// value (after trimming) is treated as no params. -pub(super) fn parse_params(arg: Option<&str>, file: Option<&Path>) -> Result, CliError> { +pub(super) fn parse_params( + arg: Option<&str>, + file: Option<&Path>, +) -> Result, CliError> { let raw = match (arg, file) { (None, None) => return Ok(None), (Some("-"), _) => read_stdin("params")?, diff --git a/src/commands/rpc/payment.rs b/src/commands/rpc/payment.rs index dd17877..aa43fb7 100644 --- a/src/commands/rpc/payment.rs +++ b/src/commands/rpc/payment.rs @@ -141,8 +141,11 @@ fn resolve_payment_config( )); } - let (key, key_file_warning) = - resolve_key(args.payment_key_file.as_deref(), env_key, section.key_file.as_deref())?; + let (key, key_file_warning) = resolve_key( + args.payment_key_file.as_deref(), + env_key, + section.key_file.as_deref(), + )?; let max_amount = args .max_amount @@ -189,7 +192,11 @@ fn resolve_payment_config( ) })?; - let svm_rpc_url = match args.svm_rpc_url.clone().or_else(|| section.svm_rpc_url.clone()) { + let svm_rpc_url = match args + .svm_rpc_url + .clone() + .or_else(|| section.svm_rpc_url.clone()) + { Some(u) => Some(crate::context::validate_endpoint_url(&u)?), None => None, }; @@ -426,8 +433,7 @@ mod tests { let mut section = empty_section(); section.key = Some(toml::Value::String("0xraw".to_string())); let args = paid_args(true); - let err = - resolve_payment_config(&args, §ion, Some("k".to_string()), None).unwrap_err(); + let err = resolve_payment_config(&args, §ion, Some("k".to_string()), None).unwrap_err(); let msg = err.to_string(); assert!(msg.contains("key_file"), "got: {msg}"); assert!(!msg.contains("0xraw"), "must not echo the key: {msg}"); @@ -449,10 +455,7 @@ mod tests { args.max_amount = Some(bad.to_string()); let err = resolve_payment_config(&args, &empty_section(), Some("k".to_string()), None) .unwrap_err(); - assert!( - err.to_string().contains("base units"), - "for {bad}: {err}" - ); + assert!(err.to_string().contains("base units"), "for {bad}: {err}"); } } diff --git a/tests/rpc_payment.rs b/tests/rpc_payment.rs new file mode 100644 index 0000000..fd7bcf3 --- /dev/null +++ b/tests/rpc_payment.rs @@ -0,0 +1,791 @@ +//! Integration tests for the crypto-micropayment lane of `qn rpc call` +//! (`--x402`/`--mpp`). +//! +//! The harness's hidden `--base-url` is threaded into +//! `PaymentConfig.base_url_override`, so the mock server doubles as the +//! payment gateway: the paid lane POSTs to `{base}/`. The 402 +//! handshake shapes mirror the SDK's own driver tests (an unpaid POST gets a +//! 402 with a payment menu; the resend carries a `payment-signature` / +//! `authorization` header and gets the JSON-RPC result). +//! +//! All key material is fake: anvil throwaway key #0 (public, never funded) +//! and the public Base Sepolia test-USDC address. + +mod common; + +use common::{parse, run_qn, run_qn_no_key}; +use serde_json::json; +use std::sync::atomic::{AtomicUsize, Ordering}; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, MockServer, Request, Respond, ResponseTemplate}; + +// anvil key #0 (public throwaway, never funded). +const EVM_KEY: &str = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; +// Base Sepolia test USDC (public address). +const USDC: &str = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; + +/// Writes the throwaway key to a tempfile and returns the (guard, path) pair. +fn key_file() -> (tempfile::NamedTempFile, String) { + use std::io::Write; + let mut f = tempfile::NamedTempFile::new().unwrap(); + f.write_all(EVM_KEY.as_bytes()).unwrap(); + f.flush().unwrap(); + let path = f.path().to_str().unwrap().to_string(); + (f, path) +} + +/// One entry of the x402 payment menu the mock gateway offers. +fn x402_accepts_entry(amount: &str) -> serde_json::Value { + json!({ + "scheme": "exact", + "network": "eip155:84532", + "amount": amount, + "payTo": "0x000000000000000000000000000000000000dEaD", + "maxTimeoutSeconds": 60, + "asset": USDC, + "extra": { "name": "USDC", "version": "2" } + }) +} + +/// Sequenced gateway responder: the first (unpaid) POST gets a 402 with a +/// one-entry menu at `amount`; any request carrying a payment signature gets +/// the JSON-RPC result. +struct X402Seq { + amount: &'static str, + calls: AtomicUsize, +} + +impl X402Seq { + fn new(amount: &'static str) -> Self { + Self { + amount, + calls: AtomicUsize::new(0), + } + } +} + +impl Respond for X402Seq { + fn respond(&self, req: &Request) -> ResponseTemplate { + let n = self.calls.fetch_add(1, Ordering::SeqCst); + let has_sig = req.headers.contains_key("payment-signature"); + if n == 0 && !has_sig { + ResponseTemplate::new(402).set_body_json(json!({ + "x402Version": 2, + "accepts": [ x402_accepts_entry(self.amount) ] + })) + } else { + ResponseTemplate::new(200).set_body_json(json!({ + "jsonrpc": "2.0", "id": 1, "result": "0x1335f9a" + })) + } + } +} + +/// Mounts `.expect(0)` mocks on the control-plane routes the default lane +/// uses, proving the paid lane never touches them. +async fn mount_control_plane_expect_zero(server: &MockServer) { + for p in ["/v0/tooling-access", "/v0/account/info"] { + Mock::given(path(p)) + .respond_with(ResponseTemplate::new(200)) + .expect(0) + .mount(server) + .await; + } +} + +// ── happy paths ────────────────────────────────────────────────────────────── + +#[tokio::test] +async fn x402_happy_path_pays_and_bypasses_control_plane() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/base-sepolia")) + .respond_with(X402Seq::new("1000")) + .expect(2) // one unpaid probe + one paid resend, nothing else + .mount(&server) + .await; + mount_control_plane_expect_zero(&server).await; + + let dir = tempfile::tempdir().unwrap(); + let cfg = dir.path().join("config.toml").to_str().unwrap().to_string(); + let (_guard, key_path) = key_file(); + + // The harness injects --api-key test: even with a key present, the paid + // lane must not mint, probe, or write the token cache. + let out = run_qn( + &server.uri(), + &[ + "--config-file", + &cfg, + "rpc", + "call", + "eth_blockNumber", + "--network", + "base-sepolia", + "--x402", + "--payment-key-file", + &key_path, + "--pay-network", + "eip155:84532", + "--asset", + USDC, + "--max-amount", + "10000", + ], + ) + .await; + assert_eq!(out.exit_code, 0, "stderr={}", out.stderr); + assert!( + !dir.path().join("tokens.toml").exists(), + "paid lane must not write the token cache" + ); +} + +#[tokio::test] +async fn paid_call_works_keyless_with_config_params() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/base-sepolia")) + .respond_with(X402Seq::new("1000")) + .expect(2) + .mount(&server) + .await; + mount_control_plane_expect_zero(&server).await; + + // Config supplies every parameter and has NO [api] key: the minimal + // invocation is method + --network + the scheme flag, with no login ever. + let dir = tempfile::tempdir().unwrap(); + let (_guard, key_path) = key_file(); + let cfg = dir.path().join("config.toml"); + std::fs::write( + &cfg, + format!( + "[rpc.payment]\nkey_file = \"{key_path}\"\nmax_amount = \"10000\"\n\ + pay_network = \"eip155:84532\"\nasset = \"{USDC}\"\n" + ), + ) + .unwrap(); + + let out = run_qn_no_key( + &server.uri(), + &[ + "--config-file", + cfg.to_str().unwrap(), + "rpc", + "call", + "eth_blockNumber", + "--network", + "base-sepolia", + "--x402", + ], + ) + .await; + assert_eq!(out.exit_code, 0, "stderr={}", out.stderr); +} + +// ── spend-cap enforcement ──────────────────────────────────────────────────── + +#[tokio::test] +async fn over_cap_offer_is_refused_before_signing() { + let server = MockServer::start().await; + // Menu offers 999999; the cap is 10000. Exactly ONE request: the unpaid + // probe. Nothing is signed, nothing is resent. + Mock::given(method("POST")) + .and(path("/base-sepolia")) + .respond_with(X402Seq::new("999999")) + .expect(1) + .mount(&server) + .await; + + let (_guard, key_path) = key_file(); + let out = run_qn( + &server.uri(), + &[ + "rpc", + "call", + "eth_blockNumber", + "--network", + "base-sepolia", + "--x402", + "--payment-key-file", + &key_path, + "--pay-network", + "eip155:84532", + "--asset", + USDC, + "--max-amount", + "10000", + ], + ) + .await; + assert_eq!(out.exit_code, 2, "stderr={}", out.stderr); + assert!( + out.stderr.contains("Nothing was charged"), + "stderr={}", + out.stderr + ); +} + +#[tokio::test] +async fn flag_max_amount_overrides_config() { + let server = MockServer::start().await; + // Config would allow the 1000 offer (cap 999999); the flag caps at 1. + // A resulting "unsupported" refusal proves the flag won. + Mock::given(method("POST")) + .and(path("/base-sepolia")) + .respond_with(X402Seq::new("1000")) + .expect(1) + .mount(&server) + .await; + + let dir = tempfile::tempdir().unwrap(); + let (_guard, key_path) = key_file(); + let cfg = dir.path().join("config.toml"); + std::fs::write( + &cfg, + format!( + "[rpc.payment]\nkey_file = \"{key_path}\"\nmax_amount = \"999999\"\n\ + pay_network = \"eip155:84532\"\nasset = \"{USDC}\"\n" + ), + ) + .unwrap(); + + let out = run_qn_no_key( + &server.uri(), + &[ + "--config-file", + cfg.to_str().unwrap(), + "rpc", + "call", + "eth_blockNumber", + "--network", + "base-sepolia", + "--x402", + "--max-amount", + "1", + ], + ) + .await; + assert_eq!(out.exit_code, 2, "stderr={}", out.stderr); + assert!( + out.stderr.contains("Nothing was charged"), + "stderr={}", + out.stderr + ); +} + +// ── activation and lane isolation ──────────────────────────────────────────── + +#[tokio::test] +async fn config_presence_does_not_auto_activate_payment() { + let server = MockServer::start().await; + // Fully populated [rpc.payment] but NO scheme flag: the call must take the + // normal Tooling Access lane (mint attempt against the control plane) and + // the gateway must see zero traffic. + Mock::given(method("POST")) + .and(path("/base-sepolia")) + .respond_with(ResponseTemplate::new(200)) + .expect(0) + .mount(&server) + .await; + Mock::given(path("/v0/tooling-access")) + .respond_with(ResponseTemplate::new(500)) + .mount(&server) + .await; + + let dir = tempfile::tempdir().unwrap(); + let (_guard, key_path) = key_file(); + let cfg = dir.path().join("config.toml"); + std::fs::write( + &cfg, + format!( + "[api]\nkey = \"test\"\n\n\ + [rpc.payment]\nkey_file = \"{key_path}\"\nmax_amount = \"10000\"\n\ + pay_network = \"eip155:84532\"\nasset = \"{USDC}\"\n" + ), + ) + .unwrap(); + + let out = run_qn_no_key( + &server.uri(), + &[ + "--config-file", + cfg.to_str().unwrap(), + "rpc", + "call", + "eth_blockNumber", + ], + ) + .await; + // The default lane fails against the 500ing control plane — the exit code + // is not the point; the .expect(0) on the gateway is. + assert_ne!(out.exit_code, 0); +} + +#[tokio::test] +async fn paid_lane_is_never_retried() { + let server = MockServer::start().await; + // The gateway 500s every request. Even with --retries 5, exactly ONE + // request may arrive: a retried paid call risks a double charge. + Mock::given(method("POST")) + .and(path("/base-sepolia")) + .respond_with(ResponseTemplate::new(500).set_body_string("boom")) + .expect(1) + .mount(&server) + .await; + + let (_guard, key_path) = key_file(); + let out = run_qn( + &server.uri(), + &[ + "--retries", + "5", + "rpc", + "call", + "eth_blockNumber", + "--network", + "base-sepolia", + "--x402", + "--payment-key-file", + &key_path, + "--pay-network", + "eip155:84532", + "--asset", + USDC, + "--max-amount", + "10000", + ], + ) + .await; + assert_ne!(out.exit_code, 0); +} + +#[tokio::test] +async fn payment_rejected_on_paid_resend_exits_2_with_wallet_warning() { + let server = MockServer::start().await; + // 402 menu, then the paid resend is refused with another 402: a signed + // payment went out, so the message must say "check your wallet". + Mock::given(method("POST")) + .and(path("/base-sepolia")) + .respond_with(ResponseTemplate::new(402).set_body_json(json!({ + "x402Version": 2, + "accepts": [ x402_accepts_entry("1000") ] + }))) + .expect(2) + .mount(&server) + .await; + + let (_guard, key_path) = key_file(); + let out = run_qn( + &server.uri(), + &[ + "rpc", + "call", + "eth_blockNumber", + "--network", + "base-sepolia", + "--x402", + "--payment-key-file", + &key_path, + "--pay-network", + "eip155:84532", + "--asset", + USDC, + "--max-amount", + "10000", + ], + ) + .await; + assert_eq!(out.exit_code, 2, "stderr={}", out.stderr); + assert!( + out.stderr.contains("check your wallet"), + "stderr={}", + out.stderr + ); +} + +// ── pre-flight failures: fail fast, zero requests ──────────────────────────── + +/// Runs a paid invocation expected to die in pre-flight: any request reaching +/// the mock is a failure. +async fn expect_preflight_error(extra: &[&str], needle: &str) { + let server = MockServer::start().await; + Mock::given(method("POST")) + .respond_with(ResponseTemplate::new(200)) + .expect(0) + .mount(&server) + .await; + + let out = run_qn(&server.uri(), extra).await; + assert_eq!(out.exit_code, 1, "stderr={}", out.stderr); + assert!( + out.stderr.contains(needle), + "expected {needle:?} in stderr: {}", + out.stderr + ); +} + +#[tokio::test] +async fn missing_network_fails_before_any_request() { + let (_guard, key_path) = key_file(); + expect_preflight_error( + &[ + "rpc", + "call", + "eth_blockNumber", + "--x402", + "--payment-key-file", + &key_path, + "--pay-network", + "eip155:84532", + "--asset", + USDC, + "--max-amount", + "10000", + ], + "--network", + ) + .await; +} + +#[tokio::test] +async fn missing_key_fails_before_any_request() { + // No flag, no env in-process, and a config dir with no key_file. + let dir = tempfile::tempdir().unwrap(); + let cfg = dir.path().join("config.toml").to_str().unwrap().to_string(); + expect_preflight_error( + &[ + "--config-file", + &cfg, + "rpc", + "call", + "eth_blockNumber", + "--network", + "base-sepolia", + "--x402", + "--pay-network", + "eip155:84532", + "--asset", + USDC, + "--max-amount", + "10000", + ], + "QN_PAYMENT_KEY", + ) + .await; +} + +#[tokio::test] +async fn missing_max_amount_fails_before_any_request() { + let dir = tempfile::tempdir().unwrap(); + let cfg = dir.path().join("config.toml").to_str().unwrap().to_string(); + let (_guard, key_path) = key_file(); + expect_preflight_error( + &[ + "--config-file", + &cfg, + "rpc", + "call", + "eth_blockNumber", + "--network", + "base-sepolia", + "--x402", + "--payment-key-file", + &key_path, + "--pay-network", + "eip155:84532", + "--asset", + USDC, + ], + "--max-amount", + ) + .await; +} + +#[tokio::test] +async fn non_integer_max_amount_fails_before_any_request() { + let (_guard, key_path) = key_file(); + expect_preflight_error( + &[ + "rpc", + "call", + "eth_blockNumber", + "--network", + "base-sepolia", + "--x402", + "--payment-key-file", + &key_path, + "--pay-network", + "eip155:84532", + "--asset", + USDC, + "--max-amount", + "1.5", + ], + "base units", + ) + .await; +} + +#[tokio::test] +async fn inline_config_key_fails_before_any_request() { + let dir = tempfile::tempdir().unwrap(); + let cfg = dir.path().join("config.toml"); + std::fs::write( + &cfg, + "[rpc.payment]\nkey = \"0xdeadbeef\"\nmax_amount = \"10000\"\n\ + pay_network = \"eip155:84532\"\nasset = \"0xabc\"\n", + ) + .unwrap(); + expect_preflight_error( + &[ + "--config-file", + cfg.to_str().unwrap(), + "rpc", + "call", + "eth_blockNumber", + "--network", + "base-sepolia", + "--x402", + ], + "key_file", + ) + .await; +} + +#[tokio::test] +async fn params_and_key_both_from_stdin_conflict() { + let (_guard, _) = key_file(); + expect_preflight_error( + &[ + "rpc", + "call", + "eth_call", + "-", + "--network", + "base-sepolia", + "--x402", + "--payment-key-file", + "-", + "--pay-network", + "eip155:84532", + "--asset", + USDC, + "--max-amount", + "10000", + ], + "stdin", + ) + .await; +} + +// ── clap surface ───────────────────────────────────────────────────────────── + +#[test] +fn x402_and_mpp_are_mutually_exclusive() { + let err = parse(&[ + "rpc", + "call", + "eth_blockNumber", + "--network", + "n", + "--x402", + "--mpp", + ]) + .unwrap_err(); + assert!(err.to_string().contains("--mpp"), "got: {err}"); +} + +#[test] +fn payment_flags_require_a_scheme_flag() { + for orphan in [ + vec!["--receipt"], + vec!["--pay-network", "eip155:84532"], + vec!["--asset", "0xabc"], + vec!["--max-amount", "1"], + vec!["--payment-key-file", "/k"], + vec!["--svm-rpc-url", "https://x"], + ] { + let mut argv = vec!["rpc", "call", "eth_blockNumber"]; + argv.extend(orphan.iter().copied()); + assert!( + parse(&argv).is_err(), + "expected {orphan:?} to require --x402/--mpp" + ); + } +} + +#[test] +fn payment_conflicts_with_endpoint_url() { + for scheme in ["--x402", "--mpp"] { + let err = parse(&[ + "rpc", + "call", + "eth_blockNumber", + scheme, + "--endpoint-url", + "https://x/rpc", + ]) + .unwrap_err(); + assert!(err.to_string().contains("--endpoint-url"), "got: {err}"); + } +} + +// ── output shapes (subprocess: the in-process harness can't capture stdout) ── + +/// Runs the real binary against `server`, with HOME pointed at a tempdir so no +/// real config leaks in. The key is passed via QN_PAYMENT_KEY to also cover +/// env resolution (a subprocess env can't race parallel tests). +fn run_qn_subprocess( + server_uri: &str, + home: &std::path::Path, + args: &[&str], +) -> std::process::Output { + assert_cmd::Command::cargo_bin("qn") + .unwrap() + .env_remove("HOME") + .env("HOME", home) + .env("QN_PAYMENT_KEY", EVM_KEY) + .args(["--base-url", server_uri, "--no-input"]) + .args(args) + .output() + .unwrap() +} + +#[tokio::test] +async fn receipt_flag_wraps_stdout_on_mpp() { + let server = MockServer::start().await; + + // MPP challenge (tempo, chain 42431) and a settlement receipt header, both + // base64url-encoded JSON — mirrors the SDK's own MPP driver test. + fn b64url(v: serde_json::Value) -> String { + use base64::Engine; + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(serde_json::to_vec(&v).unwrap()) + } + let request = b64url(json!({ + "amount": "1000", + "currency": "0x20c0000000000000000000000000000000000000", + "recipient": "0xfd24114c3981aba78ae2441991b1bdb89329c556", + "methodDetails": { "chainId": 42431, "feePayer": true } + })); + let www = format!( + "Payment id=\"c1\", realm=\"mpp.quicknode.com\", method=\"tempo\", \ + intent=\"charge\", description=\"d\", expires=\"2099-01-01T00:00:00Z\", \ + request=\"{request}\"" + ); + let receipt = b64url(json!({ + "method": "tempo", "status": "success", + "timestamp": "2026-07-13T02:05:10.119Z", + "reference": "0xdeadbeef" + })); + + struct MppSeq { + www: String, + receipt: String, + calls: AtomicUsize, + } + impl Respond for MppSeq { + fn respond(&self, req: &Request) -> ResponseTemplate { + let n = self.calls.fetch_add(1, Ordering::SeqCst); + if n == 0 && !req.headers.contains_key("authorization") { + ResponseTemplate::new(402) + .insert_header("WWW-Authenticate", self.www.as_str()) + .set_body_json(json!({ "type": "about:blank" })) + } else { + ResponseTemplate::new(200) + .insert_header("Payment-Receipt", self.receipt.as_str()) + .set_body_json(json!({ "jsonrpc": "2.0", "id": 1, "result": "0xok" })) + } + } + } + Mock::given(method("POST")) + .and(path("/tempo-testnet")) + .respond_with(MppSeq { + www, + receipt, + calls: AtomicUsize::new(0), + }) + .mount(&server) + .await; + + let home = tempfile::tempdir().unwrap(); + let output = run_qn_subprocess( + &server.uri(), + home.path(), + &[ + "rpc", + "call", + "eth_blockNumber", + "--network", + "tempo-testnet", + "--mpp", + "--receipt", + "--pay-network", + "eip155:42431", + "--asset", + "0x20c0000000000000000000000000000000000000", + "--max-amount", + "10000", + ], + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!(output.status.success(), "stderr={stderr}"); + + let v: serde_json::Value = + serde_json::from_str(&stdout).unwrap_or_else(|e| panic!("bad JSON: {e}\n{stdout}")); + assert_eq!(v["result"].as_str(), Some("0xok")); + assert_eq!( + v["payment_receipt"]["reference"].as_str(), + Some("0xdeadbeef") + ); + // The raw key must never appear on either stream. + assert!(!stdout.contains(EVM_KEY) && !stderr.contains(EVM_KEY)); +} + +#[tokio::test] +async fn receipt_is_null_on_x402_and_bare_without_flag() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/base-sepolia")) + .respond_with(X402Seq::new("1000")) + .mount(&server) + .await; + + let home = tempfile::tempdir().unwrap(); + let paid_args = [ + "rpc", + "call", + "eth_blockNumber", + "--network", + "base-sepolia", + "--x402", + "--pay-network", + "eip155:84532", + "--asset", + USDC, + "--max-amount", + "10000", + ]; + + // With --receipt: wrapped, and x402 has no settlement reference. + let mut with_receipt = paid_args.to_vec(); + with_receipt.push("--receipt"); + let output = run_qn_subprocess(&server.uri(), home.path(), &with_receipt); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + output.status.success(), + "stderr={}", + String::from_utf8_lossy(&output.stderr) + ); + let v: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert_eq!(v["result"].as_str(), Some("0x1335f9a")); + assert!(v["payment_receipt"].is_null()); + + // Without --receipt: the bare result, byte-identical shape to unpaid. + let output = run_qn_subprocess(&server.uri(), home.path(), &paid_args); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(output.status.success()); + let v: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert_eq!(v.as_str(), Some("0x1335f9a")); +} From 9c878069879fe9622ac1c63cd4f68469721aeaf2 Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Mon, 13 Jul 2026 03:27:34 -0400 Subject: [PATCH 7/8] docs: document the paid RPC lane in the agent guide and README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers the --x402/--mpp surface, the key-resolution ladder (file/stdin flag > QN_PAYMENT_KEY > key_file in config; never a raw key on argv or in config), the per-call spend ceiling, --receipt output shape, the query-chain vs pay-chain distinction, and the payment exit-code semantics: 2 = the gateway refused, 3 = outcome unknown, the payment may have settled — check the wallet before re-running. Notes that paid calls never auto-retry and that config supplies parameters but never activates payment. --- README.md | 52 +++++++++++++++++++++++++++++++++++ src/commands/agent/context.md | 47 +++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/README.md b/README.md index dced139..6011748 100644 --- a/README.md +++ b/README.md @@ -327,6 +327,58 @@ API key, so subsequent calls skip the mint round trip while it's valid. Results schemaless JSON; `-o json|yaml|toon` controls the format (`table`/`md` fall back to JSON). +#### Pay per call with a crypto micropayment + +`--x402` (EVM or Solana stablecoin) or `--mpp` (Tempo) pays for the call per +request instead of using an account API key — no login and no Tooling Access +needed. **This moves real funds** (testnet tokens are still real transfers): +use a dedicated, minimally funded wallet. + +```sh +qn rpc call eth_blockNumber --network base-sepolia --x402 \ + --payment-key-file ~/.keys/payer \ + --pay-network eip155:84532 \ + --asset 0x036CbD53842c5426634e7929541eC2318f3dCF7e \ + --max-amount 10000 + +# MPP settles on Tempo and returns a settlement receipt with --receipt: +qn rpc call eth_blockNumber --network tempo-testnet --mpp --receipt ... +``` + +- `--network` is required and names the chain you *query*, as the payment + gateway's path slug (independent of `--pay-network`, the CAIP-2 chain the + payment *settles* on). +- The private key resolves from `--payment-key-file ` (`-` for stdin), + then the `QN_PAYMENT_KEY` env var, then `key_file` in config. It is never + accepted as a flag value, never stored in config, and never printed. +- `--max-amount` is the per-call spend ceiling in integer base units of the + asset (e.g. `10000` = 0.01 USDC). There is no built-in default; offers above + the ceiling are refused before anything is signed. +- `--receipt` wraps stdout as `{"result": ..., "payment_receipt": ...}` — the + settlement transaction hash on MPP, `null` on x402. Without it, paid output + is shaped exactly like unpaid output. +- Paid calls **never auto-retry** (`--retries` does not apply). Exit code 2 + means the gateway refused; exit 3 means the outcome is unknown and the + payment may have settled — check the wallet before re-running. +- x402/Solana at volume: pass `--svm-rpc-url `; the default public Solana + RPC rate-limits aggressively. + +Store the parameters once in `~/.config/qn/config.toml` and the invocation +shrinks to the scheme flag — config supplies values but never activates +payment by itself: + +```toml +[rpc.payment] +key_file = "/home/me/.config/qn/payment.key" # a path; never the raw key +max_amount = "10000" +pay_network = "eip155:84532" +asset = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" +``` + +```sh +qn rpc call eth_blockNumber --network base-sepolia --x402 +``` + ### Other ```sh diff --git a/src/commands/agent/context.md b/src/commands/agent/context.md index f6cf74a..e4500ce 100644 --- a/src/commands/agent/context.md +++ b/src/commands/agent/context.md @@ -61,6 +61,12 @@ Branch on these — especially **4** and **5**. | 5 | Cancelled, or confirmation required and not granted (see §4). | | 130 | Interrupted (SIGINT). | +On a **paid** `rpc call` (`--x402`/`--mpp`, §6) the 2/3 split carries payment +semantics: **2** means the gateway refused (an unmatched offer settles nothing; +a rejected payment is terminal), while **3** means the outcome is unknown — the +request was sent and **may have been charged**. On exit 3, check the wallet +before re-running; never blind-retry a paid call. + ## 4. Non-interactive & confirmation behavior Destructive commands are gated. On a TTY they prompt `y/N`. To proceed without a @@ -96,6 +102,9 @@ if you need it. nothing — it is read-only and safe to retry. - `qn sql query` is read-only but **does not auto-retry**: a query consumes credits, so a retried query re-bills. `qn sql schema` is a cheap read and retries normally. +- A **paid** `rpc call` (`--x402`/`--mpp`) never auto-retries — `--retries` does + not apply. Each attempt can move funds, and after a lost response the previous + attempt may already have settled (§3, exit 3). ## 6. Command catalog @@ -128,6 +137,20 @@ Top-level nouns (plurals like `endpoints`/`streams` and `ls` are accepted aliase available keys. Custom endpoint: `--endpoint-url ` (or `[rpc] endpoint_url` in config) sends the call to a fully-formed HTTP URL that authenticates itself (no token minted); it's mutually exclusive with `--network`. + **Paid lane**: `--x402` (EVM/Solana stablecoin) or `--mpp` (Tempo) pays for + the call per request with a crypto micropayment instead of an API key — no + login, no Tooling Access. Requires `--network` as the payment gateway's path + slug (e.g. `base-sepolia`; NOT validated by `list-networks`). Parameters: + `--pay-network ` (the chain the payment settles on — independent of + `--network`), `--asset
`, `--max-amount ` (spend ceiling + per call, integer base units, no default), and the private key via + `--payment-key-file ` > `QN_PAYMENT_KEY` env > `key_file` under + `[rpc.payment]` in config (a path — never the raw key). All of these fall + back to `[rpc.payment]`, but config never activates payment by itself: the + scheme flag is always required. `--receipt` wraps stdout as + `{"result": ..., "payment_receipt": ...}` (settlement tx hash on MPP; `null` + on x402); without it the paid output shape is identical to an unpaid call. + Mutually exclusive with `--endpoint-url`. Drill into any level with `--help`: `qn endpoint --help`, `qn endpoint security --help`, `qn endpoint rate-limit --help`. Shell completions: `qn completions `. @@ -197,9 +220,33 @@ qn rpc call eth_blockNumber --endpoint-url https://my-endpoint.example/rpc is enabling Tooling Access (or pass `--yes` to enable on first use). A custom `--endpoint-url` (or `[rpc] endpoint_url` in config) bypasses that entirely. +**Pay per call with a crypto micropayment (no API key, no login):** + +```sh +# One-time: store the wallet parameters (never the raw key) in config +cat >> ~/.config/qn/config.toml <<'EOF' +[rpc.payment] +key_file = "/home/me/.config/qn/payment.key" # file holding the raw key; chmod 600 +max_amount = "10000" # spend ceiling per call, base units +pay_network = "eip155:84532" # chain the payment settles on (CAIP-2) +asset = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" +EOF + +qn rpc call eth_blockNumber --network base-sepolia --x402 # pays, prints the result +qn rpc call eth_blockNumber --network tempo-testnet --mpp --receipt # + settlement tx hash +``` + +This moves real funds (even testnet tokens are real transfers) — use a +dedicated, minimally funded wallet. The spend ceiling bounds each call; there +is no built-in default. + ## 8. Gotchas & safety rails - Mutations are never retried; re-running a failed create can double-provision (§5). +- Paid `rpc call` moves real funds and never auto-retries; exit 3 means the + payment may have settled — check the wallet before re-running (§3, §5). The + CLI never prints the payment key; keep it in a file or `QN_PAYMENT_KEY`, not + in config or argv. - No account-wide wipe command exists by design (§4). - Piped output defaults to `json`; pass `-o toon` for the compact LLM form (§2). - `--base-url` overrides the API host; it exists for testing. From 8ae231c644e67d54009a3ae5ef5625da9e82063e Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Mon, 13 Jul 2026 20:36:57 -0400 Subject: [PATCH 8/8] feat(rpc): accept network names for --pay-network and refine paid-lane errors Adds human-readable names to the paid RPC lane: --pay-network (and pay_network in [rpc.payment]) now takes a Quicknode network name like base-sepolia, solana-devnet, or tempo-testnet, resolved to CAIP-2 before reaching the SDK. Raw CAIP-2 ids still pass through verbatim, so every chain stays reachable without a table entry. EVM chain ids are verified against the public registry at chainid.network; names that could not be verified are deliberately absent, since a wrong id is worse than an error pointing at the CAIP-2 escape hatch. Also sharpens paid-lane error semantics: a gateway 5xx on the paid resend is wrapped as PaymentMaybeCharged (exit 3, payment submitted, check the wallet), so exit 2 (PaymentRejected 4xx / PaymentUnsupported) always and only means the gateway refused without settling. Messages reworded to match. 9 new tests: 5 unit on the resolver, 3 unit on payment config resolution, 2 integration (name matches a CAIP-2 offer end-to-end; unknown name fails preflight with zero requests sent); MPP flow with --pay-network tempo-testnet verified against the live gateway. --- README.md | 24 ++-- src/commands/agent/context.md | 25 ++-- src/commands/rpc/mod.rs | 11 +- src/commands/rpc/pay_network.rs | 187 ++++++++++++++++++++++++++ src/commands/rpc/payment.rs | 78 +++++++++-- src/config.rs | 10 +- src/errors.rs | 32 +++-- tests/rpc_payment.rs | 230 ++++++++++++++++++++++++++++++-- 8 files changed, 539 insertions(+), 58 deletions(-) create mode 100644 src/commands/rpc/pay_network.rs diff --git a/README.md b/README.md index 6011748..2d4d2be 100644 --- a/README.md +++ b/README.md @@ -337,7 +337,7 @@ use a dedicated, minimally funded wallet. ```sh qn rpc call eth_blockNumber --network base-sepolia --x402 \ --payment-key-file ~/.keys/payer \ - --pay-network eip155:84532 \ + --pay-network base-sepolia \ --asset 0x036CbD53842c5426634e7929541eC2318f3dCF7e \ --max-amount 10000 @@ -346,20 +346,26 @@ qn rpc call eth_blockNumber --network tempo-testnet --mpp --receipt ... ``` - `--network` is required and names the chain you *query*, as the payment - gateway's path slug (independent of `--pay-network`, the CAIP-2 chain the - payment *settles* on). + gateway's path slug (independent of `--pay-network`, the chain the payment + *settles* on). +- `--pay-network` takes a Quicknode network name (`base-sepolia`, + `solana-devnet`, `tempo-testnet`, ...) or a raw CAIP-2 id (`eip155:84532`, + `solana:EtWTRA...`). Anything containing a `:` is passed through verbatim, + so any `eip155:` works even without a named entry. - The private key resolves from `--payment-key-file ` (`-` for stdin), then the `QN_PAYMENT_KEY` env var, then `key_file` in config. It is never accepted as a flag value, never stored in config, and never printed. - `--max-amount` is the per-call spend ceiling in integer base units of the asset (e.g. `10000` = 0.01 USDC). There is no built-in default; offers above the ceiling are refused before anything is signed. -- `--receipt` wraps stdout as `{"result": ..., "payment_receipt": ...}` — the - settlement transaction hash on MPP, `null` on x402. Without it, paid output - is shaped exactly like unpaid output. +- `--receipt` wraps stdout as `{"result": ..., "payment_receipt": ...}`. On + MPP the receipt is an object (`method`, `status`, `timestamp`, and + `reference` — the settlement transaction hash); on x402 it is `null`. + Without it, paid output is shaped exactly like unpaid output. - Paid calls **never auto-retry** (`--retries` does not apply). Exit code 2 - means the gateway refused; exit 3 means the outcome is unknown and the - payment may have settled — check the wallet before re-running. + means the gateway refused and nothing settled; exit 3 means the outcome is + unknown — the payment was submitted and may have settled; check the wallet + before re-running. - x402/Solana at volume: pass `--svm-rpc-url `; the default public Solana RPC rate-limits aggressively. @@ -371,7 +377,7 @@ payment by itself: [rpc.payment] key_file = "/home/me/.config/qn/payment.key" # a path; never the raw key max_amount = "10000" -pay_network = "eip155:84532" +pay_network = "base-sepolia" # network name or CAIP-2 id asset = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" ``` diff --git a/src/commands/agent/context.md b/src/commands/agent/context.md index e4500ce..c2480cd 100644 --- a/src/commands/agent/context.md +++ b/src/commands/agent/context.md @@ -62,10 +62,12 @@ Branch on these — especially **4** and **5**. | 130 | Interrupted (SIGINT). | On a **paid** `rpc call` (`--x402`/`--mpp`, §6) the 2/3 split carries payment -semantics: **2** means the gateway refused (an unmatched offer settles nothing; -a rejected payment is terminal), while **3** means the outcome is unknown — the -request was sent and **may have been charged**. On exit 3, check the wallet -before re-running; never blind-retry a paid call. +semantics: **2** means the gateway refused and nothing settled (an unmatched +or unreadable offer, or a 4xx-refused payment), while **3** means the outcome +is unknown — the payment was submitted and **may have been charged** (a +gateway 5xx after the paid resend, a lost response, or an uninterpretable +post-payment response). On exit 3, check the wallet before re-running; never +blind-retry a paid call. ## 4. Non-interactive & confirmation behavior @@ -141,15 +143,18 @@ Top-level nouns (plurals like `endpoints`/`streams` and `ls` are accepted aliase the call per request with a crypto micropayment instead of an API key — no login, no Tooling Access. Requires `--network` as the payment gateway's path slug (e.g. `base-sepolia`; NOT validated by `list-networks`). Parameters: - `--pay-network ` (the chain the payment settles on — independent of - `--network`), `--asset
`, `--max-amount ` (spend ceiling + `--pay-network ` (the chain the payment settles on — independent of + `--network`; a network name like `base-sepolia`/`solana-devnet`/`tempo-testnet`, + or a raw CAIP-2 id like `eip155:84532`, which always passes through + verbatim), `--asset
`, `--max-amount ` (spend ceiling per call, integer base units, no default), and the private key via `--payment-key-file ` > `QN_PAYMENT_KEY` env > `key_file` under `[rpc.payment]` in config (a path — never the raw key). All of these fall back to `[rpc.payment]`, but config never activates payment by itself: the scheme flag is always required. `--receipt` wraps stdout as - `{"result": ..., "payment_receipt": ...}` (settlement tx hash on MPP; `null` - on x402); without it the paid output shape is identical to an unpaid call. + `{"result": ..., "payment_receipt": ...}` (on MPP an object whose + `reference` is the settlement tx hash; `null` on x402); without it the paid + output shape is identical to an unpaid call. Mutually exclusive with `--endpoint-url`. Drill into any level with `--help`: `qn endpoint --help`, `qn endpoint security --help`, @@ -228,12 +233,12 @@ cat >> ~/.config/qn/config.toml <<'EOF' [rpc.payment] key_file = "/home/me/.config/qn/payment.key" # file holding the raw key; chmod 600 max_amount = "10000" # spend ceiling per call, base units -pay_network = "eip155:84532" # chain the payment settles on (CAIP-2) +pay_network = "base-sepolia" # settlement chain: network name or CAIP-2 id asset = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" EOF qn rpc call eth_blockNumber --network base-sepolia --x402 # pays, prints the result -qn rpc call eth_blockNumber --network tempo-testnet --mpp --receipt # + settlement tx hash +qn rpc call eth_blockNumber --network tempo-testnet --mpp --receipt # + settlement receipt ``` This moves real funds (even testnet tokens are real transfers) — use a diff --git a/src/commands/rpc/mod.rs b/src/commands/rpc/mod.rs index e43053f..a6af2f9 100644 --- a/src/commands/rpc/mod.rs +++ b/src/commands/rpc/mod.rs @@ -24,6 +24,7 @@ //! branches off before any of this module's token-cache or Tooling Access //! machinery runs. +mod pay_network; mod payment; use std::io::Read; @@ -60,7 +61,7 @@ pub enum RpcCmd { Paid (crypto micropayment, no API key; params from [rpc.payment] in config):\n \ qn rpc call eth_blockNumber --network base-sepolia --x402\n \ qn rpc call eth_blockNumber --network base-sepolia --x402 \\\n \ - --payment-key-file ~/.keys/payer --pay-network eip155:84532 \\\n \ + --payment-key-file ~/.keys/payer --pay-network base-sepolia \\\n \ --asset 0x036C... --max-amount 10000\n \ qn rpc call eth_blockNumber --network tempo-testnet --mpp --receipt")] Call(Box), @@ -142,12 +143,12 @@ pub struct CallArgs { )] pub max_amount: Option, - /// CAIP-2 id of the chain you PAY on (e.g. `eip155:84532`), independent - /// of --network (the chain you query). Falls back to `pay_network` under - /// [rpc.payment]. + /// Chain you PAY on — a network name (e.g. `base-sepolia`) or CAIP-2 id + /// (e.g. `eip155:84532`) — independent of --network (the chain you + /// query). Falls back to `pay_network` under [rpc.payment]. #[arg( long, - value_name = "CAIP2", + value_name = "NETWORK", requires = "payment", help_heading = "Payment" )] diff --git a/src/commands/rpc/pay_network.rs b/src/commands/rpc/pay_network.rs new file mode 100644 index 0000000..48067c7 --- /dev/null +++ b/src/commands/rpc/pay_network.rs @@ -0,0 +1,187 @@ +//! Human-readable names for `--pay-network`. +//! +//! The SDK's `PaymentConfig.pay_network` is a canonical CAIP-2 id — that is +//! the form the x402/MPP offers are matched against. This module is the CLI's +//! presentation layer on top: it accepts the same Quicknode network-name +//! vocabulary as `--network` (e.g. `base-sepolia`) and resolves it to CAIP-2 +//! before the value reaches the SDK. Anything containing a `:` is treated as +//! an already-canonical CAIP-2 id and passed through verbatim, so every chain +//! is reachable even when it has no entry in the table. +//! +//! EVM chain ids are verified against the public EVM chain registry +//! (chainid.network, the dataset behind chainlist.org). Solana ids are the +//! standard CAIP-2 genesis-hash prefixes. Names whose chain id could not be +//! confirmed against a public source are deliberately absent — a wrong id +//! could match a payment offer on the wrong chain, while a missing name just +//! errors with the CAIP-2 escape hatch. + +use crate::errors::CliError; + +/// Quicknode network name → CAIP-2 pay-network id. Sorted by name (binary +/// searched); a unit test enforces order and uniqueness. +const PAY_NETWORKS: &[(&str, &str)] = &[ + ("0g-galileo", "eip155:16601"), + ("0g-mainnet", "eip155:16661"), + ("abstract-mainnet", "eip155:2741"), + ("abstract-testnet", "eip155:11124"), + ("arbitrum-mainnet", "eip155:42161"), + ("arbitrum-sepolia", "eip155:421614"), + ("ault-mainnet", "eip155:904"), + ("ault-testnet", "eip155:10904"), + ("avalanche-mainnet", "eip155:43114"), + ("avalanche-testnet", "eip155:43113"), + ("b3-mainnet", "eip155:8333"), + ("base-mainnet", "eip155:8453"), + ("base-sepolia", "eip155:84532"), + ("bera-bepolia", "eip155:80069"), + ("bera-mainnet", "eip155:80094"), + ("blast-mainnet", "eip155:81457"), + ("blast-sepolia", "eip155:168587773"), + ("bsc", "eip155:56"), + ("bsc-testnet", "eip155:97"), + ("celo-mainnet", "eip155:42220"), + ("cyber-mainnet", "eip155:7560"), + ("ethereum-hoodi", "eip155:560048"), + ("ethereum-mainnet", "eip155:1"), + ("ethereum-sepolia", "eip155:11155111"), + ("fantom", "eip155:250"), + ("flare-coston2", "eip155:114"), + ("flare-mainnet", "eip155:14"), + ("fluent-mainnet", "eip155:25363"), + ("fraxtal-mainnet", "eip155:252"), + ("gravity-alpham", "eip155:1625"), + ("hedera-mainnet", "eip155:295"), + ("hedera-testnet", "eip155:296"), + ("hemi-mainnet", "eip155:43111"), + ("hemi-testnet", "eip155:743111"), + // 999 is HyperEVM per Hyperliquid's docs; the chain registry still lists + // it under a stale earlier registration. + ("hype-mainnet", "eip155:999"), + ("hype-testnet", "eip155:998"), + ("injective-mainnet", "eip155:1776"), + ("injective-testnet", "eip155:1439"), + ("ink-mainnet", "eip155:57073"), + ("ink-sepolia", "eip155:763373"), + ("joc-mainnet", "eip155:81"), + ("kaia-kairos", "eip155:1001"), + ("kaia-mainnet", "eip155:8217"), + ("katana-mainnet", "eip155:747474"), + ("linea-mainnet", "eip155:59144"), + ("lisk-mainnet", "eip155:1135"), + ("mantle-mainnet", "eip155:5000"), + ("mantle-sepolia", "eip155:5003"), + ("megaeth-mainnet", "eip155:4326"), + ("moca-testnet", "eip155:5151"), + ("mode-mainnet", "eip155:34443"), + ("monad-mainnet", "eip155:143"), + ("monad-testnet", "eip155:10143"), + ("morph-mainnet", "eip155:2818"), + ("nova-mainnet", "eip155:42170"), + ("optimism", "eip155:10"), + ("optimism-sepolia", "eip155:11155420"), + ("peaq-mainnet", "eip155:3338"), + ("plasma-mainnet", "eip155:9745"), + ("plasma-testnet", "eip155:9746"), + ("polygon", "eip155:137"), + ("polygon-amoy", "eip155:80002"), + ("robinhood-mainnet", "eip155:4663"), + ("robinhood-testnet", "eip155:46630"), + ("sahara-testnet", "eip155:313313"), + ("scroll-mainnet", "eip155:534352"), + ("scroll-testnet", "eip155:534351"), + ("sei-atlantic", "eip155:1328"), + ("sei-pacific", "eip155:1329"), + ("solana-devnet", "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"), + ("solana-mainnet", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"), + ("solana-testnet", "solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z"), + ("soneium-mainnet", "eip155:1868"), + ("sonic-mainnet", "eip155:146"), + ("story-aeneid", "eip155:1315"), + ("story-mainnet", "eip155:1514"), + ("tempo-mainnet", "eip155:4217"), + ("tempo-testnet", "eip155:42431"), + ("unichain-mainnet", "eip155:130"), + ("unichain-sepolia", "eip155:1301"), + ("vana-mainnet", "eip155:1480"), + ("vana-moksha", "eip155:14800"), + ("worldchain-mainnet", "eip155:480"), + ("worldchain-sepolia", "eip155:4801"), + ("xdai", "eip155:100"), + ("xlayer-mainnet", "eip155:196"), + ("xlayer-testnet", "eip155:195"), + ("xrplevm-mainnet", "eip155:1440000"), + ("xrplevm-testnet", "eip155:1449000"), + ("zksync-mainnet", "eip155:324"), + ("zksync-sepolia", "eip155:300"), + ("zora-mainnet", "eip155:7777777"), +]; + +/// Resolves a `--pay-network` / config `pay_network` value to CAIP-2. Values +/// containing `:` pass through verbatim (Solana genesis-hash references are +/// case-sensitive, so no normalization is applied to them). +pub(super) fn resolve(input: &str) -> Result { + if input.contains(':') { + return Ok(input.to_string()); + } + let name = input.to_ascii_lowercase(); + match PAY_NETWORKS.binary_search_by_key(&name.as_str(), |(n, _)| n) { + Ok(i) => Ok(PAY_NETWORKS[i].1.to_string()), + Err(_) => Err(CliError::Arg(format!( + "unknown pay network '{input}'. Use a Quicknode network name \ + (e.g. base-sepolia, solana-devnet, tempo-testnet) or a raw \ + CAIP-2 id (e.g. eip155:84532) — any eip155: or \ + solana: is accepted as-is" + ))), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn table_is_sorted_and_unique() { + for pair in PAY_NETWORKS.windows(2) { + assert!( + pair[0].0 < pair[1].0, + "PAY_NETWORKS out of order or duplicated at '{}'", + pair[1].0 + ); + } + } + + #[test] + fn resolves_network_names() { + assert_eq!(resolve("base-sepolia").unwrap(), "eip155:84532"); + assert_eq!(resolve("xdai").unwrap(), "eip155:100"); + assert_eq!(resolve("tempo-testnet").unwrap(), "eip155:42431"); + assert_eq!( + resolve("solana-devnet").unwrap(), + "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" + ); + } + + #[test] + fn name_lookup_is_case_insensitive() { + assert_eq!(resolve("Base-Sepolia").unwrap(), "eip155:84532"); + } + + #[test] + fn caip2_passes_through_verbatim() { + assert_eq!(resolve("eip155:84532").unwrap(), "eip155:84532"); + // Unknown-to-the-table but well-formed ids still work, unchanged. + assert_eq!( + resolve("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1").unwrap(), + "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" + ); + assert_eq!(resolve("eip155:424242").unwrap(), "eip155:424242"); + } + + #[test] + fn unknown_name_errors_with_escape_hatch() { + let err = resolve("morph-hoodi").unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("morph-hoodi"), "got: {msg}"); + assert!(msg.contains("eip155:"), "got: {msg}"); + } +} diff --git a/src/commands/rpc/payment.rs b/src/commands/rpc/payment.rs index aa43fb7..ae4d214 100644 --- a/src/commands/rpc/payment.rs +++ b/src/commands/rpc/payment.rs @@ -173,11 +173,13 @@ fn resolve_payment_config( .or_else(|| section.pay_network.clone()) .ok_or_else(|| { CliError::Arg( - "no pay network set. Pass --pay-network (e.g. \ - eip155:84532) or set `pay_network` under [rpc.payment]" + "no pay network set. Pass --pay-network (a network \ + name like base-sepolia, or a CAIP-2 id like eip155:84532) \ + or set `pay_network` under [rpc.payment]" .to_string(), ) })?; + let pay_network = super::pay_network::resolve(&pay_network)?; let asset = args .asset @@ -290,15 +292,25 @@ fn checked_key(raw: String, source: &str) -> Result { Ok(trimmed.to_string()) } -/// Maps a paid-call failure. A `Decode` error on the paid lane means the -/// gateway's post-payment response could not be interpreted — the payment may -/// already have settled, so it gets the same never-blindly-retry treatment as -/// `PaymentIndeterminate` (exit 3) instead of rendering as a generic decode -/// failure. Everything else passes through to the normal SDK mapping. +/// Maps a paid-call failure onto the paid exit-code contract: 2 = the gateway +/// refused and nothing settled, 3 = outcome unknown, check the wallet. +/// +/// - `Decode` on the paid lane always means the gateway's post-payment 2xx +/// response could not be interpreted (the SDK classifies pre-payment parse +/// failures as `PaymentUnsupported`/`Config`) — the payment may already +/// have settled, so it gets the same never-blindly-retry treatment as +/// `PaymentIndeterminate` (exit 3). +/// - `PaymentRejected` with a 5xx status is a gateway/settlement failure +/// after the signed payment was submitted — also unknown, exit 3. A 4xx +/// rejection means the gateway refused the credential without settling it +/// and passes through to exit 2. fn map_paid_error(e: SdkError) -> CliError { - match e { + match &e { SdkError::Decode { .. } => CliError::PaymentMaybeCharged(e), - other => other.into(), + SdkError::PaymentRejected { status, .. } if *status >= 500 => { + CliError::PaymentMaybeCharged(e) + } + _ => e.into(), } } @@ -350,6 +362,36 @@ mod tests { assert_eq!(network, "base-sepolia"); } + #[test] + fn pay_network_name_resolves_to_caip2() { + let mut args = paid_args(true); + args.pay_network = Some("base-sepolia".to_string()); + let (cfg, _, _) = + resolve_payment_config(&args, &empty_section(), Some("k".to_string()), None).unwrap(); + assert_eq!(cfg.pay_network, "eip155:84532"); + } + + #[test] + fn config_pay_network_name_resolves_too() { + let mut section = empty_section(); + section.pay_network = Some("solana-devnet".to_string()); + let mut args = paid_args(true); + args.pay_network = None; + let (cfg, _, _) = + resolve_payment_config(&args, §ion, Some("k".to_string()), None).unwrap(); + assert_eq!(cfg.pay_network, "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"); + } + + #[test] + fn unknown_pay_network_name_is_an_arg_error() { + let mut args = paid_args(true); + args.pay_network = Some("btc".to_string()); + let err = resolve_payment_config(&args, &empty_section(), Some("k".to_string()), None) + .unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("unknown pay network 'btc'"), "got: {msg}"); + } + #[test] fn mpp_flag_selects_mpp_scheme() { let args = paid_args(false); @@ -559,4 +601,22 @@ mod tests { let mapped = map_paid_error(rejected); assert_eq!(crate::errors::exit_code_for(&mapped), 2); } + + #[test] + fn rejected_5xx_maps_to_payment_maybe_charged() { + // A settlement failure after the signed payment was submitted: the + // outcome is unknown, so it must NOT land in the "refused" bucket. + for status in [500, 502, 503] { + let rejected = SdkError::PaymentRejected { + status, + body: "settlement error".to_string(), + }; + let mapped = map_paid_error(rejected); + assert!( + matches!(mapped, CliError::PaymentMaybeCharged(_)), + "status {status} mapped to {mapped:?}" + ); + assert_eq!(crate::errors::exit_code_for(&mapped), 3); + } + } } diff --git a/src/config.rs b/src/config.rs index d9de3b2..414aec5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,7 +7,10 @@ //! There is deliberately no environment-variable source: a key left exported //! in a shell is invisible state that outlives the session it was set for, //! and is the easiest way to run a destructive command against the wrong -//! account. +//! account. (`QN_PAYMENT_KEY`, read by the paid RPC lane, is the one +//! deliberate exception: it never selects an account, so a stale value +//! cannot aim a mutating command at the wrong one — see +//! `commands::rpc::payment`.) //! //! When both sources fail we return [`CliError::NoApiKey`] which exits 4 with //! a message directing the user to `qn auth login`. The `qn auth login` @@ -91,8 +94,9 @@ pub struct PaymentSection { /// Accepts a TOML string or integer. #[serde(default, deserialize_with = "de_opt_string_or_int")] pub max_amount: Option, - /// CAIP-2 id of the chain payments settle on, e.g. `eip155:84532`. - /// Independent of `--network` (the chain the RPC call queries). + /// Chain payments settle on: a Quicknode network name (e.g. + /// `base-sepolia`) or a CAIP-2 id (e.g. `eip155:84532`). Independent of + /// `--network` (the chain the RPC call queries). #[serde(default)] pub pay_network: Option, /// Token to pay with: EVM contract address or Solana mint. diff --git a/src/errors.rs b/src/errors.rs index b8bd29a..d732920 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -66,10 +66,11 @@ pub enum CliError { /// - 1: generic CLI failure (arg parse, IO, decode). clap usage errors are /// mapped to 1 in main.rs too, so 2 always and only means an API error. /// - 2: SdkError::Api (server returned a non-2xx); also a payment the gateway -/// refused or never matched (PaymentRejected / PaymentUnsupported) +/// refused without settling (PaymentRejected 4xx / PaymentUnsupported — +/// the paid lane wraps 5xx rejections into PaymentMaybeCharged first) /// - 3: SdkError::Http (network failure); also an unknown payment outcome -/// (PaymentIndeterminate / PaymentMaybeCharged — request sent, response -/// lost, the caller may have been charged) +/// (PaymentIndeterminate / PaymentMaybeCharged — the payment was +/// submitted, the caller may have been charged) /// - 4: NoApiKey / BadConfig /// - 5: user cancelled or needs --yes pub fn exit_code_for(err: &CliError) -> i32 { @@ -109,9 +110,13 @@ pub fn render_with_argv(err: &CliError, verbose: bool, argv: &[String]) -> Strin ) } CliError::Sdk(SdkError::PaymentRejected { status, body }) => { + // Only 4xx rejections reach this arm from the paid lane (5xx are + // wrapped into PaymentMaybeCharged): the gateway refused the + // credential without settling it. let msg = format!( - "Error: the gateway rejected the submitted payment (HTTP {status}). \ - A signed payment was sent — check your wallet before retrying." + "Error: the gateway refused the payment (HTTP {status}). The signed \ + payment was not accepted, so nothing should have settled; check the \ + wallet balance and --pay-network/--asset/--max-amount, then retry." ); if verbose && !body.is_empty() { format!("{msg}\n{body}") @@ -126,9 +131,9 @@ pub fn render_with_argv(err: &CliError, verbose: bool, argv: &[String]) -> Strin .to_string() } CliError::PaymentMaybeCharged(source) => { - let msg = "Error: the paid request returned an unexpected response — the \ - request may have been settled; check your wallet before retrying. \ - Do not blindly re-run this command."; + let msg = "Error: the paid request failed after the payment was submitted — \ + the payment may have been settled; check your wallet before \ + retrying. Do not blindly re-run this command."; if verbose { format!("{msg}\n{source}") } else { @@ -525,8 +530,10 @@ mod tests { #[test] fn exit_code_payment_refusals_are_2() { - // The gateway said no and nothing settled (unsupported) or the refusal - // is terminal (rejected): the API-error bucket. + // The gateway said no and nothing settled: an unmatched/unreadable + // offer (unsupported) or a 4xx-refused credential (rejected). The + // paid lane wraps 5xx rejections into PaymentMaybeCharged before + // this mapping ever sees them. let unsupported = CliError::Sdk(SdkError::PaymentUnsupported { offered: "eip155:84532/0xabc amount 999999".to_string(), }); @@ -560,14 +567,15 @@ mod tests { } #[test] - fn renders_payment_rejected_with_wallet_warning() { + fn renders_payment_rejected_as_refused_without_settling() { let err = CliError::Sdk(SdkError::PaymentRejected { status: 402, body: "invalid signature".to_string(), }); let msg = render(&err, false); assert!(msg.contains("402"), "got: {msg}"); - assert!(msg.contains("check your wallet"), "got: {msg}"); + assert!(msg.contains("refused"), "got: {msg}"); + assert!(msg.contains("nothing should have settled"), "got: {msg}"); assert!(!msg.contains("invalid signature"), "got: {msg}"); let verbose = render(&err, true); assert!(verbose.contains("invalid signature"), "got: {verbose}"); diff --git a/tests/rpc_payment.rs b/tests/rpc_payment.rs index fd7bcf3..fa96126 100644 --- a/tests/rpc_payment.rs +++ b/tests/rpc_payment.rs @@ -49,16 +49,27 @@ fn x402_accepts_entry(amount: &str) -> serde_json::Value { /// Sequenced gateway responder: the first (unpaid) POST gets a 402 with a /// one-entry menu at `amount`; any request carrying a payment signature gets -/// the JSON-RPC result. +/// the `paid` response (the JSON-RPC result by default). struct X402Seq { amount: &'static str, + paid: ResponseTemplate, calls: AtomicUsize, } impl X402Seq { fn new(amount: &'static str) -> Self { + Self::with_paid_response( + amount, + ResponseTemplate::new(200).set_body_json(json!({ + "jsonrpc": "2.0", "id": 1, "result": "0x1335f9a" + })), + ) + } + + fn with_paid_response(amount: &'static str, paid: ResponseTemplate) -> Self { Self { amount, + paid, calls: AtomicUsize::new(0), } } @@ -74,9 +85,7 @@ impl Respond for X402Seq { "accepts": [ x402_accepts_entry(self.amount) ] })) } else { - ResponseTemplate::new(200).set_body_json(json!({ - "jsonrpc": "2.0", "id": 1, "result": "0x1335f9a" - })) + self.paid.clone() } } } @@ -141,6 +150,44 @@ async fn x402_happy_path_pays_and_bypasses_control_plane() { ); } +#[tokio::test] +async fn pay_network_name_matches_caip2_offer() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/base-sepolia")) + .respond_with(X402Seq::new("1000")) + .expect(2) + .mount(&server) + .await; + mount_control_plane_expect_zero(&server).await; + + let (_guard, key_path) = key_file(); + + // `base-sepolia` resolves to eip155:84532 before reaching the SDK, so it + // matches the mock gateway's CAIP-2 offer exactly. + let out = run_qn( + &server.uri(), + &[ + "rpc", + "call", + "eth_blockNumber", + "--network", + "base-sepolia", + "--x402", + "--payment-key-file", + &key_path, + "--pay-network", + "base-sepolia", + "--asset", + USDC, + "--max-amount", + "10000", + ], + ) + .await; + assert_eq!(out.exit_code, 0, "stderr={}", out.stderr); +} + #[tokio::test] async fn paid_call_works_keyless_with_config_params() { let server = MockServer::start().await; @@ -361,10 +408,11 @@ async fn paid_lane_is_never_retried() { } #[tokio::test] -async fn payment_rejected_on_paid_resend_exits_2_with_wallet_warning() { +async fn payment_rejected_on_paid_resend_exits_2_as_refused() { let server = MockServer::start().await; - // 402 menu, then the paid resend is refused with another 402: a signed - // payment went out, so the message must say "check your wallet". + // 402 menu, then the paid resend is refused with another 402: the gateway + // refused the credential without settling it — exit 2, "refused", and no + // unknown-outcome language. Mock::given(method("POST")) .and(path("/base-sepolia")) .respond_with(ResponseTemplate::new(402).set_body_json(json!({ @@ -397,6 +445,56 @@ async fn payment_rejected_on_paid_resend_exits_2_with_wallet_warning() { ) .await; assert_eq!(out.exit_code, 2, "stderr={}", out.stderr); + assert!(out.stderr.contains("refused"), "stderr={}", out.stderr); + assert!( + out.stderr.contains("nothing should have settled"), + "stderr={}", + out.stderr + ); + assert!( + !out.stderr.contains("may have been settled"), + "a 4xx refusal must not carry unknown-outcome language: {}", + out.stderr + ); +} + +#[tokio::test] +async fn settlement_5xx_on_paid_resend_exits_3_check_wallet() { + let server = MockServer::start().await; + // 402 menu, then the paid resend dies with a 500: the signed payment was + // submitted and the outcome is unknown — exit 3, check-your-wallet. + Mock::given(method("POST")) + .and(path("/base-sepolia")) + .respond_with(X402Seq::with_paid_response( + "1000", + ResponseTemplate::new(500).set_body_string("settlement error"), + )) + .expect(2) + .mount(&server) + .await; + + let (_guard, key_path) = key_file(); + let out = run_qn( + &server.uri(), + &[ + "rpc", + "call", + "eth_blockNumber", + "--network", + "base-sepolia", + "--x402", + "--payment-key-file", + &key_path, + "--pay-network", + "eip155:84532", + "--asset", + USDC, + "--max-amount", + "10000", + ], + ) + .await; + assert_eq!(out.exit_code, 3, "stderr={}", out.stderr); assert!( out.stderr.contains("check your wallet"), "stderr={}", @@ -404,6 +502,93 @@ async fn payment_rejected_on_paid_resend_exits_2_with_wallet_warning() { ); } +#[tokio::test] +async fn unparseable_paid_response_exits_3_check_wallet() { + let server = MockServer::start().await; + // The paid resend returns 200 with a body that isn't JSON: the payment + // was submitted (and likely settled) but the result is uninterpretable — + // exit 3, never a generic decode error. + Mock::given(method("POST")) + .and(path("/base-sepolia")) + .respond_with(X402Seq::with_paid_response( + "1000", + ResponseTemplate::new(200).set_body_string("ok?"), + )) + .expect(2) + .mount(&server) + .await; + + let (_guard, key_path) = key_file(); + let out = run_qn( + &server.uri(), + &[ + "rpc", + "call", + "eth_blockNumber", + "--network", + "base-sepolia", + "--x402", + "--payment-key-file", + &key_path, + "--pay-network", + "eip155:84532", + "--asset", + USDC, + "--max-amount", + "10000", + ], + ) + .await; + assert_eq!(out.exit_code, 3, "stderr={}", out.stderr); + assert!( + out.stderr.contains("may have been settled"), + "stderr={}", + out.stderr + ); +} + +#[tokio::test] +async fn malformed_challenge_menu_exits_2_nothing_charged() { + let server = MockServer::start().await; + // The 402 challenge body is not JSON. Nothing has been signed or paid, so + // this must exit 2 with "Nothing was charged" — never the exit-3 + // check-your-wallet path — after exactly one request. + Mock::given(method("POST")) + .and(path("/base-sepolia")) + .respond_with(ResponseTemplate::new(402).set_body_string("menu?")) + .expect(1) + .mount(&server) + .await; + + let (_guard, key_path) = key_file(); + let out = run_qn( + &server.uri(), + &[ + "rpc", + "call", + "eth_blockNumber", + "--network", + "base-sepolia", + "--x402", + "--payment-key-file", + &key_path, + "--pay-network", + "eip155:84532", + "--asset", + USDC, + "--max-amount", + "10000", + ], + ) + .await; + assert_eq!(out.exit_code, 2, "stderr={}", out.stderr); + assert!( + out.stderr.contains("Nothing was charged"), + "stderr={}", + out.stderr + ); +} + // ── pre-flight failures: fail fast, zero requests ──────────────────────────── /// Runs a paid invocation expected to die in pre-flight: any request reaching @@ -448,6 +633,31 @@ async fn missing_network_fails_before_any_request() { .await; } +#[tokio::test] +async fn unknown_pay_network_name_fails_before_any_request() { + let (_guard, key_path) = key_file(); + expect_preflight_error( + &[ + "rpc", + "call", + "eth_blockNumber", + "--network", + "base-sepolia", + "--x402", + "--payment-key-file", + &key_path, + "--pay-network", + "not-a-chain", + "--asset", + USDC, + "--max-amount", + "10000", + ], + "unknown pay network 'not-a-chain'", + ) + .await; +} + #[tokio::test] async fn missing_key_fails_before_any_request() { // No flag, no env in-process, and a config dir with no key_file. @@ -665,17 +875,17 @@ async fn receipt_flag_wraps_stdout_on_mpp() { let request = b64url(json!({ "amount": "1000", "currency": "0x20c0000000000000000000000000000000000000", - "recipient": "0xfd24114c3981aba78ae2441991b1bdb89329c556", + "recipient": "0x000000000000000000000000000000000000bEEF", "methodDetails": { "chainId": 42431, "feePayer": true } })); let www = format!( - "Payment id=\"c1\", realm=\"mpp.quicknode.com\", method=\"tempo\", \ + "Payment id=\"c1\", realm=\"mpp.example.com\", method=\"tempo\", \ intent=\"charge\", description=\"d\", expires=\"2099-01-01T00:00:00Z\", \ request=\"{request}\"" ); let receipt = b64url(json!({ "method": "tempo", "status": "success", - "timestamp": "2026-07-13T02:05:10.119Z", + "timestamp": "2026-01-01T00:00:00Z", "reference": "0xdeadbeef" }));