diff --git a/Cargo.lock b/Cargo.lock index ce8a2838..f744fc2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,7 +27,7 @@ dependencies = [ "getrandom 0.2.15", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -56,9 +56,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.96" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "arbitrary" @@ -132,6 +132,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "async-channel" version = "1.9.0" @@ -145,9 +151,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310c9bcae737a48ef5cdee3174184e6d548b292739ede61a1f955ef76a738861" +checksum = "c0cf008e5e1a9e9e22a7d3c9a4992e21a350290069e36d8fb72304ed17e8f2d2" dependencies = [ "brotli", "flate2", @@ -243,9 +249,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] name = "basic_solana" @@ -256,6 +262,7 @@ dependencies = [ "bs58", "candid", "getrandom 0.2.15", + "http 1.3.1", "ic-cdk", "ic-ed25519", "num 0.4.3", @@ -347,18 +354,18 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" dependencies = [ "serde", ] [[package]] name = "blake3" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1230237285e3e10cde447185e8975408ae24deaa67205ce684805c25bc0c7937" +checksum = "675f87afced0413c9bb02843499dbbd3882a237645883f71a2b59644a6d2f753" dependencies = [ "arrayref", "arrayvec 0.7.6", @@ -366,7 +373,6 @@ dependencies = [ "cfg-if", "constant_time_eq", "digest 0.10.7", - "memmap2", ] [[package]] @@ -427,7 +433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8b668d39970baad5356d7c83a86fee3a539e6f93bf6764c97368243e17a0487" dependencies = [ "once_cell", - "proc-macro-crate 3.2.0", + "proc-macro-crate 3.3.0", "proc-macro2", "quote", "syn 2.0.100", @@ -503,9 +509,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.21.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" [[package]] name = "bytemuck_derive" @@ -526,9 +532,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "camino" @@ -593,6 +599,23 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "canhttp" +version = "0.1.0" +source = "git+https://github.com/dfinity/evm-rpc-canister?rev=c1d9a12eb9d3af92671dc07963c8887ec178fcbc#c1d9a12eb9d3af92671dc07963c8887ec178fcbc" +dependencies = [ + "futures-util", + "http 1.3.1", + "ic-cdk", + "num-traits", + "pin-project", + "serde", + "serde_json", + "thiserror 2.0.12", + "tower", + "tower-layer", +] + [[package]] name = "canlog" version = "0.1.0" @@ -653,9 +676,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.15" +version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ "jobserver", "libc", @@ -715,7 +738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", - "half 2.4.1", + "half 2.5.0", ] [[package]] @@ -1045,6 +1068,26 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "digest" version = "0.9.0" @@ -1122,9 +1165,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" [[package]] name = "ed25519" @@ -1178,9 +1221,9 @@ dependencies = [ [[package]] name = "either" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "ena" @@ -1284,9 +1327,9 @@ checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" [[package]] name = "ff" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ "rand_core 0.6.4", "subtle", @@ -1562,7 +1605,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.2.0", + "http 1.3.1", "indexmap", "slab", "tokio", @@ -1578,9 +1621,9 @@ checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" [[package]] name = "half" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" dependencies = [ "cfg-if", "crunchy", @@ -1665,9 +1708,9 @@ dependencies = [ [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -1692,27 +1735,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.2.0", + "http 1.3.1", ] [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", - "http 1.2.0", + "futures-core", + "http 1.3.1", "http-body 1.0.1", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1754,7 +1797,7 @@ dependencies = [ "futures-channel", "futures-util", "h2 0.4.8", - "http 1.2.0", + "http 1.3.1", "http-body 1.0.1", "httparse", "itoa", @@ -1785,14 +1828,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", - "http 1.2.0", + "http 1.3.1", "hyper 1.6.0", "hyper-util", "rustls 0.23.23", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.1", + "tokio-rustls 0.26.2", "tower-service", "webpki-roots 0.26.8", ] @@ -1806,7 +1849,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.2.0", + "http 1.3.1", "http-body 1.0.1", "hyper 1.6.0", "pin-project-lite", @@ -1875,10 +1918,19 @@ dependencies = [ "hkdf", "pem", "rand 0.8.5", - "thiserror 2.0.11", + "thiserror 2.0.12", "zeroize", ] +[[package]] +name = "ic-sha3" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3715f0f4370e8ce6aa9805b81e915ef4420c9dfb5209c71489c27e6f98bd5d65" +dependencies = [ + "sha3", +] + [[package]] name = "ic-stable-structures" version = "0.6.8" @@ -1912,7 +1964,7 @@ dependencies = [ "serde_cbor", "serde_repr", "sha2 0.10.8", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -2082,9 +2134,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -2138,9 +2190,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jni" @@ -2250,9 +2302,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.170" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libredox" @@ -2260,7 +2312,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "libc", ] @@ -2312,9 +2364,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" [[package]] name = "litemap" @@ -2391,15 +2443,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memmap2" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" -dependencies = [ - "libc", -] - [[package]] name = "memoffset" version = "0.9.1" @@ -2437,6 +2480,26 @@ dependencies = [ "unicase", ] +[[package]] +name = "minicbor" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9463b6054ac1e6df4f04fdec5313cf443363e15c804deae85ceee9c14eb7ea33" +dependencies = [ + "minicbor-derive", +] + +[[package]] +name = "minicbor-derive" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75aafe3a28e1128ffca395f05b987fbdd0c49faefb8a852e0dd2ac007c9669a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2475,7 +2538,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "cfg-if", "cfg_aliases", "libc", @@ -2688,9 +2751,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" [[package]] name = "opaque-debug" @@ -2812,6 +2875,26 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2853,7 +2936,7 @@ dependencies = [ "hex", "ic-certification", "ic-transport-types", - "reqwest 0.12.12", + "reqwest 0.12.14", "schemars", "serde", "serde_bytes", @@ -2861,9 +2944,9 @@ dependencies = [ "serde_json", "sha2 0.10.8", "slog", - "strum 0.26.3", - "strum_macros 0.26.4", - "thiserror 2.0.11", + "strum", + "strum_macros", + "thiserror 2.0.12", "tokio", "tracing", "tracing-appender", @@ -2885,11 +2968,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy", + "zerocopy 0.8.23", ] [[package]] @@ -2900,9 +2983,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "pretty" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55c4d17d994b637e2f4daf6e5dc5d660d209d5642377d675d7a1c3ab69fa579" +checksum = "ac98773b7109bc75f475ab5a134c9b64b87e59d776d31098d8f346922396a477" dependencies = [ "arrayvec 0.5.2", "typed-arena", @@ -2920,9 +3003,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ "toml_edit", ] @@ -2944,7 +3027,7 @@ checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set 0.8.0", "bit-vec 0.8.0", - "bitflags 2.8.0", + "bitflags 2.9.0", "lazy_static", "num-traits", "rand 0.8.5", @@ -2999,7 +3082,7 @@ dependencies = [ "rustc-hash", "rustls 0.23.23", "socket2", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", ] @@ -3019,7 +3102,7 @@ dependencies = [ "rustls-pki-types", "rustls-platform-verifier", "slab", - "thiserror 2.0.11", + "thiserror 2.0.12", "tinyvec", "tracing", "web-time", @@ -3134,7 +3217,7 @@ version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", ] [[package]] @@ -3159,11 +3242,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", ] [[package]] @@ -3267,9 +3350,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.12" +version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +checksum = "989e327e510263980e231de548a33e63d34962d29ae61b467389a1a09627a254" dependencies = [ "base64 0.22.1", "bytes", @@ -3277,7 +3360,7 @@ dependencies = [ "futures-core", "futures-util", "h2 0.4.8", - "http 1.2.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", "hyper 1.6.0", @@ -3301,7 +3384,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", - "tokio-rustls 0.26.1", + "tokio-rustls 0.26.2", "tokio-socks", "tokio-util", "tower", @@ -3332,9 +3415,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.11" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5349ae27d3887ca812fb375b45a4fbb36d8d12d2df394968cd86e35683fe73" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", @@ -3376,11 +3459,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.44" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "errno", "libc", "linux-raw-sys", @@ -3515,9 +3598,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "rusty-fork" @@ -3533,9 +3616,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -3601,7 +3684,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -3615,7 +3698,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "core-foundation 0.10.0", "core-foundation-sys", "libc", @@ -3634,9 +3717,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" dependencies = [ "serde", ] @@ -3714,9 +3797,9 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", @@ -3899,21 +3982,33 @@ dependencies = [ name = "sol_rpc_canister" version = "0.1.0" dependencies = [ + "assert_matches", "candid", "candid_parser", + "canhttp", "canlog", "ciborium", "const_format", + "derive_more", + "futures", "hex", + "http 1.3.1", "ic-cdk", + "ic-sha3", "ic-stable-structures", "maplit", + "minicbor", + "num-traits", "proptest", "regex", "serde", "serde_bytes", "serde_json", "sol_rpc_types", + "solana-clock", + "thiserror 1.0.69", + "tower", + "tower-http", "url", "zeroize", ] @@ -3957,10 +4052,11 @@ version = "0.1.0" dependencies = [ "candid", "canlog", + "derive_more", "ic-cdk", "regex", "serde", - "strum 0.27.1", + "thiserror 1.0.69", "url", ] @@ -4113,7 +4209,7 @@ dependencies = [ "solana-transaction", "solana-transaction-error", "solana-udp-client", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", ] @@ -4185,7 +4281,7 @@ dependencies = [ "solana-net-utils", "solana-time-utils", "solana-transaction-error", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", ] @@ -4275,7 +4371,7 @@ dependencies = [ "solana-pubkey", "solana-sdk-ids", "solana-system-interface", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -4367,7 +4463,7 @@ name = "solana-instructions-sysvar" version = "2.2.0" source = "git+https://github.com/lpahlavi/agave.git#fa0bacd5508c1e443db79a67d470c537fa558de0" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "solana-account-info", "solana-instruction", "solana-program-error", @@ -4496,7 +4592,7 @@ dependencies = [ "solana-cluster-type", "solana-sha256-hasher", "solana-time-utils", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -4552,7 +4648,7 @@ version = "2.2.0" source = "git+https://github.com/lpahlavi/agave.git#fa0bacd5508c1e443db79a67d470c537fa558de0" dependencies = [ "bincode", - "bitflags 2.8.0", + "bitflags 2.9.0", "cfg_eval", "serde", "serde_derive", @@ -4664,7 +4760,7 @@ dependencies = [ "solana-sysvar", "solana-sysvar-id", "solana-vote-interface", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -4755,7 +4851,7 @@ dependencies = [ "solana-pubkey", "solana-rpc-client-api", "solana-signature", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tokio-stream", "tokio-tungstenite", @@ -4789,7 +4885,7 @@ dependencies = [ "solana-streamer", "solana-tls-utils", "solana-transaction-error", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", ] @@ -4895,7 +4991,7 @@ dependencies = [ "solana-transaction-error", "solana-transaction-status-client-types", "solana-version", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -4911,7 +5007,7 @@ dependencies = [ "solana-pubkey", "solana-rpc-client", "solana-sdk-ids", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -4945,7 +5041,7 @@ source = "git+https://github.com/lpahlavi/agave.git#fa0bacd5508c1e443db79a67d470 dependencies = [ "libsecp256k1", "solana-define-syscall", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -5098,7 +5194,7 @@ dependencies = [ "solana-tls-utils", "solana-transaction-error", "solana-transaction-metrics-tracker", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tokio-util", "x509-parser", @@ -5235,7 +5331,7 @@ dependencies = [ "solana-signer", "solana-transaction", "solana-transaction-error", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", ] @@ -5319,7 +5415,7 @@ dependencies = [ "solana-transaction", "solana-transaction-context", "solana-transaction-error", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -5333,7 +5429,7 @@ dependencies = [ "solana-net-utils", "solana-streamer", "solana-transaction-error", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", ] @@ -5435,16 +5531,7 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros 0.26.4", -] - -[[package]] -name = "strum" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" -dependencies = [ - "strum_macros 0.27.1", + "strum_macros", ] [[package]] @@ -5460,19 +5547,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "strum_macros" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.100", -] - [[package]] name = "subtle" version = "2.6.1" @@ -5571,11 +5645,10 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.17.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" +checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600" dependencies = [ - "cfg-if", "fastrand", "getrandom 0.3.1", "once_cell", @@ -5614,11 +5687,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -5634,9 +5707,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -5655,9 +5728,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" dependencies = [ "deranged", "itoa", @@ -5670,15 +5743,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" dependencies = [ "num-conv", "time-core", @@ -5705,9 +5778,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] @@ -5759,9 +5832,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ "rustls 0.23.23", "tokio", @@ -5807,9 +5880,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" dependencies = [ "bytes", "futures-core", @@ -5859,6 +5932,21 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-http" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" +dependencies = [ + "bitflags 2.9.0", + "bytes", + "http 1.3.1", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -6012,9 +6100,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-segmentation" @@ -6304,34 +6392,39 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + [[package]] name = "windows-registry" -version = "0.2.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ "windows-result", "windows-strings", - "windows-targets 0.52.6", + "windows-targets 0.53.0", ] [[package]] name = "windows-result" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-result", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -6385,13 +6478,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -6404,6 +6513,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -6416,6 +6531,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -6428,12 +6549,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -6446,6 +6579,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -6458,6 +6597,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -6470,6 +6615,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -6482,11 +6633,17 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" dependencies = [ "memchr", ] @@ -6507,7 +6664,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", ] [[package]] @@ -6576,8 +6733,16 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive 0.8.23", ] [[package]] @@ -6591,6 +6756,17 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 3083da2a..4f24564c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,22 +27,30 @@ opt-level = 's' inherits = "release" [workspace.dependencies] +assert_matches = "1.5.0" async-trait = "0.1.88" candid = "0.10.13" candid_parser = "0.1.4" +canhttp = { git = "https://github.com/dfinity/evm-rpc-canister", rev = "c1d9a12eb9d3af92671dc07963c8887ec178fcbc" } ciborium = "0.2.2" # Transitive dependency of ic-ed25519 # See https://forum.dfinity.org/t/module-imports-function-wbindgen-describe-from-wbindgen-placeholder-that-is-not-exported-by-the-runtime/11545/8 const_format = "0.2.34" +derive_more = { version = "2.0.1", features = ["from"] } futures = "0.3.31" getrandom = { version = "*", default-features = false, features = ["custom"] } hex = "0.4.3" +http = "1.2.0" ic-canister-log = "0.2.0" ic-cdk = "0.17.1" ic-ed25519 = "0.1.0" -ic-stable-structures = "0.6.8" +ic-sha3 = "1.0.0" +ic-stable-structures = "0.6.7" ic-test-utilities-load-wasm = { git = "https://github.com/dfinity/ic", tag = "release-2025-01-23_03-04-base" } maplit = "1.0.2" +minicbor = { version = "0.26.1", features = ["alloc", "derive"] } +num = "0.4.3" +num-traits = "0.2.19" pocket-ic = "7.0.0" proptest = "1.6.0" regex = "1.11.1" @@ -60,7 +68,11 @@ solana-pubkey = "2.2.0" solana-signature = "2.2.0" solana-transaction = { version = "2.2.0", features = ["bincode"] } strum = { version = "0.27.0", features = ["derive"] } +thiserror = "1.0.69" tokio = "1.44.1" +tower = "0.5.2" +tower-layer = "0.3.3" +tower-http = "0.6.2" url = "2.5" zeroize = { version = "1.8", features = ["zeroize_derive"] } diff --git a/canister/Cargo.toml b/canister/Cargo.toml index bd1e21ab..2136e070 100644 --- a/canister/Cargo.toml +++ b/canister/Cargo.toml @@ -13,21 +13,33 @@ name = "sol_rpc_canister" path = "src/main.rs" [dependencies] +assert_matches = { workspace = true } candid = { workspace = true } +canhttp = { workspace = true, features = ["json"] } canlog = { path = "../canlog", features = ["derive"] } ciborium = { workspace = true } const_format = { workspace = true } +derive_more = { workspace = true } +futures = { workspace = true } hex = { workspace = true } +http = { workspace = true } ic-cdk = { workspace = true } +ic-sha3 = { workspace = true } ic-stable-structures = { workspace = true } maplit = { workspace = true } +minicbor = { workspace = true } +num-traits = { workspace = true } +solana-clock = { workspace = true } sol_rpc_types = { path = "../libs/types" } regex = { workspace = true } serde = { workspace = true } -serde_json = {workspace = true} +serde_json = { workspace = true } serde_bytes = { workspace = true } +tower = { workspace = true } +tower-http = { workspace = true, features = ["set-header", "util"] } url = { workspace = true } zeroize = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] candid_parser = { workspace = true } diff --git a/canister/sol_rpc_canister.did b/canister/sol_rpc_canister.did index 741c5ea1..3abc44d3 100644 --- a/canister/sol_rpc_canister.did +++ b/canister/sol_rpc_canister.did @@ -86,6 +86,70 @@ type HttpHeader = record { name : text }; +// Represents an error that occurred while trying to perform an RPC call. +type RpcError = variant { + JsonRpcError : JsonRpcError; + ProviderError : ProviderError; + ValidationError : text; + HttpOutcallError : HttpOutcallError; +}; + +// Represents a JSON-RPC error. +type JsonRpcError = record { code : int64; message : text }; + +// Represents an error with an RPC provider. +type ProviderError = variant { + TooFewCycles : record { expected : nat; received : nat }; + InvalidRpcConfig : text; + UnsupportedCluster : text; +}; + +// Represents an HTTP outcall error. +type HttpOutcallError = variant { + IcError : record { code: RejectionCode; message: text }; + InvalidHttpJsonRpcResponse : record { + status : nat16; + body : text; + parsingError : opt text; + }; +}; + +// Represents an IC rejection code for an HTTP outcall. +type RejectionCode = variant { + NoError; + CanisterError; + SysTransient; + DestinationInvalid; + Unknown; + SysFatal; + CanisterReject; +}; + +// Represents a Solana slot +type Slot = nat64; + +// Represents the result of a call to the `getSlot` Solana RPC method. +type GetSlotResult = variant { Ok : Slot; Err : RpcError }; + +// Represents an aggregated result from multiple RPC calls to the `getSlot` Solana RPC method. +type MultiGetSlotResult = variant { + Consistent : GetSlotResult; + Inconsistent : vec record { RpcSource; GetSlotResult }; +}; + +// Commitment levels in Solana, representing finality guarantees of transactions and state queries. +type CommitmentLevel = variant { + processed; + confirmed; + finalized; +}; + +// The parameters for a call to the `getSlot` Solana RPC method. +type GetSlotParams = record { + commitment: opt CommitmentLevel; + minContextSlot: opt nat64; +}; + // A string used as a regex pattern. type Regex = text; @@ -108,11 +172,24 @@ type LogFilter = variant { HidePattern : Regex; }; -/// The installation args for the Solana RPC canister. +// The number of nodes in the subnet +type NumSubnetNodes = nat32; + +// The canister operation mode. Default is 'Normal'. +type Mode = variant { + // Normal mode, where cycle payment is required for certain operations. + Normal; + // Demo mode, where cycle payment is not required. + Demo; +}; + +// The installation args for the Solana RPC canister. type InstallArgs = record { manageApiKeys: opt vec principal; overrideProvider: opt OverrideProvider; logFilter: opt LogFilter; + numSubnetNodes : opt NumSubnetNodes; + mode : opt Mode; }; service : (InstallArgs,) -> { @@ -123,9 +200,9 @@ service : (InstallArgs,) -> { // // # Preconditions // - // The caller is the controller or a principal specified in `Installargs::manage_api_keys`. + // The caller is the controller or a principal specified in `InstallArgs::manage_api_keys`. updateApiKeys : (vec record { SupportedProvider; opt text }) -> (); - // TODO XC-292: change signature - getSlot : (RpcSources, opt RpcConfig) -> (nat64); + // Call the Solana `getSlot` RPC method and return the resulting slot. + getSlot : (RpcSources, opt RpcConfig, opt GetSlotParams) -> (MultiGetSlotResult); }; \ No newline at end of file diff --git a/canister/src/candid_rpc/mod.rs b/canister/src/candid_rpc/mod.rs new file mode 100644 index 00000000..03cceaa8 --- /dev/null +++ b/canister/src/candid_rpc/mod.rs @@ -0,0 +1,39 @@ +use crate::{ + rpc_client::{MultiCallError, SolRpcClient}, + types::MultiRpcResult, +}; +use sol_rpc_types::{GetSlotParams, RpcConfig, RpcResult, RpcSources}; +use solana_clock::Slot; + +fn process_result(result: Result>) -> MultiRpcResult { + match result { + Ok(value) => MultiRpcResult::Consistent(Ok(value)), + Err(err) => match err { + MultiCallError::ConsistentError(err) => MultiRpcResult::Consistent(Err(err)), + MultiCallError::InconsistentResults(multi_call_results) => { + let results = multi_call_results.into_vec(); + results.iter().for_each(|(_service, _service_result)| { + // TODO XC-296: Add metrics for inconsistent providers + }); + MultiRpcResult::Inconsistent(results) + } + }, + } +} + +/// Adapt the `EthRpcClient` to the `Candid` interface used by the EVM-RPC canister. +pub struct CandidRpcClient { + client: SolRpcClient, +} + +impl CandidRpcClient { + pub fn new(source: RpcSources, config: Option) -> RpcResult { + Ok(Self { + client: SolRpcClient::new(source, config)?, + }) + } + + pub async fn get_slot(&self, params: GetSlotParams) -> MultiRpcResult { + process_result(self.client.get_slot(params).await) + } +} diff --git a/canister/src/constants.rs b/canister/src/constants.rs index 744e29ff..bfc06759 100644 --- a/canister/src/constants.rs +++ b/canister/src/constants.rs @@ -1,3 +1,9 @@ +// Cycles (per node) which must be passed with each RPC request +// as processing fee. +pub const COLLATERAL_CYCLES_PER_NODE: u128 = 10_000_000; + +pub const CONTENT_TYPE_VALUE: &str = "application/json"; + pub const API_KEY_REPLACE_STRING: &str = "{API_KEY}"; pub const API_KEY_MAX_SIZE: usize = 512; pub const VALID_API_KEY_CHARS: &str = diff --git a/canister/src/http/errors.rs b/canister/src/http/errors.rs new file mode 100644 index 00000000..02c2cc15 --- /dev/null +++ b/canister/src/http/errors.rs @@ -0,0 +1,88 @@ +use canhttp::{ + http::{ + json::{JsonRequestConversionError, JsonResponseConversionError}, + FilterNonSuccessfulHttpResponseError, HttpRequestConversionError, + HttpResponseConversionError, + }, + CyclesAccountingError, HttpsOutcallError, IcError, +}; +use derive_more::From; +use sol_rpc_types::{HttpOutcallError, ProviderError, RpcError}; +use thiserror::Error; + +#[derive(Clone, Debug, Error, From)] +pub enum HttpClientError { + #[error("IC error: {0}")] + IcError(IcError), + #[error("unknown error (most likely sign of a bug): {0}")] + NotHandledError(String), + #[error("cycles accounting error: {0}")] + CyclesAccountingError(CyclesAccountingError), + #[error("HTTP response was not successful: {0}")] + UnsuccessfulHttpResponse(FilterNonSuccessfulHttpResponseError>), + #[error("Error converting response to JSON: {0}")] + InvalidJsonResponse(JsonResponseConversionError), +} + +impl From for HttpClientError { + fn from(value: HttpRequestConversionError) -> Self { + HttpClientError::NotHandledError(value.to_string()) + } +} + +impl From for HttpClientError { + fn from(value: HttpResponseConversionError) -> Self { + // Replica should return valid http::Response + HttpClientError::NotHandledError(value.to_string()) + } +} + +impl From for HttpClientError { + fn from(value: JsonRequestConversionError) -> Self { + HttpClientError::NotHandledError(value.to_string()) + } +} + +impl From for RpcError { + fn from(error: HttpClientError) -> Self { + match error { + HttpClientError::IcError(IcError { code, message }) => { + RpcError::HttpOutcallError(HttpOutcallError::IcError { code, message }) + } + HttpClientError::NotHandledError(e) => RpcError::ValidationError(e), + HttpClientError::CyclesAccountingError( + CyclesAccountingError::InsufficientCyclesError { expected, received }, + ) => RpcError::ProviderError(ProviderError::TooFewCycles { expected, received }), + HttpClientError::InvalidJsonResponse( + JsonResponseConversionError::InvalidJsonResponse { + status, + body, + parsing_error, + }, + ) => RpcError::HttpOutcallError(HttpOutcallError::InvalidHttpJsonRpcResponse { + status, + body, + parsing_error: Some(parsing_error), + }), + HttpClientError::UnsuccessfulHttpResponse( + FilterNonSuccessfulHttpResponseError::UnsuccessfulResponse(response), + ) => RpcError::HttpOutcallError(HttpOutcallError::InvalidHttpJsonRpcResponse { + status: response.status().as_u16(), + body: String::from_utf8_lossy(response.body()).to_string(), + parsing_error: None, + }), + } + } +} + +impl HttpsOutcallError for HttpClientError { + fn is_response_too_large(&self) -> bool { + match self { + HttpClientError::IcError(e) => e.is_response_too_large(), + HttpClientError::NotHandledError(_) + | HttpClientError::CyclesAccountingError(_) + | HttpClientError::UnsuccessfulHttpResponse(_) + | HttpClientError::InvalidJsonResponse(_) => false, + } + } +} diff --git a/canister/src/http/mod.rs b/canister/src/http/mod.rs new file mode 100644 index 00000000..8d5d6365 --- /dev/null +++ b/canister/src/http/mod.rs @@ -0,0 +1,162 @@ +mod errors; + +use crate::{ + constants::{COLLATERAL_CYCLES_PER_NODE, CONTENT_TYPE_VALUE}, + http::errors::HttpClientError, + logs::Priority, + state::{next_request_id, read_state, State}, +}; +use canhttp::{ + convert::ConvertRequestLayer, + http::{ + json::{ + HttpJsonRpcRequest, HttpJsonRpcResponse, JsonRequestConverter, JsonResponseConverter, + }, + FilterNonSuccessfulHttpResponse, HttpRequestConverter, HttpResponseConverter, + }, + observability::ObservabilityLayer, + retry::DoubleMaxResponseBytes, + ConvertServiceBuilder, CyclesAccounting, CyclesChargingPolicy, +}; +use canlog::log; +use http::{header::CONTENT_TYPE, HeaderValue}; +use ic_cdk::api::management_canister::http_request::CanisterHttpRequestArgument; +use serde::{de::DeserializeOwned, Serialize}; +use sol_rpc_types::{Mode, RpcError}; +use std::fmt::Debug; +use tower::{ + layer::util::{Identity, Stack}, + retry::RetryLayer, + util::MapRequestLayer, + Service, ServiceBuilder, +}; +use tower_http::{set_header::SetRequestHeaderLayer, ServiceBuilderExt}; + +pub fn http_client( + retry: bool, +) -> impl Service, Response = HttpJsonRpcResponse, Error = RpcError> +where + I: Serialize + Clone + Debug, + O: DeserializeOwned + Debug, +{ + let maybe_retry = if retry { + Some(RetryLayer::new(DoubleMaxResponseBytes)) + } else { + None + }; + let maybe_unique_id = if retry { + Some(MapRequestLayer::new(generate_request_id)) + } else { + None + }; + ServiceBuilder::new() + .map_err(|e: HttpClientError| RpcError::from(e)) + .option_layer(maybe_retry) + .option_layer(maybe_unique_id) + // TODO XC-296: Flesh out observability layer + .layer( + ObservabilityLayer::new() + .on_request(move |req: &HttpJsonRpcRequest| { + log!( + Priority::TraceHttp, + "JSON-RPC request with id `{}` to {}: {:?}", + req.body().id().clone(), + req.uri().host().unwrap().to_string(), + req.body() + ); + }) + .on_response(|_req_data: (), response: &HttpJsonRpcResponse| { + log!( + Priority::TraceHttp, + "JSON-RPC response: {:?}", + response.body() + ); + }), + ) + .layer(service_request_builder()) + .convert_response(JsonResponseConverter::new()) + .convert_response(FilterNonSuccessfulHttpResponse) + .convert_response(HttpResponseConverter) + .convert_request(CyclesAccounting::new( + read_state(|s| s.get_num_subnet_nodes()), + ChargingPolicyWithCollateral::default(), + )) + .service(canhttp::Client::new_with_error::()) +} + +fn generate_request_id(request: HttpJsonRpcRequest) -> HttpJsonRpcRequest { + let (parts, mut body) = request.into_parts(); + body.set_id(next_request_id()); + http::Request::from_parts(parts, body) +} + +type JsonRpcServiceBuilder = ServiceBuilder< + Stack< + ConvertRequestLayer, + Stack< + ConvertRequestLayer>, + Stack, Identity>, + >, + >, +>; + +/// Middleware that takes care of transforming the request. +/// +/// It's required to separate it from the other middlewares, to compute the exact request cost. +pub fn service_request_builder() -> JsonRpcServiceBuilder { + ServiceBuilder::new() + .insert_request_header_if_not_present( + CONTENT_TYPE, + HeaderValue::from_static(CONTENT_TYPE_VALUE), + ) + .convert_request(JsonRequestConverter::::new()) + .convert_request(HttpRequestConverter) +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ChargingPolicyWithCollateral { + charge_user: bool, + collateral_cycles: u128, +} + +impl ChargingPolicyWithCollateral { + pub fn new( + num_nodes_in_subnet: u32, + charge_user: bool, + collateral_cycles_per_node: u128, + ) -> Self { + let collateral_cycles = + collateral_cycles_per_node.saturating_mul(num_nodes_in_subnet as u128); + Self { + charge_user, + collateral_cycles, + } + } + + fn new_from_state(s: &State) -> Self { + Self::new( + s.get_num_subnet_nodes(), + !matches!(s.get_mode(), Mode::Demo), + COLLATERAL_CYCLES_PER_NODE, + ) + } +} + +impl Default for ChargingPolicyWithCollateral { + fn default() -> Self { + read_state(Self::new_from_state) + } +} + +impl CyclesChargingPolicy for ChargingPolicyWithCollateral { + fn cycles_to_charge( + &self, + _request: &CanisterHttpRequestArgument, + attached_cycles: u128, + ) -> u128 { + if self.charge_user { + return attached_cycles.saturating_add(self.collateral_cycles); + } + 0 + } +} diff --git a/canister/src/lib.rs b/canister/src/lib.rs index cf5a74a6..f0050f6c 100644 --- a/canister/src/lib.rs +++ b/canister/src/lib.rs @@ -1,4 +1,6 @@ +pub mod candid_rpc; pub mod constants; +pub mod http; pub mod http_types; pub mod lifecycle; pub mod logs; diff --git a/canister/src/lifecycle/mod.rs b/canister/src/lifecycle/mod.rs index 39e5441c..bf6b7c72 100644 --- a/canister/src/lifecycle/mod.rs +++ b/canister/src/lifecycle/mod.rs @@ -25,5 +25,11 @@ pub fn post_upgrade(args: Option) { if let Some(log_filter) = args.log_filter { mutate_state(|s| s.set_log_filter(log_filter)); } + if let Some(num_subnet_nodes) = args.num_subnet_nodes { + mutate_state(|s| s.set_num_subnet_nodes(num_subnet_nodes.into())); + } + if let Some(mode) = args.mode { + mutate_state(|s| s.set_mode(mode)); + } } } diff --git a/canister/src/main.rs b/canister/src/main.rs index cb142e8d..1642dbb9 100644 --- a/canister/src/main.rs +++ b/canister/src/main.rs @@ -1,22 +1,18 @@ -use candid::{candid_method, Nat}; +use candid::candid_method; use canlog::{log, Log, Sort}; -use ic_cdk::api::management_canister::http_request::{ - CanisterHttpRequestArgument, HttpHeader, HttpMethod, -}; -use ic_cdk::{ - api::is_controller, - {query, update}, -}; -use serde_json::json; +use ic_cdk::{api::is_controller, query, update}; use sol_rpc_canister::{ + candid_rpc::CandidRpcClient, http_types, lifecycle, logs::Priority, providers::{get_provider, PROVIDERS}, state::{mutate_state, read_state}, }; use sol_rpc_types::{ - RpcAccess, RpcConfig, RpcSources, SupportedRpcProvider, SupportedRpcProviderId, + GetSlotParams, MultiRpcResult, RpcAccess, RpcConfig, RpcSources, SupportedRpcProvider, + SupportedRpcProviderId, }; +use solana_clock::Slot; use std::str::FromStr; pub fn require_api_key_principal_or_controller() -> Result<(), String> { @@ -75,40 +71,17 @@ async fn update_api_keys(api_keys: Vec<(SupportedRpcProviderId, Option)> } } -//TODO XC-292: change implementation #[update(name = "getSlot")] #[candid_method(rename = "getSlot")] -async fn get_slot(_source: RpcSources, _config: Option) -> u64 { - let body = json!({ "jsonrpc": "2.0", "id": 1, "method": "getSlot" }); - let request = CanisterHttpRequestArgument { - url: "http://localhost:8899".to_string(), - max_response_bytes: Some(1_000), - method: HttpMethod::POST, - headers: vec![HttpHeader { - name: "content-type".to_string(), - value: "application/json".to_string(), - }], - body: Some(serde_json::to_vec(&body).unwrap()), - transform: None, - }; - ic_cdk::println!("getSlot request: {body}"); - let response = - match ic_cdk::api::management_canister::http_request::http_request(request, 1_000_000_000) - .await - { - Ok((response,)) => response, - Err((code, message)) => panic!("Error {code:?}: {message}"), - }; - assert_eq!( - response.status, - Nat::from(200_u8), - "Non successful HTTP response" - ); - let response_body: serde_json::Value = - serde_json::from_slice(response.body.as_slice()).expect("Invalid JSON response"); - let slot = response_body["result"].as_u64().unwrap(); - ic_cdk::println!("getSlot response: {slot}"); - slot +async fn get_slot( + source: RpcSources, + config: Option, + params: Option, +) -> MultiRpcResult { + match CandidRpcClient::new(source, config) { + Ok(client) => client.get_slot(params.unwrap_or_default()).await.into(), + Err(err) => Err(err).into(), + } } #[query(hidden = true)] diff --git a/canister/src/providers/mod.rs b/canister/src/providers/mod.rs index f776f39e..68ca0065 100644 --- a/canister/src/providers/mod.rs +++ b/canister/src/providers/mod.rs @@ -1,11 +1,14 @@ #[cfg(test)] mod tests; +use crate::{constants::API_KEY_REPLACE_STRING, state::read_state, types::OverrideProvider}; +use ic_cdk::api::management_canister::http_request::HttpHeader; use maplit::btreemap; use sol_rpc_types::{ - RpcAccess, RpcAuth, SolanaCluster, SupportedRpcProvider, SupportedRpcProviderId, + ConsensusStrategy, ProviderError, RpcAccess, RpcAuth, RpcEndpoint, RpcError, RpcResult, + RpcSource, RpcSources, SolanaCluster, SupportedRpcProvider, SupportedRpcProviderId, }; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; thread_local! { pub static PROVIDERS: BTreeMap = btreemap! { @@ -87,3 +90,204 @@ thread_local! { pub fn get_provider(provider_id: &SupportedRpcProviderId) -> Option { PROVIDERS.with(|providers| providers.get(provider_id).cloned()) } + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Providers { + /// *Non-empty* set of providers to query. + pub sources: BTreeSet, +} + +impl Providers { + // Order of providers matters! + // The threshold consensus strategy will consider the first `total` providers in the order + // they are specified (taking the default ones first, followed by the non default ones if necessary) + // if the providers are not explicitly specified by the caller. + const DEFAULT_MAINNET_SUPPORTED_PROVIDERS: &'static [SupportedRpcProviderId] = &[ + SupportedRpcProviderId::AlchemyMainnet, + SupportedRpcProviderId::AnkrMainnet, + SupportedRpcProviderId::DrpcMainnet, + ]; + const NON_DEFAULT_MAINNET_SUPPORTED_PROVIDERS: &'static [SupportedRpcProviderId] = &[ + SupportedRpcProviderId::HeliusMainnet, + SupportedRpcProviderId::PublicNodeMainnet, + ]; + + const DEFAULT_DEVNET_SUPPORTED_PROVIDERS: &'static [SupportedRpcProviderId] = &[ + SupportedRpcProviderId::AlchemyDevnet, + SupportedRpcProviderId::AnkrDevnet, + SupportedRpcProviderId::DrpcDevnet, + ]; + const NON_DEFAULT_DEVNET_SUPPORTED_PROVIDERS: &'static [SupportedRpcProviderId] = + &[SupportedRpcProviderId::HeliusDevnet]; + + pub fn new(source: RpcSources, strategy: ConsensusStrategy) -> Result { + fn get_sources(provider_ids: &[SupportedRpcProviderId]) -> Vec { + provider_ids + .iter() + .map(|provider| RpcSource::Supported(*provider)) + .collect() + } + + let providers: BTreeSet<_> = match source { + RpcSources::Custom(sources) => { + choose_providers(Some(sources), vec![], vec![], strategy)? + } + RpcSources::Default(cluster) => match cluster { + SolanaCluster::Mainnet => choose_providers( + None, + get_sources(Self::DEFAULT_MAINNET_SUPPORTED_PROVIDERS), + get_sources(Self::NON_DEFAULT_MAINNET_SUPPORTED_PROVIDERS), + strategy, + )?, + SolanaCluster::Devnet => choose_providers( + None, + get_sources(Self::DEFAULT_DEVNET_SUPPORTED_PROVIDERS), + get_sources(Self::NON_DEFAULT_DEVNET_SUPPORTED_PROVIDERS), + strategy, + )?, + cluster => return Err(ProviderError::UnsupportedCluster(format!("{:?}", cluster))), + }, + }; + + if providers.is_empty() { + return Err(ProviderError::InvalidRpcConfig( + "No matching providers found".to_string(), + )); + } + + Ok(Self { sources: providers }) + } +} + +fn choose_providers( + user_input: Option>, + default_providers: Vec, + non_default_providers: Vec, + strategy: ConsensusStrategy, +) -> Result, ProviderError> { + match strategy { + ConsensusStrategy::Equality => Ok(user_input + .unwrap_or_else(|| default_providers.to_vec()) + .into_iter() + .collect()), + ConsensusStrategy::Threshold { total, min } => { + // Ensure that + // 0 < min <= total <= all_providers.len() + if min == 0 { + return Err(ProviderError::InvalidRpcConfig( + "min must be greater than 0".to_string(), + )); + } + match user_input { + None => { + let all_providers_len = default_providers.len() + non_default_providers.len(); + let total = total.ok_or_else(|| { + ProviderError::InvalidRpcConfig( + "total must be specified when using default providers".to_string(), + ) + })?; + + if min > total { + return Err(ProviderError::InvalidRpcConfig(format!( + "min {} is greater than total {}", + min, total + ))); + } + + if total > all_providers_len as u8 { + return Err(ProviderError::InvalidRpcConfig(format!( + "total {} is greater than the number of all supported providers {}", + total, all_providers_len + ))); + } + let providers: BTreeSet<_> = default_providers + .iter() + .chain(non_default_providers.iter()) + .take(total as usize) + .cloned() + .collect(); + assert_eq!(providers.len(), total as usize, "BUG: duplicate providers"); + Ok(providers) + } + Some(providers) => { + if min > providers.len() as u8 { + return Err(ProviderError::InvalidRpcConfig(format!( + "min {} is greater than the number of specified providers {}", + min, + providers.len() + ))); + } + if let Some(total) = total { + if total != providers.len() as u8 { + return Err(ProviderError::InvalidRpcConfig(format!( + "total {} is different than the number of specified providers {}", + total, + providers.len() + ))); + } + } + Ok(providers.into_iter().collect()) + } + } + } + } +} + +pub fn resolve_rpc_provider(service: RpcSource) -> RpcEndpoint { + match service { + RpcSource::Supported(provider_id) => PROVIDERS + .with(|providers| providers.get(&provider_id).cloned()) + .map(|provider| resolve_api_key(provider.access, provider_id)) + .expect("Unknown provider"), + RpcSource::Custom(api) => api, + } +} + +fn resolve_api_key(access: RpcAccess, provider: SupportedRpcProviderId) -> RpcEndpoint { + match &access { + RpcAccess::Authenticated { auth, public_url } => { + let api_key = read_state(|s| s.get_api_key(&provider)); + match api_key { + Some(api_key) => match auth { + RpcAuth::BearerToken { url } => RpcEndpoint { + url: url.to_string(), + headers: Some(vec![HttpHeader { + name: "Authorization".to_string(), + value: format!("Bearer {}", api_key.read()), + }]), + }, + RpcAuth::UrlParameter { url_pattern } => RpcEndpoint { + url: url_pattern.replace(API_KEY_REPLACE_STRING, api_key.read()), + headers: None, + }, + }, + None => RpcEndpoint { + url: public_url.clone().unwrap_or_else(|| { + panic!("API key not yet initialized for provider: {:?}", provider) + }), + headers: None, + }, + } + } + RpcAccess::Unauthenticated { public_url } => RpcEndpoint { + url: public_url.to_string(), + headers: None, + }, + } +} + +pub fn request_builder( + endpoint: RpcEndpoint, + override_provider: &OverrideProvider, +) -> RpcResult { + let endpoint = override_provider.apply(endpoint).map_err(|regex_error| { + RpcError::ValidationError(format!( + "BUG: regex should have been validated when initially set. Error: {regex_error}" + )) + })?; + let mut request_builder = http::Request::post(endpoint.url); + for HttpHeader { name, value } in endpoint.headers.unwrap_or_default() { + request_builder = request_builder.header(name, value); + } + Ok(request_builder) +} diff --git a/canister/src/rpc_client/mod.rs b/canister/src/rpc_client/mod.rs index faf405b5..0329aee0 100644 --- a/canister/src/rpc_client/mod.rs +++ b/canister/src/rpc_client/mod.rs @@ -1,47 +1,340 @@ -use crate::providers::PROVIDERS; -use crate::{constants::API_KEY_REPLACE_STRING, state::read_state}; -use ic_cdk::api::management_canister::http_request::HttpHeader; -use sol_rpc_types::{RpcAccess, RpcAuth, RpcEndpoint, RpcSource, SupportedRpcProviderId}; - -pub fn resolve_rpc_provider(service: RpcSource) -> RpcEndpoint { - match service { - RpcSource::Supported(provider_id) => PROVIDERS - .with(|providers| providers.get(&provider_id).cloned()) - .map(|provider| resolve_api_key(provider.access, provider_id)) - .expect("Unknown provider"), - RpcSource::Custom(api) => api, - } -} - -fn resolve_api_key(access: RpcAccess, provider: SupportedRpcProviderId) -> RpcEndpoint { - match &access { - RpcAccess::Authenticated { auth, public_url } => { - let api_key = read_state(|s| s.get_api_key(&provider)); - match api_key { - Some(api_key) => match auth { - RpcAuth::BearerToken { url } => RpcEndpoint { - url: url.to_string(), - headers: Some(vec![HttpHeader { - name: "Authorization".to_string(), - value: format!("Bearer {}", api_key.read()), - }]), - }, - RpcAuth::UrlParameter { url_pattern } => RpcEndpoint { - url: url_pattern.replace(API_KEY_REPLACE_STRING, api_key.read()), - headers: None, - }, - }, - None => RpcEndpoint { - url: public_url.clone().unwrap_or_else(|| { - panic!("API key not yet initialized for provider: {:?}", provider) - }), - headers: None, - }, +mod sol_rpc; +#[cfg(test)] +mod tests; + +use crate::{ + logs::Priority, + providers::Providers, + rpc_client::sol_rpc::{ResponseSizeEstimate, ResponseTransform, HEADER_SIZE_LIMIT}, +}; +use canlog::log; +use serde::{de::DeserializeOwned, Serialize}; +use sol_rpc_types::{ + ConsensusStrategy, GetSlotParams, ProviderError, RpcConfig, RpcError, RpcResult, RpcSource, + RpcSources, +}; +use solana_clock::Slot; +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt::Debug, +}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SolRpcClient { + providers: Providers, + config: RpcConfig, +} + +impl SolRpcClient { + pub fn new(source: RpcSources, config: Option) -> Result { + let config = config.unwrap_or_default(); + let strategy = config.response_consensus.clone().unwrap_or_default(); + Ok(Self { + providers: Providers::new(source, strategy)?, + config, + }) + } + + fn providers(&self) -> &BTreeSet { + &self.providers.sources + } + + fn response_size_estimate(&self, estimate: u64) -> ResponseSizeEstimate { + ResponseSizeEstimate::new(self.config.response_size_estimate.unwrap_or(estimate)) + } + + fn consensus_strategy(&self) -> ConsensusStrategy { + self.config + .response_consensus + .as_ref() + .cloned() + .unwrap_or_default() + } + + /// Query all providers in parallel and return all results. + /// It's up to the caller to decide how to handle the results, which could be inconsistent + /// (e.g., if different providers gave different responses). + /// This method is useful for querying data that is critical for the system to ensure that + /// there is no single point of failure. + async fn parallel_call( + &self, + method: impl Into + Clone, + params: I, + response_size_estimate: ResponseSizeEstimate, + response_transform: &Option, + ) -> MultiCallResults + where + I: Serialize + Clone + Debug, + O: Debug + DeserializeOwned, + { + let providers = self.providers(); + let results = { + let mut fut = Vec::with_capacity(providers.len()); + for provider in providers { + log!( + Priority::Debug, + "[parallel_call]: will call provider: {:?}", + provider + ); + fut.push(async { + sol_rpc::call::<_, _>( + provider, + method.clone(), + params.clone(), + response_size_estimate, + response_transform, + ) + .await + }); + } + futures::future::join_all(fut).await + }; + MultiCallResults::from_non_empty_iter(providers.iter().cloned().zip(results.into_iter())) + } + + /// Query the Solana [`getSlot`](https://solana.com/docs/rpc/http/getslot) RPC method. + pub async fn get_slot(&self, params: GetSlotParams) -> Result> { + self.parallel_call( + "getSlot", + vec![params], + self.response_size_estimate(1024 + HEADER_SIZE_LIMIT), + &Some(ResponseTransform::GetSlot), + ) + .await + .reduce(self.consensus_strategy()) + } +} + +/// Aggregates responses of different providers to the same query. +/// Guaranteed to be non-empty. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MultiCallResults { + ok_results: BTreeMap, + errors: BTreeMap, +} + +impl Default for MultiCallResults { + fn default() -> Self { + Self::new() + } +} + +impl MultiCallResults { + pub fn new() -> Self { + Self { + ok_results: BTreeMap::new(), + errors: BTreeMap::new(), + } + } + + pub fn from_non_empty_iter)>>(iter: I) -> Self { + let mut results = Self::new(); + for (provider, result) in iter { + results.insert_once(provider, result); + } + if results.is_empty() { + panic!("BUG: MultiCallResults cannot be empty!") + } + results + } + + fn is_empty(&self) -> bool { + self.ok_results.is_empty() && self.errors.is_empty() + } + + fn insert_once(&mut self, provider: RpcSource, result: RpcResult) { + match result { + Ok(value) => { + assert!(!self.errors.contains_key(&provider)); + assert!(self.ok_results.insert(provider, value).is_none()); + } + Err(error) => { + assert!(!self.ok_results.contains_key(&provider)); + assert!(self.errors.insert(provider, error).is_none()); + } + } + } + + pub fn into_vec(self) -> Vec<(RpcSource, RpcResult)> { + self.ok_results + .into_iter() + .map(|(provider, result)| (provider, Ok(result))) + .chain( + self.errors + .into_iter() + .map(|(provider, error)| (provider, Err(error))), + ) + .collect() + } + + fn group_errors(&self) -> BTreeMap<&RpcError, BTreeSet<&RpcSource>> { + let mut errors: BTreeMap<_, _> = BTreeMap::new(); + for (provider, error) in self.errors.iter() { + errors + .entry(error) + .or_insert_with(BTreeSet::new) + .insert(provider); + } + errors + } +} + +impl MultiCallResults { + /// Expects all results to be ok or return the following error: + /// * MultiCallError::ConsistentError: all errors are the same and there is no ok results. + /// * MultiCallError::InconsistentResults: in all other cases. + fn all_ok(self) -> Result, MultiCallError> { + if self.errors.is_empty() { + return Ok(self.ok_results); + } + Err(self.expect_error()) + } + + fn expect_error(self) -> MultiCallError { + let errors = self.group_errors(); + match errors.len() { + 0 => { + panic!("BUG: errors should be non-empty") + } + 1 if self.ok_results.is_empty() => { + MultiCallError::ConsistentError(errors.into_keys().next().unwrap().clone()) + } + _ => MultiCallError::InconsistentResults(self), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum MultiCallError { + ConsistentError(RpcError), + InconsistentResults(MultiCallResults), +} + +impl MultiCallResults { + pub fn reduce(self, strategy: ConsensusStrategy) -> Result> { + match strategy { + ConsensusStrategy::Equality => self.reduce_with_equality(), + ConsensusStrategy::Threshold { total: _, min } => self.reduce_with_threshold(min), + } + } + + fn reduce_with_equality(self) -> Result> { + let mut results = self.all_ok()?.into_iter(); + let (base_node_provider, base_result) = results + .next() + .expect("BUG: MultiCallResults is guaranteed to be non-empty"); + let mut inconsistent_results: Vec<_> = results + .filter(|(_provider, result)| result != &base_result) + .collect(); + if !inconsistent_results.is_empty() { + inconsistent_results.push((base_node_provider, base_result)); + let error = MultiCallError::InconsistentResults(MultiCallResults::from_non_empty_iter( + inconsistent_results + .into_iter() + .map(|(provider, result)| (provider, Ok(result))), + )); + log!( + Priority::Info, + "[reduce_with_equality]: inconsistent results {error:?}" + ); + return Err(error); + } + Ok(base_result) + } + + fn reduce_with_threshold(self, min: u8) -> Result> { + assert!(min > 0, "BUG: min must be greater than 0"); + if self.ok_results.len() < min as usize { + // At least total >= min were queried, + // so there is at least one error + return Err(self.expect_error()); + } + let distribution = ResponseDistribution::from_non_empty_iter(self.ok_results.clone()); + let (most_likely_response, providers) = distribution + .most_frequent() + .expect("BUG: distribution should be non-empty"); + if providers.len() >= min as usize { + Ok(most_likely_response.clone()) + } else { + log!( + Priority::Info, + "[reduce_with_threshold]: too many inconsistent ok responses to reach threshold of {min}, results: {self:?}" + ); + Err(MultiCallError::InconsistentResults(self)) + } + } +} + +/// Distribution of responses observed from different providers. +/// +/// From the API point of view, it emulates a map from a response instance to a set of providers that returned it. +/// At the implementation level, to avoid requiring `T` to have a total order (i.e., must implements `Ord` if it were to be used as keys in a `BTreeMap`) which might not always be meaningful, +/// we use as key the hash of the serialized response instance. +struct ResponseDistribution { + hashes: BTreeMap<[u8; 32], T>, + responses: BTreeMap<[u8; 32], BTreeSet>, +} + +impl Default for ResponseDistribution { + fn default() -> Self { + Self::new() + } +} + +impl ResponseDistribution { + pub fn new() -> Self { + Self { + hashes: BTreeMap::new(), + responses: BTreeMap::new(), + } + } + + /// Returns the most frequent response and the set of providers that returned it. + pub fn most_frequent(&self) -> Option<(&T, &BTreeSet)> { + self.responses + .iter() + .max_by_key(|(_hash, providers)| providers.len()) + .map(|(hash, providers)| { + ( + self.hashes.get(hash).expect("BUG: hash should be present"), + providers, + ) + }) + } +} + +impl ResponseDistribution { + pub fn from_non_empty_iter>(iter: I) -> Self { + let mut distribution = Self::new(); + for (provider, result) in iter { + distribution.insert_once(provider, result); + } + distribution + } + + pub fn insert_once(&mut self, provider: RpcSource, result: T) { + use ic_sha3::Keccak256; + let hash = Keccak256::hash(serde_json::to_vec(&result).expect("BUG: failed to serialize")); + match self.hashes.get(&hash) { + Some(existing_result) => { + assert_eq!( + existing_result, &result, + "BUG: different results once serialized have the same hash" + ); + let providers = self + .responses + .get_mut(&hash) + .expect("BUG: hash is guaranteed to be present"); + assert!( + providers.insert(provider), + "BUG: provider is already present" + ); + } + None => { + assert_eq!(self.hashes.insert(hash, result), None); + let providers = BTreeSet::from_iter(std::iter::once(provider)); + assert_eq!(self.responses.insert(hash, providers), None); } } - RpcAccess::Unauthenticated { public_url } => RpcEndpoint { - url: public_url.to_string(), - headers: None, - }, } } diff --git a/canister/src/rpc_client/sol_rpc/mod.rs b/canister/src/rpc_client/sol_rpc/mod.rs new file mode 100644 index 00000000..3cab5fe3 --- /dev/null +++ b/canister/src/rpc_client/sol_rpc/mod.rs @@ -0,0 +1,151 @@ +#[cfg(test)] +mod tests; + +use crate::{ + http::http_client, + providers::{request_builder, resolve_rpc_provider}, + state::read_state, +}; +use candid::candid_method; +use canhttp::{ + http::json::{JsonRpcRequest, JsonRpcResponse}, + MaxResponseBytesRequestExtension, TransformContextRequestExtension, +}; +use ic_cdk::{ + api::management_canister::http_request::{HttpResponse, TransformArgs, TransformContext}, + query, +}; +use minicbor::{Decode, Encode}; +use serde::{de::DeserializeOwned, Serialize}; +use sol_rpc_types::{JsonRpcError, RpcError, RpcSource}; +use solana_clock::Slot; +use std::{fmt, fmt::Debug}; + +// This constant is our approximation of the expected header size. +// The HTTP standard doesn't define any limit, and many implementations limit +// the headers size to 8 KiB. We chose a lower limit because headers observed on most providers +// fit in the constant defined below, and if there is a spike, then the payload size adjustment +// should take care of that. +pub const HEADER_SIZE_LIMIT: u64 = 2 * 1024; + +// This constant comes from the IC specification: +// > If provided, the value must not exceed 2MB +const HTTP_MAX_SIZE: u64 = 2_000_000; + +pub const MAX_PAYLOAD_SIZE: u64 = HTTP_MAX_SIZE - HEADER_SIZE_LIMIT; + +/// Describes a payload transformation to execute before passing the HTTP response to consensus. +/// The purpose of these transformations is to ensure that the response encoding is deterministic +/// (the field order is the same). +#[derive(Debug, Decode, Encode)] +pub enum ResponseTransform { + #[n(0)] + GetSlot, +} + +impl ResponseTransform { + fn apply(&self, body_bytes: &mut Vec) { + fn redact_response(body: &mut Vec) + where + T: Serialize + DeserializeOwned, + { + let response: JsonRpcResponse = match serde_json::from_slice(body) { + Ok(response) => response, + Err(_) => return, + }; + *body = serde_json::to_vec(&response).expect("BUG: failed to serialize response"); + } + + match self { + // TODO XC-292: Add rounding to the response transform and + // add a unit test simulating consensus when the providers + // return slightly differing results. + Self::GetSlot => redact_response::(body_bytes), + } + } +} + +#[query] +#[candid_method(query)] +fn cleanup_response(mut args: TransformArgs) -> HttpResponse { + args.response.headers.clear(); + let status_ok = args.response.status >= 200u16 && args.response.status < 300u16; + if status_ok && !args.context.is_empty() { + let maybe_transform: Result = minicbor::decode(&args.context[..]); + if let Ok(transform) = maybe_transform { + transform.apply(&mut args.response.body); + } + } + args.response +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct ResponseSizeEstimate(u64); + +impl ResponseSizeEstimate { + pub fn new(num_bytes: u64) -> Self { + assert!(num_bytes > 0); + assert!(num_bytes <= MAX_PAYLOAD_SIZE); + Self(num_bytes) + } + + /// Describes the expected (90th percentile) number of bytes in the HTTP response body. + /// This number should be less than `MAX_PAYLOAD_SIZE`. + pub fn get(self) -> u64 { + self.0 + } +} + +impl fmt::Display for ResponseSizeEstimate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Calls a JSON-RPC method at the specified URL. +pub async fn call( + provider: &RpcSource, + method: impl Into, + params: I, + response_size_estimate: ResponseSizeEstimate, + response_transform: &Option, +) -> Result +where + I: Serialize + Clone + Debug, + O: Debug + DeserializeOwned, +{ + use tower::Service; + + let transform_op = response_transform + .as_ref() + .map(|t| { + let mut buf = vec![]; + minicbor::encode(t, &mut buf).unwrap(); + buf + }) + .unwrap_or_default(); + + let effective_size_estimate = response_size_estimate.get(); + let request = request_builder( + resolve_rpc_provider(provider.clone()), + &read_state(|state| state.get_override_provider()), + )? + .max_response_bytes(effective_size_estimate) + .transform_context(TransformContext::from_name( + "cleanup_response".to_owned(), + transform_op.clone(), + )) + .body(JsonRpcRequest::new(method, params)) + .expect("BUG: invalid request"); + + let mut client = http_client(true); + let response = client.call(request).await?; + match response.into_body().into_result() { + Ok(r) => Ok(r), + Err(canhttp::http::json::JsonRpcError { + code, + message, + data: _, + }) => Err(JsonRpcError { code, message }.into()), + } +} diff --git a/canister/src/rpc_client/sol_rpc/tests.rs b/canister/src/rpc_client/sol_rpc/tests.rs new file mode 100644 index 00000000..2fb7efb2 --- /dev/null +++ b/canister/src/rpc_client/sol_rpc/tests.rs @@ -0,0 +1 @@ +// TODO XC-292: Add unit tests diff --git a/canister/src/rpc_client/tests.rs b/canister/src/rpc_client/tests.rs new file mode 100644 index 00000000..2d31732f --- /dev/null +++ b/canister/src/rpc_client/tests.rs @@ -0,0 +1,49 @@ +// TODO XC-292: Add more unit tests + +mod sol_rpc_client { + use crate::rpc_client::SolRpcClient; + use assert_matches::assert_matches; + use maplit::btreeset; + use sol_rpc_types::{ + ProviderError, RpcSource, RpcSources, SolanaCluster, SupportedRpcProviderId, + }; + + #[test] + fn should_fail_when_providers_explicitly_set_to_empty() { + assert_matches!( + SolRpcClient::new(RpcSources::Custom(vec![]), None), + Err(ProviderError::InvalidRpcConfig(_)) + ); + } + + #[test] + fn should_use_default_providers() { + for cluster in [SolanaCluster::Mainnet, SolanaCluster::Devnet] { + let client = SolRpcClient::new(RpcSources::Default(cluster), None).unwrap(); + assert!(!client.providers().is_empty()); + } + } + + #[test] + fn should_use_specified_provider() { + let provider1 = SupportedRpcProviderId::AlchemyMainnet; + let provider2 = SupportedRpcProviderId::PublicNodeMainnet; + + let client = SolRpcClient::new( + RpcSources::Custom(vec![ + RpcSource::Supported(provider1), + RpcSource::Supported(provider2), + ]), + None, + ) + .unwrap(); + + assert_eq!( + client.providers(), + &btreeset! { + RpcSource::Supported(provider1), + RpcSource::Supported(provider2), + } + ); + } +} diff --git a/canister/src/state/mod.rs b/canister/src/state/mod.rs index 44e32ef6..98cc752a 100644 --- a/canister/src/state/mod.rs +++ b/canister/src/state/mod.rs @@ -3,6 +3,7 @@ mod tests; use crate::types::{ApiKey, OverrideProvider}; use candid::{Deserialize, Principal}; +use canhttp::http::json::Id; use canlog::LogFilter; use ic_stable_structures::{ memory_manager::{MemoryId, MemoryManager, VirtualMemory}, @@ -10,7 +11,7 @@ use ic_stable_structures::{ Cell, DefaultMemoryImpl, Storable, }; use serde::Serialize; -use sol_rpc_types::{InstallArgs, SupportedRpcProviderId}; +use sol_rpc_types::{InstallArgs, Mode, SupportedRpcProviderId}; use std::{borrow::Cow, cell::RefCell, collections::BTreeMap}; const STATE_MEMORY_ID: MemoryId = MemoryId::new(0); @@ -19,7 +20,7 @@ type StableMemory = VirtualMemory; thread_local! { // Unstable static data: these are reset when the canister is upgraded. - // TODO: Add metrics + static UNSTABLE_HTTP_REQUEST_COUNTER: RefCell = const {RefCell::new(0)}; // Stable static data: these are preserved when the canister is upgraded. static MEMORY_MANAGER: RefCell> = @@ -86,6 +87,8 @@ pub struct State { api_key_principals: Vec, override_provider: OverrideProvider, log_filter: LogFilter, + mode: Mode, + num_subnet_nodes: u32, } impl State { @@ -131,6 +134,22 @@ impl State { pub fn set_log_filter(&mut self, filter: LogFilter) { self.log_filter = filter; } + + pub fn get_num_subnet_nodes(&self) -> u32 { + self.num_subnet_nodes + } + + pub fn set_num_subnet_nodes(&mut self, num_subnet_nodes: u32) { + self.num_subnet_nodes = num_subnet_nodes + } + + pub fn get_mode(&self) -> Mode { + self.mode + } + + pub fn set_mode(&mut self, mode: Mode) { + self.mode = mode + } } impl From for State { @@ -140,6 +159,8 @@ impl From for State { api_key_principals: value.manage_api_keys.unwrap_or_default(), override_provider: value.override_provider.unwrap_or_default().into(), log_filter: value.log_filter.unwrap_or_default(), + mode: value.mode.unwrap_or_default(), + num_subnet_nodes: value.num_subnet_nodes.unwrap_or_default().into(), } } } @@ -190,3 +211,13 @@ pub fn reset_state() { .unwrap_or_else(|err| panic!("Could not reset state: {:?}", err)); }) } + +pub fn next_request_id() -> Id { + UNSTABLE_HTTP_REQUEST_COUNTER.with_borrow_mut(|counter| { + let current_request_id = *counter; + // overflow is not an issue here because we only use `next_request_id` to correlate + // requests and responses in logs. + *counter = counter.wrapping_add(1); + Id::from(current_request_id) + }) +} diff --git a/canister/src/types/mod.rs b/canister/src/types/mod.rs index 2f358eb0..970bc8e0 100644 --- a/canister/src/types/mod.rs +++ b/canister/src/types/mod.rs @@ -3,8 +3,8 @@ mod tests; use crate::{constants::API_KEY_REPLACE_STRING, validate::validate_api_key}; use serde::{Deserialize, Serialize}; -use sol_rpc_types::{RegexSubstitution, RpcEndpoint}; -use std::fmt; +use sol_rpc_types::{RegexSubstitution, RpcEndpoint, RpcResult, RpcSource}; +use std::{fmt, fmt::Debug}; use zeroize::{Zeroize, ZeroizeOnDrop}; #[derive(Clone, PartialEq, Zeroize, ZeroizeOnDrop, Deserialize, Serialize)] @@ -18,7 +18,7 @@ impl ApiKey { } /// Enable printing data structures which include an API key -impl fmt::Debug for ApiKey { +impl Debug for ApiKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{API_KEY_REPLACE_STRING}") } @@ -74,3 +74,69 @@ impl OverrideProvider { } } } + +/// Copy of [`sol_rpc_types::MultiRpcResult`] to keep the implementation details out of the +/// [`sol_rpc_types`] crate. +pub enum MultiRpcResult { + Consistent(RpcResult), + Inconsistent(Vec<(RpcSource, RpcResult)>), +} + +impl MultiRpcResult { + pub fn map(self, mut f: impl FnMut(T) -> R) -> MultiRpcResult { + match self { + MultiRpcResult::Consistent(result) => MultiRpcResult::Consistent(result.map(f)), + MultiRpcResult::Inconsistent(results) => MultiRpcResult::Inconsistent( + results + .into_iter() + .map(|(service, result)| { + ( + service, + match result { + Ok(ok) => Ok(f(ok)), + Err(err) => Err(err), + }, + ) + }) + .collect(), + ), + } + } +} + +impl MultiRpcResult { + pub fn expect_consistent(self) -> RpcResult { + match self { + MultiRpcResult::Consistent(result) => result, + MultiRpcResult::Inconsistent(inconsistent_result) => { + panic!("Expected consistent, but got: {:?}", inconsistent_result) + } + } + } + + pub fn expect_inconsistent(self) -> Vec<(RpcSource, RpcResult)> { + match self { + MultiRpcResult::Consistent(consistent_result) => { + panic!("Expected inconsistent:, but got: {:?}", consistent_result) + } + MultiRpcResult::Inconsistent(results) => results, + } + } +} + +impl From> for MultiRpcResult { + fn from(result: RpcResult) -> Self { + MultiRpcResult::Consistent(result) + } +} + +impl From> for sol_rpc_types::MultiRpcResult { + fn from(value: MultiRpcResult) -> Self { + match value { + MultiRpcResult::Consistent(result) => sol_rpc_types::MultiRpcResult::Consistent(result), + MultiRpcResult::Inconsistent(result) => { + sol_rpc_types::MultiRpcResult::Inconsistent(result) + } + } + } +} diff --git a/canister/src/types/tests.rs b/canister/src/types/tests.rs index 59de4e54..d3bb6f0d 100644 --- a/canister/src/types/tests.rs +++ b/canister/src/types/tests.rs @@ -1,6 +1,5 @@ use crate::{ - providers::PROVIDERS, - rpc_client::resolve_rpc_provider, + providers::{resolve_rpc_provider, PROVIDERS}, state::{init_state, reset_state, State}, types::{ApiKey, OverrideProvider}, }; diff --git a/examples/basic_solana/Cargo.toml b/examples/basic_solana/Cargo.toml index 00642ef5..6ab729b4 100644 --- a/examples/basic_solana/Cargo.toml +++ b/examples/basic_solana/Cargo.toml @@ -16,7 +16,8 @@ candid = { workspace = true } getrandom = { workspace = true, default-features = false, features = ["custom"] } ic-cdk = { workspace = true } ic-ed25519 = { workspace = true } -num = "0.4.3" +http = { workspace = true } +num = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } solana-hash = { workspace = true } diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 480a2884..a1548abe 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -1,10 +1,12 @@ use async_trait::async_trait; -use candid::utils::ArgumentEncoder; -use candid::{decode_args, encode_args, CandidType, Encode, Principal}; +use candid::{decode_args, encode_args, utils::ArgumentEncoder, CandidType, Encode, Principal}; use canlog::{Log, LogEntry}; use ic_cdk::api::call::RejectionCode; -use pocket_ic::management_canister::{CanisterId, CanisterSettings}; -use pocket_ic::{nonblocking::PocketIc, PocketIcBuilder, RejectCode, RejectResponse}; +use pocket_ic::{ + management_canister::{CanisterId, CanisterSettings}, + nonblocking::PocketIc, + PocketIcBuilder, RejectCode, RejectResponse, +}; use serde::de::DeserializeOwned; use sol_rpc_canister::{ http_types::{HttpRequest, HttpResponse}, @@ -12,8 +14,7 @@ use sol_rpc_canister::{ }; use sol_rpc_client::{Runtime, SolRpcClient}; use sol_rpc_types::{InstallArgs, SupportedRpcProviderId}; -use std::path::PathBuf; -use std::time::Duration; +use std::{path::PathBuf, time::Duration}; pub const DEFAULT_CALLER_TEST_ID: Principal = Principal::from_slice(&[0x9d, 0xf7, 0x01]); pub const DEFAULT_CONTROLLER_TEST_ID: Principal = Principal::from_slice(&[0x9d, 0xf7, 0x02]); diff --git a/integration_tests/tests/solana_test_validator.rs b/integration_tests/tests/solana_test_validator.rs index 34dfd69e..86c24d4c 100644 --- a/integration_tests/tests/solana_test_validator.rs +++ b/integration_tests/tests/solana_test_validator.rs @@ -6,7 +6,7 @@ use futures::future; use pocket_ic::PocketIcBuilder; use sol_rpc_client::SolRpcClient; use sol_rpc_int_tests::PocketIcLiveModeRuntime; -use sol_rpc_types::{InstallArgs, OverrideProvider, RegexSubstitution}; +use sol_rpc_types::{InstallArgs, Mode, MultiRpcResult, OverrideProvider, RegexSubstitution}; use solana_client::rpc_client::RpcClient as SolanaRpcClient; use std::future::Future; @@ -17,7 +17,12 @@ async fn should_get_slot() { let (sol_res, ic_res) = setup .compare_client( |sol| sol.get_slot().expect("Failed to get slot"), - |ic| async move { ic.get_slot().await }, + |ic| async move { + match ic.get_slot(None).await { + MultiRpcResult::Consistent(Ok(slot)) => slot, + result => panic!("Failed to get slot, received: {:?}", result), + } + }, ) .await; @@ -49,6 +54,8 @@ impl Setup { setup: sol_rpc_int_tests::Setup::with_pocket_ic_and_args( pic, InstallArgs { + // TODO XC-323: handle cycles properly + mode: Some(Mode::Demo), override_provider: Some(OverrideProvider { override_url: Some(RegexSubstitution { pattern: ".*".into(), diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 9d09951f..a5411f5e 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -4,12 +4,12 @@ #![forbid(missing_docs)] use async_trait::async_trait; -use candid::utils::ArgumentEncoder; -use candid::{CandidType, Principal}; +use candid::{utils::ArgumentEncoder, CandidType, Principal}; use ic_cdk::api::call::RejectionCode; use serde::de::DeserializeOwned; use sol_rpc_types::{ - RpcConfig, RpcSources, SolanaCluster, SupportedRpcProvider, SupportedRpcProviderId, + GetSlotParams, RpcConfig, RpcSources, SolanaCluster, SupportedRpcProvider, + SupportedRpcProviderId, }; use solana_clock::Slot; @@ -97,8 +97,10 @@ impl SolRpcClient { } /// Call `getSlot` on the SOL RPC canister. - //TODO XC-292: change me! - pub async fn get_slot(&self) -> Slot { + pub async fn get_slot( + &self, + params: Option, + ) -> sol_rpc_types::MultiRpcResult { self.runtime .update_call( self.sol_rpc_canister, @@ -106,6 +108,7 @@ impl SolRpcClient { ( RpcSources::Default(SolanaCluster::Devnet), None::, + params, ), 1_000_000_000, ) diff --git a/libs/types/Cargo.toml b/libs/types/Cargo.toml index 40798c75..1ba6bcad 100644 --- a/libs/types/Cargo.toml +++ b/libs/types/Cargo.toml @@ -12,9 +12,10 @@ include = ["src", "Cargo.toml", "CHANGELOG.md", "LICENSE", "README.md"] [dependencies] candid = { workspace = true } +canlog = { path = "../../canlog" } ic-cdk = { workspace = true } regex = { workspace = true } serde = { workspace = true } -canlog = { path = "../../canlog" } -strum = { workspace = true } +thiserror = { workspace = true } url = { workspace = true } +derive_more = { workspace = true } diff --git a/libs/types/src/lib.rs b/libs/types/src/lib.rs index 1f84af0e..ef72aa00 100644 --- a/libs/types/src/lib.rs +++ b/libs/types/src/lib.rs @@ -4,11 +4,15 @@ #![forbid(missing_docs)] mod lifecycle; +mod response; mod rpc_client; +mod solana; -pub use lifecycle::InstallArgs; +pub use lifecycle::{InstallArgs, Mode, NumSubnetNodes}; +pub use response::MultiRpcResult; pub use rpc_client::{ - ConsensusStrategy, HttpHeader, OverrideProvider, RegexString, RegexSubstitution, RpcAccess, - RpcAuth, RpcConfig, RpcEndpoint, RpcSource, RpcSources, SolanaCluster, SupportedRpcProvider, - SupportedRpcProviderId, + ConsensusStrategy, HttpHeader, HttpOutcallError, JsonRpcError, OverrideProvider, ProviderError, + RegexString, RegexSubstitution, RpcAccess, RpcAuth, RpcConfig, RpcEndpoint, RpcError, + RpcResult, RpcSource, RpcSources, SolanaCluster, SupportedRpcProvider, SupportedRpcProviderId, }; +pub use solana::{CommitmentLevel, GetSlotParams}; diff --git a/libs/types/src/lifecycle/mod.rs b/libs/types/src/lifecycle/mod.rs index 93b2634a..294e4532 100644 --- a/libs/types/src/lifecycle/mod.rs +++ b/libs/types/src/lifecycle/mod.rs @@ -1,7 +1,7 @@ use crate::OverrideProvider; use candid::{CandidType, Principal}; use canlog::LogFilter; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; /// The installation args for the Solana RPC canister. #[derive(Clone, Debug, Default, CandidType, Deserialize)] @@ -15,4 +15,41 @@ pub struct InstallArgs { /// Only log entries matching this filter will be recorded. #[serde(rename = "logFilter")] pub log_filter: Option, + /// Number of subnet nodes. + #[serde(rename = "numSubnetNodes")] + pub num_subnet_nodes: Option, + /// Mode of operation. Default is `Mode::Normal`. + pub mode: Option, +} + +/// Mode of operation +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, CandidType, Deserialize, Serialize)] +pub enum Mode { + #[default] + /// Normal mode, where cycle payment is required for certain operations. + Normal, + /// Demo mode, where cycle payment is not required. + Demo, +} + +/// Number of subnet nodes with a default value set to 34. +#[derive(Debug, Copy, Clone, CandidType, Deserialize, Serialize)] +pub struct NumSubnetNodes(u32); + +impl Default for NumSubnetNodes { + fn default() -> Self { + NumSubnetNodes(34) + } +} + +impl From for u32 { + fn from(nodes: NumSubnetNodes) -> u32 { + nodes.0 + } +} + +impl From for NumSubnetNodes { + fn from(nodes: u32) -> Self { + NumSubnetNodes(nodes) + } } diff --git a/libs/types/src/response/mod.rs b/libs/types/src/response/mod.rs new file mode 100644 index 00000000..813b3626 --- /dev/null +++ b/libs/types/src/response/mod.rs @@ -0,0 +1,19 @@ +use crate::{RpcResult, RpcSource}; +use candid::CandidType; +use serde::Deserialize; + +/// Represents an aggregated result from multiple RPC calls to different RPC providers. +/// The results are aggregated using a [`crate::ConsensusStrategy`]. +#[derive(Clone, Debug, Eq, PartialEq, CandidType, Deserialize)] +pub enum MultiRpcResult { + /// The results from the different providers were consistent. + Consistent(RpcResult), + /// The results from the different providers were not consistent. + Inconsistent(Vec<(RpcSource, RpcResult)>), +} + +impl From> for MultiRpcResult { + fn from(result: RpcResult) -> Self { + MultiRpcResult::Consistent(result) + } +} diff --git a/libs/types/src/rpc_client/mod.rs b/libs/types/src/rpc_client/mod.rs index dd66c288..a43af238 100644 --- a/libs/types/src/rpc_client/mod.rs +++ b/libs/types/src/rpc_client/mod.rs @@ -2,10 +2,88 @@ mod tests; use candid::CandidType; +use derive_more::From; +use ic_cdk::api::call::RejectionCode; pub use ic_cdk::api::management_canister::http_request::HttpHeader; use regex::Regex; use serde::{Deserialize, Serialize}; use std::fmt::Debug; +use thiserror::Error; + +/// An RPC result type. +pub type RpcResult = Result; + +/// An RPC error. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, CandidType, Deserialize, Error, From)] +pub enum RpcError { + /// An error occurred with the RPC provider. + #[error("Provider error: {0}")] + ProviderError(ProviderError), + /// An error occurred with the HTTP outcall. + #[error("HTTP outcall error: {0}")] + HttpOutcallError(HttpOutcallError), + /// A JSON-RPC error occurred. + #[error("JSON-RPC error: {0}")] + JsonRpcError(JsonRpcError), + /// A validation error occurred. + #[error("Validation error: {0}")] + ValidationError(String), +} + +/// An error with an RPC provider. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, CandidType, Deserialize, Error)] +pub enum ProviderError { + /// Attempted to make an HTTP outcall with an insufficient amount of cycles. + #[error("Not enough cycles, expected {expected}, received {received}")] + TooFewCycles { + /// Expected to receive this many cycles. + expected: u128, + /// Received this many cycles. + received: u128, + }, + /// The [`RpcConfig`] was invalid. + #[error("Invalid RPC config: {0}")] + InvalidRpcConfig(String), + /// The [`SolanaCluster`] is not supported. + #[error("Unsupported Solana cluster: {0}")] + UnsupportedCluster(String), +} + +/// An HTTP outcall error. +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, CandidType, Deserialize, Error)] +pub enum HttpOutcallError { + /// Error from the IC system API. + #[error("IC error (code: {code:?}): {message}")] + IcError { + /// The error code. + code: RejectionCode, + /// The error message. + message: String, + }, + /// Response is not a valid JSON-RPC response, + /// which means that the response was not successful (status other than 2xx) + /// or that the response body could not be deserialized into a JSON-RPC response. + #[error("Invalid HTTP JSON-RPC response: status {status}, body: {body}, parsing error: {parsing_error:?}")] + InvalidHttpJsonRpcResponse { + /// The HTTP status code returned. + status: u16, + /// The serialized response body. + body: String, + /// The parsing error message. + #[serde(rename = "parsingError")] + parsing_error: Option, + }, +} + +/// A JSON-RPC 2.0 error as per the [specifications](https://www.jsonrpc.org/specification#error_object). +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, CandidType, Deserialize, Error)] +#[error("JSON-RPC error (code: {code}): {message}")] +pub struct JsonRpcError { + /// The error code. See the specifications for a detailed list of error codes. + pub code: i64, + /// The error message. + pub message: String, +} /// Configures how to perform RPC HTTP calls. #[derive(Clone, Debug, PartialEq, Eq, Default, CandidType, Deserialize)] diff --git a/libs/types/src/solana/mod.rs b/libs/types/src/solana/mod.rs new file mode 100644 index 00000000..41179e5d --- /dev/null +++ b/libs/types/src/solana/mod.rs @@ -0,0 +1,28 @@ +use candid::CandidType; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +/// The parameters for a Solana [`getSlot`](https://solana.com/docs/rpc/http/getslot) RPC method call. +#[derive(Debug, Clone, Default, Deserialize, Serialize, CandidType)] +pub struct GetSlotParams { + /// The request returns the slot that has reached this or the default commitment level. + pub commitment: Option, + /// The minimum slot that the request can be evaluated at. + #[serde(rename = "minContextSlot")] + pub min_context_slot: Option, +} + +/// [Commitment levels](https://solana.com/docs/rpc#configuring-state-commitment) in Solana, +/// representing finality guarantees of transactions and state queries. +#[derive(Debug, Clone, Deserialize, Serialize, CandidType)] +pub enum CommitmentLevel { + /// The transaction is processed by a leader, but may be dropped. + #[serde(rename = "processed")] + Processed, + /// The transaction has been included in a block that has reached 1 confirmation. + #[serde(rename = "confirmed")] + Confirmed, + /// The transaction is finalized and cannot be rolled back. + #[serde(rename = "finalized")] + Finalized, +}