diff --git a/apps/argus/.gitignore b/apps/argus/.gitignore new file mode 100644 index 0000000000..7e54106f12 --- /dev/null +++ b/apps/argus/.gitignore @@ -0,0 +1,4 @@ +/target +*config.yaml +*secret* +*private-key* diff --git a/apps/argus/Cargo.lock b/apps/argus/Cargo.lock new file mode 100644 index 0000000000..9d7f9a6cbc --- /dev/null +++ b/apps/argus/Cargo.lock @@ -0,0 +1,5056 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "argus" +version = "0.0.1" +dependencies = [ + "anyhow", + "axum", + "axum-macros", + "axum-test", + "backoff", + "base64 0.21.4", + "bincode", + "byteorder", + "chrono", + "clap", + "ethabi", + "ethers", + "fortuna", + "futures", + "futures-locks", + "hex", + "lazy_static", + "mockito", + "once_cell", + "prometheus-client", + "pythnet-sdk", + "rand", + "reqwest", + "serde", + "serde_json", + "serde_qs", + "serde_with", + "serde_yaml", + "sha3", + "thiserror", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", + "url", + "utoipa", + "utoipa-swagger-ui", +] + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-trait" +version = "0.1.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "auto-future" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" + +[[package]] +name = "auto_impl" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee3da8ef1276b0bee5dd1c7258010d8fffd31801447323115a25560e1327b89" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "base64 0.21.4", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.27", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 0.2.9", + "http-body 0.4.5", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-macros" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdca6a10ecad987bda04e95606ef85a5417dcaac1a78455242d72e031e2b6b62" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "axum-test" +version = "13.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e559a1b9b6e81018cd95f2528fc7b333e181191175f34daa9cf3369c7a18fd5" +dependencies = [ + "anyhow", + "async-trait", + "auto-future", + "axum", + "bytes", + "cookie", + "http 0.2.9", + "hyper 0.14.27", + "reserve-port", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tokio", + "tower", + "url", +] + +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "futures-core", + "getrandom", + "instant", + "pin-project-lite", + "rand", + "tokio", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +dependencies = [ + "borsh-derive 0.9.3", + "hashbrown 0.11.2", +] + +[[package]] +name = "borsh" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" +dependencies = [ + "borsh-derive 0.10.3", + "hashbrown 0.12.3", +] + +[[package]] +name = "borsh-derive" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" +dependencies = [ + "borsh-derive-internal 0.9.3", + "borsh-schema-derive-internal 0.9.3", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" +dependencies = [ + "borsh-derive-internal 0.10.3", + "borsh-schema-derive-internal 0.10.3", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bs58" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" +dependencies = [ + "sha2", + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "byte-slice-cast" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" + +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12024c4645c97566567129c204f65d5815a8c9aecf30fcbe682b2fe034996d36" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.5", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "coins-bip32" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b6be4a5df2098cd811f3194f64ddb96c267606bffd9689ac7b0160097b01ad3" +dependencies = [ + "bs58", + "coins-core", + "digest", + "hmac", + "k256", + "serde", + "sha2", + "thiserror", +] + +[[package]] +name = "coins-bip39" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8fba409ce3dc04f7d804074039eb68b960b0829161f8e06c95fea3f122528" +dependencies = [ + "bitvec", + "coins-bip32", + "hmac", + "once_cell", + "pbkdf2 0.12.2", + "rand", + "sha2", + "thiserror", +] + +[[package]] +name = "coins-core" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5286a0843c21f8367f7be734f89df9b822e0321d8bcce8d6e735aadff7d74979" +dependencies = [ + "base64 0.21.4", + "bech32", + "bs58", + "digest", + "generic-array", + "hex", + "ripemd", + "serde", + "serde_derive", + "sha2", + "sha3", + "thiserror", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.52.0", +] + +[[package]] +name = "const-hex" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c37be52ef5e3b394db27a2341010685ad5103c72ac15ce2e9420a7e8f93f342c" +dependencies = [ + "cfg-if", + "cpufeatures", + "hex", + "serde", +] + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 2.0.87", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "dyn-clone" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" + +[[package]] +name = "ecdsa" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "elliptic-curve" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "ena" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533630cf40e9caa44bd91aadc88a75d75a4c3a12b4cfde353cbed41daa1e1f1" +dependencies = [ + "log", +] + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enr" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3d8dc56e02f954cac8eb489772c552c473346fc34f67412bb6244fd647f7e4" +dependencies = [ + "base64 0.21.4", + "bytes", + "hex", + "k256", + "log", + "rand", + "rlp", + "serde", + "sha3", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "eth-keystore" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fda3bf123be441da5260717e0661c25a2fd9cb2b2c1d20bf2e05580047158ab" +dependencies = [ + "aes", + "ctr", + "digest", + "hex", + "hmac", + "pbkdf2 0.11.0", + "rand", + "scrypt", + "serde", + "serde_json", + "sha2", + "sha3", + "thiserror", + "uuid", +] + +[[package]] +name = "ethabi" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" +dependencies = [ + "ethereum-types", + "hex", + "once_cell", + "regex", + "serde", + "serde_json", + "sha3", + "thiserror", + "uint", +] + +[[package]] +name = "ethbloom" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" +dependencies = [ + "crunchy", + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "scale-info", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "primitive-types", + "scale-info", + "uint", +] + +[[package]] +name = "ethers" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816841ea989f0c69e459af1cf23a6b0033b19a55424a1ea3a30099becdb8dec0" +dependencies = [ + "ethers-addressbook", + "ethers-contract", + "ethers-core", + "ethers-etherscan", + "ethers-middleware", + "ethers-providers", + "ethers-signers", + "ethers-solc", +] + +[[package]] +name = "ethers-addressbook" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5495afd16b4faa556c3bba1f21b98b4983e53c1755022377051a975c3b021759" +dependencies = [ + "ethers-core", + "once_cell", + "serde", + "serde_json", +] + +[[package]] +name = "ethers-contract" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fceafa3578c836eeb874af87abacfb041f92b4da0a78a5edd042564b8ecdaaa" +dependencies = [ + "const-hex", + "ethers-contract-abigen", + "ethers-contract-derive", + "ethers-core", + "ethers-providers", + "futures-util", + "once_cell", + "pin-project", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "ethers-contract-abigen" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04ba01fbc2331a38c429eb95d4a570166781f14290ef9fdb144278a90b5a739b" +dependencies = [ + "Inflector", + "const-hex", + "dunce", + "ethers-core", + "ethers-etherscan", + "eyre", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "reqwest", + "serde", + "serde_json", + "syn 2.0.87", + "toml 0.8.12", + "walkdir", +] + +[[package]] +name = "ethers-contract-derive" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87689dcabc0051cde10caaade298f9e9093d65f6125c14575db3fd8c669a168f" +dependencies = [ + "Inflector", + "const-hex", + "ethers-contract-abigen", + "ethers-core", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.87", +] + +[[package]] +name = "ethers-core" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d80cc6ad30b14a48ab786523af33b37f28a8623fc06afd55324816ef18fb1f" +dependencies = [ + "arrayvec", + "bytes", + "cargo_metadata", + "chrono", + "const-hex", + "elliptic-curve", + "ethabi", + "generic-array", + "k256", + "num_enum", + "once_cell", + "open-fastrlp", + "rand", + "rlp", + "serde", + "serde_json", + "strum 0.26.2", + "syn 2.0.87", + "tempfile", + "thiserror", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "ethers-etherscan" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79e5973c26d4baf0ce55520bd732314328cabe53193286671b47144145b9649" +dependencies = [ + "chrono", + "ethers-core", + "reqwest", + "semver", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "ethers-middleware" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f9fdf09aec667c099909d91908d5eaf9be1bd0e2500ba4172c1d28bfaa43de" +dependencies = [ + "async-trait", + "auto_impl", + "ethers-contract", + "ethers-core", + "ethers-etherscan", + "ethers-providers", + "ethers-signers", + "futures-channel", + "futures-locks", + "futures-util", + "instant", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "tracing-futures", + "url", +] + +[[package]] +name = "ethers-providers" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6434c9a33891f1effc9c75472e12666db2fa5a0fec4b29af6221680a6fe83ab2" +dependencies = [ + "async-trait", + "auto_impl", + "base64 0.21.4", + "bytes", + "const-hex", + "enr", + "ethers-core", + "futures-channel", + "futures-core", + "futures-timer", + "futures-util", + "hashers", + "http 0.2.9", + "instant", + "jsonwebtoken", + "once_cell", + "pin-project", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-tungstenite", + "tracing", + "tracing-futures", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "ws_stream_wasm", +] + +[[package]] +name = "ethers-signers" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228875491c782ad851773b652dd8ecac62cda8571d3bc32a5853644dd26766c2" +dependencies = [ + "async-trait", + "coins-bip32", + "coins-bip39", + "const-hex", + "elliptic-curve", + "eth-keystore", + "ethers-core", + "rand", + "sha2", + "thiserror", + "tracing", +] + +[[package]] +name = "ethers-solc" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66244a771d9163282646dbeffe0e6eca4dda4146b6498644e678ac6089b11edd" +dependencies = [ + "cfg-if", + "const-hex", + "dirs 5.0.1", + "dunce", + "ethers-core", + "glob", + "home", + "md-5", + "num_cpus", + "once_cell", + "path-slash", + "rayon", + "regex", + "semver", + "serde", + "serde_json", + "solang-parser", + "svm-rs", + "thiserror", + "tiny-keccak", + "tokio", + "tracing", + "walkdir", + "yansi", +] + +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fast-math" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2465292146cdfc2011350fe3b1c616ac83cf0faeedb33463ba1c332ed8948d66" +dependencies = [ + "ieee754", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fortuna" +version = "7.4.3" +dependencies = [ + "anyhow", + "axum", + "axum-macros", + "backoff", + "base64 0.21.4", + "bincode", + "byteorder", + "chrono", + "clap", + "ethabi", + "ethers", + "futures", + "futures-locks", + "hex", + "lazy_static", + "once_cell", + "prometheus-client", + "pythnet-sdk", + "rand", + "reqwest", + "serde", + "serde_json", + "serde_qs", + "serde_with", + "serde_yaml", + "sha3", + "thiserror", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", + "url", + "utoipa", + "utoipa-swagger-ui", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-locks" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ec6fe3675af967e67c5536c0b9d44e34e6c52f86bedc4ea49c5317b8e94d06" +dependencies = [ + "futures-channel", + "futures-task", + "tokio", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +dependencies = [ + "gloo-timers", + "send_wrapper 0.4.0", +] + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.9", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.2.0", + "indexmap 2.7.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "hashers" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bca93b15ea5a746f220e56587f71e73c6165eab783df9e26590069953e3c30" +dependencies = [ + "fxhash", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http 0.2.9", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.2.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.21", + "http 0.2.9", + "http-body 0.4.5", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.8", + "http 1.2.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +dependencies = [ + "futures-util", + "http 0.2.9", + "hyper 0.14.27", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.27", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "hyper 1.1.0", + "pin-project-lite", + "socket2 0.5.8", + "tokio", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "ieee754" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9007da9cacbd3e6343da136e98b0d2df013f553d35bdec8b518f07bea768e19c" + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", + "serde", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "jobserver" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.4", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "k256" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + +[[package]] +name = "keccak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lalrpop" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da4081d44f4611b66c6dd725e6de3169f9f63905421e8626fcb86b6a898998b8" +dependencies = [ + "ascii-canvas", + "bit-set", + "diff", + "ena", + "is-terminal", + "itertools 0.10.5", + "lalrpop-util", + "petgraph", + "regex", + "regex-syntax 0.7.5", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f35c735096c0293d313e8f2a641627472b83d01b937177fe76e5e2708d31e0d" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.170" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" + +[[package]] +name = "linux-raw-sys" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "mockito" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "652cd6d169a36eaf9d1e6bce1a221130439a966d7f27858af66a33a66e9c4ee2" +dependencies = [ + "assert-json-diff", + "bytes", + "colored", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.1.0", + "hyper-util", + "log", + "rand", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70bf6736f74634d299d00086f02986875b3c2d924781a6a2cb6c201e73da0ceb" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ea360eafe1022f7cc56cd7b869ed57330fb2453d0c7831d99b74c65d2f5597" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "open-fastrlp" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786393f80485445794f6043fd3138854dd109cc6c4bd1a6383db304c9ce9b9ce" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", + "ethereum-types", + "open-fastrlp-derive", +] + +[[package]] +name = "open-fastrlp-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003b2be5c6c53c1cfeb0a238b8a1c3915cd410feb684457a36c10038f764bb1c" +dependencies = [ + "bytes", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "openssl" +version = "0.10.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +dependencies = [ + "bitflags 2.4.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parity-scale-codec" +version = "3.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dec8a8073036902368c2cdc0387e85ff9a37054d7e7c98e592145e0c92cd4fb" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312270ee71e1cd70289dacf597cab7b207aa107d2f28191c2ae45b2ece18a260" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "path-slash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap 2.7.1", +] + +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2", + "syn 2.0.87", +] + +[[package]] +name = "primitive-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "scale-info", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml 0.5.11", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prometheus-client" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c99afa9a01501019ac3a14d71d9f94050346f55ca471ce90c799a15c58f61e2" +dependencies = [ + "dtoa", + "itoa", + "parking_lot", + "prometheus-client-derive-encode", +] + +[[package]] +name = "prometheus-client-derive-encode" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "pyth-sdk" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5c805ba3dfb5b7ed6a8ffa62ec38391f485a79c7cf6b3b11d3bd44fb0325824" +dependencies = [ + "borsh 0.9.3", + "borsh-derive 0.9.3", + "hex", + "schemars", + "serde", +] + +[[package]] +name = "pythnet-sdk" +version = "2.3.1" +dependencies = [ + "bincode", + "borsh 0.10.3", + "bytemuck", + "byteorder", + "fast-math", + "hex", + "pyth-sdk", + "rustc_version", + "serde", + "sha3", + "slow_primes", + "strum 0.24.1", + "thiserror", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.3.9", + "regex-syntax 0.7.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "reqwest" +version = "0.11.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +dependencies = [ + "base64 0.21.4", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.21", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.27", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "reserve-port" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b212efd3460286cd590149feedd0afabef08ee352445dd6b4452f0d136098a5f" +dependencies = [ + "lazy_static", + "thiserror", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest", +] + +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rlp-derive", + "rustc-hex", +] + +[[package]] +name = "rlp-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33d7b2abe0c340d8797fe2907d3f20d3b5ea5908683618bfe80df7f621f672a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust-embed" +version = "6.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "shellexpand", + "syn 2.0.87", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustls" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.4", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scale-info" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0a159d0c45c12b20c5a844feb1fe4bea86e28f17b92a5f0c42193634d3782" +dependencies = [ + "cfg-if", + "derive_more", + "parity-scale-codec", + "scale-info-derive", +] + +[[package]] +name = "scale-info-derive" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "912e55f6d20e0e80d63733872b40e1227c0bce1e1ab81ba67d696339bfd7fd29" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "schemars" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.87", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f9e24d2b632954ded8ab2ef9fea0a0c769ea56ea98bddbafbad22caeeadf45d" +dependencies = [ + "hmac", + "pbkdf2 0.11.0", + "salsa20", + "sha2", +] + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" +dependencies = [ + "serde", +] + +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + +[[package]] +name = "serde" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serde_json" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "axum", + "futures", + "percent-encoding", + "serde", + "thiserror", +] + +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +dependencies = [ + "base64 0.21.4", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.7.1", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.7.1", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs 4.0.0", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slow_primes" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58267dd2fbaa6dceecba9e3e106d2d90a2b02497c0e8b01b8759beccf5113938" +dependencies = [ + "num", +] + +[[package]] +name = "smallvec" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "solang-parser" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c425ce1c59f4b154717592f0bdf4715c3a1d55058883622d3157e1f0908a5b26" +dependencies = [ + "itertools 0.11.0", + "lalrpop", + "lalrpop-util", + "phf", + "thiserror", + "unicode-xid", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros 0.24.3", +] + +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros 0.26.2", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.87", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "svm-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597e3a746727984cb7ea2487b6a40726cad0dbe86628e7d429aa6b8c4c153db4" +dependencies = [ + "dirs 5.0.1", + "fs2", + "hex", + "once_cell", + "reqwest", + "semver", + "serde", + "serde_json", + "sha2", + "thiserror", + "url", + "zip", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.8", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "rustls", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots", +] + +[[package]] +name = "tokio-util" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.9", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.7.1", + "toml_datetime", + "winnow 0.5.16", +] + +[[package]] +name = "toml_edit" +version = "0.22.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +dependencies = [ + "indexmap 2.7.1", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.5", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +dependencies = [ + "bitflags 2.4.0", + "bytes", + "futures-core", + "futures-util", + "http 0.2.9", + "http-body 0.4.5", + "http-range-header", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 0.2.9", + "httparse", + "log", + "rand", + "rustls", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "utoipa" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82b1bc5417102a73e8464c686eef947bdfb99fcdfc0a4f228e81afa9526470a" +dependencies = [ + "indexmap 2.7.1", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d96dcd6fc96f3df9b3280ef480770af1b7c5d14bc55192baa9b067976d920c" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 2.0.87", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84614caa239fb25b2bb373a52859ffd94605ceb256eeb1d63436325cf81e3653" +dependencies = [ + "axum", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "utoipa", + "zip", +] + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "winnow" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "ws_stream_wasm" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7999f5f4217fe3818726b66257a4475f71e74ffd190776ad053fa159e50737f5" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper 0.6.0", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2 0.11.0", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.9+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/apps/argus/Cargo.toml b/apps/argus/Cargo.toml new file mode 100644 index 0000000000..5f62e866d0 --- /dev/null +++ b/apps/argus/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "argus" +version = "0.0.1" +edition = "2021" + +[lib] +name = "argus" +path = "src/lib.rs" + +[dependencies] +anyhow = "1.0.75" +axum = { version = "0.6.20", features = ["json", "ws", "macros"] } +axum-macros = { version = "0.3.8" } +base64 = { version = "0.21.0" } +bincode = "1.3.3" +byteorder = "1.5.0" +clap = { version = "4.4.6", features = ["derive", "cargo", "env"] } +ethabi = "18.0.0" +ethers = { version = "2.0.14", features = ["ws"] } +futures = { version = "0.3.28" } +hex = "0.4.3" +prometheus-client = { version = "0.21.2" } +pythnet-sdk = { path = "../../pythnet/pythnet_sdk", features = ["strum"] } +rand = "0.8.5" +reqwest = { version = "0.11.22", features = ["json", "blocking"] } +serde = { version = "1.0.188", features = ["derive"] } +serde_qs = { version = "0.12.0", features = ["axum"] } +serde_json = "1.0.107" +serde_with = { version = "3.4.0", features = ["hex", "base64"] } +serde_yaml = "0.9.25" +sha3 = "0.10.8" +tokio = { version = "1.33.0", features = ["full"] } +tower-http = { version = "0.4.0", features = ["cors"] } +tracing = { version = "0.1.37", features = ["log"] } +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +utoipa = { version = "3.4.0", features = ["axum_extras"] } +utoipa-swagger-ui = { version = "3.1.4", features = ["axum"] } +once_cell = "1.18.0" +lazy_static = "1.4.0" +url = "2.5.0" +chrono = { version = "0.4.38", features = [ + "clock", + "std", +], default-features = false } +backoff = { version = "0.4.0", features = ["futures", "tokio"] } +thiserror = "1.0.61" +futures-locks = "0.7.1" +fortuna = { path = "../fortuna" } + + +[dev-dependencies] +axum-test = "13.1.1" +mockito = "1.2.0" diff --git a/apps/argus/Dockerfile b/apps/argus/Dockerfile new file mode 100644 index 0000000000..c5e4fbec42 --- /dev/null +++ b/apps/argus/Dockerfile @@ -0,0 +1,18 @@ +ARG RUST_VERSION=1.82.0 + +FROM rust:${RUST_VERSION} AS build + +# Build +WORKDIR /src +COPY apps/argus apps/argus +COPY pythnet pythnet +COPY target_chains/ethereum/entropy_sdk/solidity/abis target_chains/ethereum/entropy_sdk/solidity/abis + +WORKDIR /src/apps/argus + +RUN --mount=type=cache,target=/root/.cargo/registry cargo build --release + + +FROM rust:${RUST_VERSION} +# Copy artifacts from other images +COPY --from=build /src/apps/argus/target/release/argus /usr/local/bin/ diff --git a/apps/argus/README.md b/apps/argus/README.md new file mode 100644 index 0000000000..9a98e63653 --- /dev/null +++ b/apps/argus/README.md @@ -0,0 +1,51 @@ +# Argus + +Argus is a webservice that serves price updates according to the Pulse protocol. +The webservice processes and delivers price updates to callers when permitted by the protocol. +The service operates a keeper task that performs callback transactions for user requests. + +A single instance of this service can simultaneously serve price updates for several different blockchains. +Each blockchain is configured in `config.yaml`. + +## Build & Test + +Argus uses Cargo for building and dependency management. +Simply run `cargo build` and `cargo test` to build and test the project. + +## Command-Line Interface + +The Argus binary has a command-line interface to perform useful operations on the contract, such as +registering a new price provider, or requesting price updates. To see the available commands, simply run `cargo run`. + +## Hermes Integration + +Argus integrates with the Hermes API to fetch price updates for fulfilling requests. When a price update request is received, Argus: + +1. Retrieves the request details from the blockchain +2. Fetches the required price updates from Hermes using the `/v2/updates/price/{publish_time}` endpoint +3. Executes the callback on the Pulse contract with the fetched price updates + +The Hermes client is implemented in the `keeper/hermes.rs` module and handles: + +- Converting price IDs to the format expected by Hermes +- Fetching price updates from the Hermes API +- Parsing the response and converting it to the format expected by the Pulse contract +- Error handling and retries + +## Local Development + +To start an instance of the webserver for local testing, you first need to perform a few setup steps: + +1. Create a `config.yaml` file to point to the desired blockchains and Pulse contracts. Copy the content in `config.sample.yaml` and follow the directions inside to generate the necessary private keys and secrets. +1. Make sure the wallets you have generated in step (1) contain some gas tokens for the configured networks. +1. Run `cargo run -- setup-provider` to register a price provider for this service. This command + will update the on-chain contracts such that the configured provider key is a price provider, + and its on-chain configuration matches `config.yaml`. + +Once you've completed the setup, simply run the following command to start the service: + +```bash +RUST_LOG=INFO cargo run -- run +``` + +This command will start the webservice on `localhost:34000`. diff --git a/apps/argus/config.sample.yaml b/apps/argus/config.sample.yaml new file mode 100644 index 0000000000..f32450d873 --- /dev/null +++ b/apps/argus/config.sample.yaml @@ -0,0 +1,77 @@ +chains: + linea_sepolia: + geth_rpc_addr: https://rpc.sepolia.linea.build + contract_addr: 0xEbe57e8045F2F230872523bbff7374986E45C486 + + # Keeper configuration for the chain + gas_limit: 500000 + + # Multiplier for the priority fee estimate, as a percentage (i.e., 100 = no change). + # Defaults to 100 if the field is omitted. + priority_fee_multiplier_pct: 100 + + escalation_policy: + # Pad the first callback transaction's gas estimate by 25%, + # then multiply each successive callback transaction's gas estimate by 10% until the cap is reached. + # All numbers are expressed as percentages where 100 = no change. + initial_gas_multiplier_pct: 125 + gas_multiplier_pct: 110 + gas_multiplier_cap_pct: 600 + + # Multiply successive callback transaction's fees by 10% until the cap is reached. + # All numbers are expressed as percentages where 100 = no change. + # (See also priority_fee_multiplier_pct above to generically adjust the priority fee estimates for the chain -- + # adjusting that parameter will influence the fee of the first transaction, in addition to other things) + fee_multiplier_pct: 110 + fee_multiplier_cap_pct: 200 + + min_keeper_balance: 100000000000000000 + + # Provider configuration + # How much to charge in fees + fee: 1500000000000000 + + # Configuration for dynamic fees under high gas prices. The keeper will set + # on-chain fees to make between [min_profit_pct, max_profit_pct] of the max callback + # cost in profit per transaction. + min_profit_pct: 0 + target_profit_pct: 20 + max_profit_pct: 100 + + # A list of block delays for processing blocks multiple times. Each number represents + # how many blocks to wait before processing. For example, [5, 10, 20] means process + # blocks after 5 blocks, then again after 10 blocks, and finally after 20 blocks. + block_delays: [5, 10, 20] + +provider: + uri: http://localhost:8080/ + chain_length: 100000 + chain_sample_interval: 10 + + # An ethereum wallet address and private key. Generate with `cast wallet new` + address: 0xADDRESS + private_key: + # For local development, you can hardcode the private key here + value: 0xabcd + # For production, you can store the private key in a file. + # file: provider-key.txt + + # Set this to the address of your keeper wallet if you would like the keeper wallet to + # be able to withdraw fees from the contract. + fee_manager: 0xADDRESS +keeper: + # An ethereum wallet address and private key for running the keeper service. + # This does not have to be the same key as the provider's key above. + # Generate with `cast wallet new`. + # The keeper private key can be omitted to run the webservice without the keeper. + private_key: + # For local development, you can hardcode the private key here + value: 0xabcd + # For production, you can store the private key in a file. + # file: keeper-key.txt + +# Hermes API configuration +hermes: + # Base URL for the Hermes API + # This can be overridden by setting the HERMES_BASE_URL environment variable + base_url: https://hermes.pyth.network diff --git a/apps/argus/rust-toolchain b/apps/argus/rust-toolchain new file mode 100644 index 0000000000..71fae54fb2 --- /dev/null +++ b/apps/argus/rust-toolchain @@ -0,0 +1 @@ +1.82.0 diff --git a/apps/argus/src/api.rs b/apps/argus/src/api.rs new file mode 100644 index 0000000000..3e0cd2b055 --- /dev/null +++ b/apps/argus/src/api.rs @@ -0,0 +1,447 @@ +use { + crate::chain::reader::PulseReader, + anyhow::Result, + axum::{ + body::Body, + http::StatusCode, + response::{IntoResponse, Response}, + routing::get, + Router, + }, + ethers::core::types::Address, + prometheus_client::{ + encoding::EncodeLabelSet, + metrics::{counter::Counter, family::Family}, + registry::Registry, + }, + std::{collections::HashMap, sync::Arc}, + tokio::sync::RwLock, + url::Url, +}; +pub use {chain_ids::*, index::*, live::*, metrics::*, price_updates::*, ready::*}; + +mod chain_ids; +mod index; +mod live; +mod metrics; +mod price_updates; +mod ready; + +pub type ChainId = String; + +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] +pub struct RequestLabel { + pub value: String, +} + +pub struct ApiMetrics { + pub http_requests: Family, +} + +#[derive(Clone)] +pub struct ApiState { + pub chains: Arc>, + + pub metrics_registry: Arc>, + + /// Prometheus metrics + pub metrics: Arc, +} + +impl ApiState { + pub async fn new( + chains: HashMap, + metrics_registry: Arc>, + ) -> ApiState { + let metrics = ApiMetrics { + http_requests: Family::default(), + }; + + let http_requests = metrics.http_requests.clone(); + metrics_registry.write().await.register( + "http_requests", + "Number of HTTP requests received", + http_requests, + ); + + ApiState { + chains: Arc::new(chains), + metrics: Arc::new(metrics), + metrics_registry, + } + } +} + +/// The state of the price update service for a single blockchain. +#[derive(Clone)] +pub struct BlockchainState { + /// The chain id for this blockchain, useful for logging + pub id: ChainId, + /// The contract that the server is fulfilling requests for. + pub contract: Arc, + /// The address of the provider that this server is operating for. + pub provider_address: Address, +} + +pub enum RestError { + /// The caller passed a sequence number that isn't within the supported range + InvalidSequenceNumber, + /// The caller passed an unsupported chain id + InvalidChainId, + /// The caller requested price updates that can't currently be provided (because they + /// haven't been committed to on-chain) + NoPendingRequest, + /// The server cannot currently communicate with the blockchain, so is not able to verify + /// which price updates have been requested. + TemporarilyUnavailable, + /// A catch-all error for all other types of errors that could occur during processing. + Unknown, +} + +impl IntoResponse for RestError { + fn into_response(self) -> Response { + match self { + RestError::InvalidSequenceNumber => ( + StatusCode::BAD_REQUEST, + "The sequence number is out of the permitted range", + ) + .into_response(), + RestError::InvalidChainId => { + (StatusCode::BAD_REQUEST, "The chain id is not supported").into_response() + } + RestError::NoPendingRequest => ( + StatusCode::FORBIDDEN, + "The request with the given sequence number has not been made yet, or the price updates have already been provided on chain.", + ).into_response(), + RestError::TemporarilyUnavailable => ( + StatusCode::SERVICE_UNAVAILABLE, + "This service is temporarily unavailable", + ) + .into_response(), + RestError::Unknown => ( + StatusCode::INTERNAL_SERVER_ERROR, + "An unknown error occurred processing the request", + ) + .into_response(), + } + } +} + +pub fn routes(state: ApiState) -> Router<(), Body> { + Router::new() + .route("/", get(index)) + .route("/live", get(live)) + .route("/metrics", get(metrics)) + .route("/ready", get(ready)) + .route("/v1/chains", get(chain_ids)) + .route( + "/v1/chains/:chain_id/price-updates/:sequence", + get(price_update), + ) + .with_state(state) +} + +/// We are registering the provider on chain with the following url: +/// `{base_uri}/v1/chains/{chain_id}` +/// The path and API are highly coupled. Please be sure to keep them consistent. +pub fn get_register_uri(base_uri: &str, chain_id: &str) -> Result { + let base_uri = Url::parse(base_uri)?; + let path = format!("/v1/chains/{}", chain_id); + let uri = base_uri.join(&path)?; + Ok(uri.to_string()) +} + +#[cfg(test)] +mod test { + use { + crate::{ + api::{self, ApiState, BlockchainState}, + chain::reader::mock::MockPulseReader, + }, + axum::http::StatusCode, + axum_test::{TestResponse, TestServer}, + ethers::prelude::{Address, U256}, + lazy_static::lazy_static, + prometheus_client::registry::Registry, + std::{collections::HashMap, sync::Arc}, + tokio::sync::RwLock, + }; + + const PROVIDER: Address = Address::zero(); + lazy_static! { + static ref OTHER_PROVIDER: Address = Address::from_low_u64_be(1); + } + + async fn test_server() -> (TestServer, Arc, Arc) { + let eth_read = Arc::new(MockPulseReader::with_requests(10, &[])); + + let eth_state = BlockchainState { + id: "ethereum".into(), + contract: eth_read.clone(), + provider_address: PROVIDER, + }; + + let metrics_registry = Arc::new(RwLock::new(Registry::default())); + + let avax_read = Arc::new(MockPulseReader::with_requests(10, &[])); + + let avax_state = BlockchainState { + id: "avalanche".into(), + contract: avax_read.clone(), + provider_address: PROVIDER, + }; + + let mut chains = HashMap::new(); + chains.insert("ethereum".into(), eth_state); + chains.insert("avalanche".into(), avax_state); + + let api_state = ApiState::new(chains, metrics_registry).await; + + let app = api::routes(api_state); + (TestServer::new(app).unwrap(), eth_read, avax_read) + } + + async fn get_and_assert_status( + server: &TestServer, + path: &str, + status: StatusCode, + ) -> TestResponse { + let response = server.get(path).await; + response.assert_status(status); + response + } + + // TODO: fix this test + // #[tokio::test] + // async fn test_price_updates() { + // let (server, eth_contract, avax_contract) = test_server().await; + // let empty_price_ids: Vec<[u8; 32]> = vec![]; + // let callback_gas_limit = U256::from(100000); + // let publish_time = U256::from(1000); + + // // Can't access price updates if they haven't been requested + // get_and_assert_status( + // &server, + // "/v1/chains/ethereum/price-updates/0", + // StatusCode::FORBIDDEN, + // ) + // .await; + + // // Once someone requests the price updates, then they are accessible + // eth_contract.insert( + // PROVIDER, + // 0, + // callback_gas_limit, + // empty_price_ids.clone(), + // publish_time, + // ); + // get_and_assert_status( + // &server, + // "/v1/chains/ethereum/price-updates/0", + // StatusCode::OK, + // ) + // .await; + + // // Each chain and provider has its own set of requests + // eth_contract.insert( + // PROVIDER, + // 100, + // callback_gas_limit, + // empty_price_ids.clone(), + // publish_time, + // ); + // eth_contract.insert( + // *OTHER_PROVIDER, + // 101, + // callback_gas_limit, + // empty_price_ids.clone(), + // publish_time, + // ); + // eth_contract.insert( + // PROVIDER, + // 102, + // callback_gas_limit, + // empty_price_ids.clone(), + // publish_time, + // ); + // avax_contract.insert( + // PROVIDER, + // 102, + // callback_gas_limit, + // empty_price_ids.clone(), + // publish_time, + // ); + // avax_contract.insert( + // PROVIDER, + // 103, + // callback_gas_limit, + // empty_price_ids.clone(), + // publish_time, + // ); + // avax_contract.insert( + // *OTHER_PROVIDER, + // 104, + // callback_gas_limit, + // empty_price_ids.clone(), + // publish_time, + // ); + + // get_and_assert_status( + // &server, + // "/v1/chains/ethereum/price-updates/100", + // StatusCode::OK, + // ) + // .await; + + // get_and_assert_status( + // &server, + // "/v1/chains/ethereum/price-updates/101", + // StatusCode::FORBIDDEN, + // ) + // .await; + + // get_and_assert_status( + // &server, + // "/v1/chains/ethereum/price-updates/102", + // StatusCode::OK, + // ) + // .await; + + // get_and_assert_status( + // &server, + // "/v1/chains/ethereum/price-updates/103", + // StatusCode::FORBIDDEN, + // ) + // .await; + + // get_and_assert_status( + // &server, + // "/v1/chains/ethereum/price-updates/104", + // StatusCode::FORBIDDEN, + // ) + // .await; + + // get_and_assert_status( + // &server, + // "/v1/chains/avalanche/price-updates/100", + // StatusCode::FORBIDDEN, + // ) + // .await; + + // get_and_assert_status( + // &server, + // "/v1/chains/avalanche/price-updates/101", + // StatusCode::FORBIDDEN, + // ) + // .await; + + // get_and_assert_status( + // &server, + // "/v1/chains/avalanche/price-updates/102", + // StatusCode::OK, + // ) + // .await; + + // get_and_assert_status( + // &server, + // "/v1/chains/avalanche/price-updates/103", + // StatusCode::OK, + // ) + // .await; + + // get_and_assert_status( + // &server, + // "/v1/chains/avalanche/price-updates/104", + // StatusCode::FORBIDDEN, + // ) + // .await; + + // // Bad chain ids fail + // get_and_assert_status( + // &server, + // "/v1/chains/not_a_chain/price-updates/0", + // StatusCode::BAD_REQUEST, + // ) + // .await; + // } + + #[tokio::test] + async fn test_price_update_confirmation_delay() { + let (server, eth_contract, avax_contract) = test_server().await; + let empty_price_ids: Vec<[u8; 32]> = vec![]; + let callback_gas_limit = U256::from(100000); + let publish_time = U256::from(1000); + + // No requests yet, so all requests should be forbidden + get_and_assert_status( + &server, + "/v1/chains/ethereum/price-updates/0", + StatusCode::FORBIDDEN, + ) + .await; + + get_and_assert_status( + &server, + "/v1/chains/avalanche/price-updates/100", + StatusCode::FORBIDDEN, + ) + .await; + + // Add requests - they should be immediately available + eth_contract.insert( + PROVIDER, + 0, + callback_gas_limit, + empty_price_ids.clone(), + publish_time, + ); + eth_contract.insert( + PROVIDER, + 1, + callback_gas_limit, + empty_price_ids.clone(), + publish_time, + ); + avax_contract.insert( + PROVIDER, + 100, + callback_gas_limit, + empty_price_ids.clone(), + publish_time, + ); + + // All inserted requests should be immediately available + get_and_assert_status( + &server, + "/v1/chains/ethereum/price-updates/0", + StatusCode::OK, + ) + .await; + get_and_assert_status( + &server, + "/v1/chains/ethereum/price-updates/1", + StatusCode::OK, + ) + .await; + get_and_assert_status( + &server, + "/v1/chains/avalanche/price-updates/100", + StatusCode::OK, + ) + .await; + + // Non-inserted requests should still be forbidden + get_and_assert_status( + &server, + "/v1/chains/ethereum/price-updates/2", + StatusCode::FORBIDDEN, + ) + .await; + get_and_assert_status( + &server, + "/v1/chains/avalanche/price-updates/101", + StatusCode::FORBIDDEN, + ) + .await; + } +} diff --git a/apps/argus/src/api/chain_ids.rs b/apps/argus/src/api/chain_ids.rs new file mode 100644 index 0000000000..0df175443f --- /dev/null +++ b/apps/argus/src/api/chain_ids.rs @@ -0,0 +1,20 @@ +use { + crate::api::{ChainId, RestError}, + anyhow::Result, + axum::{extract::State, Json}, +}; + +/// Get the list of supported chain ids +#[utoipa::path( +get, +path = "/v1/chains", +responses( +(status = 200, description = "Successfully retrieved the list of chain ids", body = GetRandomValueResponse), +) +)] +pub async fn chain_ids( + State(state): State, +) -> Result>, RestError> { + let chain_ids = state.chains.iter().map(|(id, _)| id.clone()).collect(); + Ok(Json(chain_ids)) +} diff --git a/apps/argus/src/api/index.rs b/apps/argus/src/api/index.rs new file mode 100644 index 0000000000..e145ab3c7a --- /dev/null +++ b/apps/argus/src/api/index.rs @@ -0,0 +1,15 @@ +use axum::{response::IntoResponse, Json}; + +/// This is the index page for the REST service. It lists all the available endpoints. +/// +/// TODO: Dynamically generate this list if possible. +pub async fn index() -> impl IntoResponse { + Json([ + "/", + "/live", + "/metrics", + "/ready", + "/v1/chains", + "/v1/chains/:chain_id/price-updates/:sequence" + ]) +} diff --git a/apps/argus/src/api/live.rs b/apps/argus/src/api/live.rs new file mode 100644 index 0000000000..2a6f8bff4d --- /dev/null +++ b/apps/argus/src/api/live.rs @@ -0,0 +1,8 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; + +pub async fn live() -> Response { + (StatusCode::OK, "OK").into_response() +} diff --git a/apps/argus/src/api/metrics.rs b/apps/argus/src/api/metrics.rs new file mode 100644 index 0000000000..7977c58d96 --- /dev/null +++ b/apps/argus/src/api/metrics.rs @@ -0,0 +1,17 @@ +//! Exposing prometheus metrics via HTTP in openmetrics format. + +use { + axum::{extract::State, response::IntoResponse}, + prometheus_client::encoding::text::encode, +}; + +pub async fn metrics(State(state): State) -> impl IntoResponse { + let registry = state.metrics_registry.read().await; + let mut buffer = String::new(); + + // Should not fail if the metrics are valid and there is memory available + // to write to the buffer. + encode(&mut buffer, ®istry).unwrap(); + + buffer +} diff --git a/apps/argus/src/api/price_updates.rs b/apps/argus/src/api/price_updates.rs new file mode 100644 index 0000000000..9b003df148 --- /dev/null +++ b/apps/argus/src/api/price_updates.rs @@ -0,0 +1,124 @@ +use { + crate::api::{ChainId, RequestLabel, RestError}, + anyhow::Result, + axum::{ + extract::{Path, Query, State}, + Json, + }, + utoipa::{IntoParams, ToSchema}, +}; + +/// Get price update data for a given sequence number and blockchain. +/// +/// Given a sequence number, retrieve the corresponding price update data that this provider can deliver. +/// This endpoint will not return the price data unless someone has requested the sequence number on-chain. +/// +/// Every blockchain supported by this service has a distinct sequence of price updates and chain_id. +/// Callers must pass the appropriate chain_id to ensure they fetch the correct price data. +#[utoipa::path( +get, +path = "/v1/chains/{chain_id}/price-updates/{sequence}", +responses( +(status = 200, description = "Price update data successfully retrieved", body = GetPriceUpdateResponse), +(status = 403, description = "Price update data cannot currently be retrieved", body = String) +), +params(PriceUpdatePathParams, PriceUpdateQueryParams) +)] +pub async fn price_update( + State(state): State, + Path(PriceUpdatePathParams { chain_id, sequence }): Path, + Query(PriceUpdateQueryParams { format }): Query, +) -> Result, RestError> { + let _ = format; // Ignore the unused variable + + state + .metrics + .http_requests + .get_or_create(&RequestLabel { + value: "/v1/chains/{chain_id}/price-updates/{sequence}".to_string(), + }) + .inc(); + + let state = state + .chains + .get(&chain_id) + .ok_or(RestError::InvalidChainId)?; + + let maybe_request_fut = state.contract.get_request(sequence); + + let maybe_request = maybe_request_fut.await.map_err(|e| { + tracing::error!(chain_id = chain_id, "RPC request failed {}", e); + RestError::TemporarilyUnavailable + })?; + + match maybe_request { + Some(request) => { + // In a real implementation, we would fetch the price update data from a data source + // For now, we'll just return a mock response + let price_update_data = + generate_price_update_data(&request.price_ids, request.publish_time.as_u64()); + + Ok(Json(GetPriceUpdateResponse { + data: PriceUpdateData::new(price_update_data), + })) + } + None => Err(RestError::NoPendingRequest), + } +} + +// Helper function to generate price update data based on price IDs and publish time +// In a real implementation, this would fetch actual price data from a data source +fn generate_price_update_data(price_ids: &[[u8; 32]], publish_time: u64) -> Vec { + // This is just a placeholder implementation + // In a real system, we would generate actual price update data + let mut data = Vec::new(); + + // Add publish time to the data + data.extend_from_slice(&publish_time.to_be_bytes()); + + // Add a simple representation of each price ID + for price_id in price_ids { + data.extend_from_slice(price_id); + } + + data +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, IntoParams)] +#[into_params(parameter_in=Path)] +pub struct PriceUpdatePathParams { + #[param(value_type = String)] + pub chain_id: ChainId, + pub sequence: u64, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, IntoParams)] +#[into_params(parameter_in=Query)] +pub struct PriceUpdateQueryParams { + pub format: Option, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, ToSchema)] +#[serde(rename_all = "kebab-case")] +pub enum ResponseFormat { + #[serde(rename = "json")] + Json, + #[serde(rename = "binary")] + Binary, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, ToSchema, PartialEq)] +pub struct GetPriceUpdateResponse { + pub data: PriceUpdateData, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, ToSchema, PartialEq)] +pub struct PriceUpdateData { + data: Vec, +} + +impl PriceUpdateData { + pub fn new(data: Vec) -> Self { + Self { data } + } +} diff --git a/apps/argus/src/api/ready.rs b/apps/argus/src/api/ready.rs new file mode 100644 index 0000000000..d242106fd9 --- /dev/null +++ b/apps/argus/src/api/ready.rs @@ -0,0 +1,9 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; + +pub async fn ready() -> Response { + // TODO: are there useful checks here? At the moment, everything important occurs synchronously on startup. + (StatusCode::OK, "OK").into_response() +} diff --git a/apps/argus/src/chain.rs b/apps/argus/src/chain.rs new file mode 100644 index 0000000000..34724c3b76 --- /dev/null +++ b/apps/argus/src/chain.rs @@ -0,0 +1,2 @@ +pub mod ethereum; +pub mod reader; diff --git a/apps/argus/src/chain/ethereum.rs b/apps/argus/src/chain/ethereum.rs new file mode 100644 index 0000000000..738dd39e7f --- /dev/null +++ b/apps/argus/src/chain/ethereum.rs @@ -0,0 +1,317 @@ +use { + crate::{ + api::ChainId, + chain::reader::{self, BlockNumber, PulseReader, RequestedWithCallbackEvent}, + config::EthereumConfig, + }, + anyhow::{anyhow, Context, Result}, + axum::async_trait, + ethers::{ + abi::RawLog, + contract::{abigen, EthLogDecode}, + core::types::Address, + middleware::{gas_oracle::GasOracleMiddleware, SignerMiddleware}, + prelude::JsonRpcClient, + providers::{Http, Middleware, Provider}, + signers::{LocalWallet, Signer}, + types::U256, + }, + fortuna::eth_utils::{ + eth_gas_oracle::EthProviderOracle, + legacy_tx_middleware::LegacyTxMiddleware, + nonce_manager::NonceManagerMiddleware, + traced_client::{RpcMetrics, TracedClient}, + }, + std::sync::Arc, +}; + +// TODO: Programmatically generate this so we don't have to keep committed ABI in sync with the +// contract in the same repo. +abigen!( + Pulse, + "../../target_chains/ethereum/contracts/out/IPulse.sol/IPulse.abi.json" +); + +pub type MiddlewaresWrapper = LegacyTxMiddleware< + GasOracleMiddleware< + NonceManagerMiddleware, LocalWallet>>, + EthProviderOracle>, + >, +>; + +pub type SignablePythContractInner = Pulse>; +pub type SignablePythContract = SignablePythContractInner; +pub type InstrumentedSignablePythContract = SignablePythContractInner; + +pub type PythContract = Pulse>; +pub type InstrumentedPythContract = Pulse>; + +impl SignablePythContractInner { + /// Get the wallet that signs transactions sent to this contract. + pub fn wallet(&self) -> LocalWallet { + self.client().inner().inner().inner().signer().clone() + } + + /// Get the underlying provider that communicates with the blockchain. + pub fn provider(&self) -> Provider { + self.client().inner().inner().inner().provider().clone() + } + + /// Submit a request for price updates to the contract. + /// + /// This method is a version of the autogenned `requestPriceUpdatesWithCallback` method that parses the emitted logs + /// to return the sequence number of the created Request. + pub async fn request_price_updates_wrapper( + &self, + publish_time: U256, + price_ids: Vec<[u8; 32]>, + callback_gas_limit: U256, + ) -> Result { + let fee = self.get_fee(callback_gas_limit).call().await?; + + if let Some(r) = self + .request_price_updates_with_callback(publish_time, price_ids, callback_gas_limit) + .value(fee) + .send() + .await? + .await? + { + // Extract Log from TransactionReceipt. + let l: RawLog = r.logs[0].clone().into(); + if let PulseEvents::PriceUpdateRequestedFilter(r) = PulseEvents::decode_log(&l)? { + Ok(r.request.sequence_number) + } else { + Err(anyhow!("No log with sequence number")) + } + } else { + Err(anyhow!("Request failed")) + } + } + + /// Execute the callback for a price update request. + /// + /// This method is a version of the autogenned `executeCallback` method that parses the emitted logs + /// to return the updated price IDs. + pub async fn execute_callback_wrapper( + &self, + sequence_number: u64, + update_data: Vec>, + price_ids: Vec<[u8; 32]>, + ) -> Result> { + if let Some(r) = self + .execute_callback( + sequence_number, + update_data + .into_iter() + .map(ethers::types::Bytes::from) + .collect(), + price_ids.clone(), + ) + .send() + .await? + .await? + { + if let PulseEvents::PriceUpdateExecutedFilter(r) = + PulseEvents::decode_log(&r.logs[0].clone().into())? + { + Ok(r.price_ids.to_vec()) + } else { + Err(anyhow!("No log with price updates")) + } + } else { + Err(anyhow!("Request failed")) + } + } + + pub async fn from_config_and_provider( + chain_config: &EthereumConfig, + private_key: &str, + provider: Provider, + ) -> Result> { + let chain_id = provider.get_chainid().await?; + let gas_oracle = + EthProviderOracle::new(provider.clone(), chain_config.priority_fee_multiplier_pct); + let wallet__ = private_key + .parse::()? + .with_chain_id(chain_id.as_u64()); + + let address = wallet__.address(); + + Ok(Pulse::new( + chain_config.contract_addr, + Arc::new(LegacyTxMiddleware::new( + chain_config.legacy_tx, + GasOracleMiddleware::new( + NonceManagerMiddleware::new(SignerMiddleware::new(provider, wallet__), address), + gas_oracle, + ), + )), + )) + } +} + +impl SignablePythContract { + pub async fn from_config(chain_config: &EthereumConfig, private_key: &str) -> Result { + let provider = Provider::::try_from(&chain_config.geth_rpc_addr)?; + Self::from_config_and_provider(chain_config, private_key, provider).await + } +} + +impl InstrumentedSignablePythContract { + pub async fn from_config( + chain_config: &EthereumConfig, + private_key: &str, + chain_id: ChainId, + metrics: Arc, + ) -> Result { + let provider = TracedClient::new(chain_id, &chain_config.geth_rpc_addr, metrics)?; + Self::from_config_and_provider(chain_config, private_key, provider).await + } +} + +impl PythContract { + pub fn from_config(chain_config: &EthereumConfig) -> Result { + let provider = Provider::::try_from(&chain_config.geth_rpc_addr)?; + + Ok(Pulse::new(chain_config.contract_addr, Arc::new(provider))) + } +} + +impl InstrumentedPythContract { + pub fn from_config( + chain_config: &EthereumConfig, + chain_id: ChainId, + metrics: Arc, + ) -> Result { + let provider = TracedClient::new(chain_id, &chain_config.geth_rpc_addr, metrics)?; + + Ok(Pulse::new(chain_config.contract_addr, Arc::new(provider))) + } +} + +#[async_trait] +impl PulseReader for Pulse> { + async fn get_request(&self, sequence_number: u64) -> Result> { + let r = self + .get_request(sequence_number) + // TODO: This doesn't work for lighlink right now. Figure out how to do this in lightlink + // .block(ethers::core::types::BlockNumber::Finalized) + .call() + .await?; + + // sequence_number == 0 means the request does not exist. + if r.sequence_number != 0 { + Ok(Some(reader::Request { + requester: r.requester, + sequence_number: r.sequence_number, + callback_gas_limit: r.callback_gas_limit, + price_ids: r.price_ids.to_vec(), + publish_time: r.publish_time, + num_price_ids: r.num_price_ids, + })) + } else { + Ok(None) + } + } + + async fn get_block_number(&self) -> Result { + let block = self + .client() + .get_block_number() + .await + .context("Failed to get block number")?; + Ok(block.as_u64()) + } + + async fn get_active_requests(&self, count: usize) -> Result> { + let (requests, actual_count) = self.get_first_active_requests(count.into()).call().await?; + + // Convert actual_count (U256) to usize safely + let actual_count_usize = actual_count.as_u64() as usize; + let mut result = Vec::with_capacity(actual_count_usize); + + for i in 0..actual_count_usize { + let r = &requests[i]; + if r.sequence_number != 0 { + result.push(reader::Request { + requester: r.requester, + sequence_number: r.sequence_number, + callback_gas_limit: r.callback_gas_limit, + price_ids: r.price_ids.to_vec(), + publish_time: r.publish_time, + num_price_ids: r.num_price_ids, + }); + } + } + + Ok(result) + } + + async fn get_price_update_requested_events( + &self, + from_block: BlockNumber, + to_block: BlockNumber, + ) -> Result> { + let mut event = self.price_update_requested_filter(); + event.filter = event.filter.from_block(from_block).to_block(to_block); + + let res: Vec = event.query().await?; + + Ok(res + .iter() + .map(|r| RequestedWithCallbackEvent { + sequence_number: r.request.sequence_number, + requester: r.request.requester, + price_ids: r.price_ids.to_vec(), + callback_gas_limit: r.request.callback_gas_limit, + }) + .collect()) + } + + async fn estimate_execute_callback_gas( + &self, + sender: Address, + sequence_number: u64, + update_data: Vec>, + price_ids: Vec<[u8; 32]>, + ) -> Result { + let result = self + .execute_callback( + sequence_number, + update_data + .into_iter() + .map(ethers::types::Bytes::from) + .collect(), + price_ids, + ) + .from(sender) + .estimate_gas() + .await; + + result.map_err(|e| e.into()) + } +} + +impl Pulse> { + /// Get the current sequence number from the contract. + /// + /// This method directly accesses the contract's storage to get the currentSequenceNumber. + pub async fn get_current_sequence_number(&self) -> Result { + // The currentSequenceNumber is stored in the State struct at slot 0, offset 32 bytes + // (after admin, pythFeeInWei, accruedFeesInWei, and pyth) + let storage_slot = ethers::types::H256::zero(); + let storage_value = self + .client() + .get_storage_at(self.address(), storage_slot, None) + .await?; + + // H256 is always 32 bytes, so we don't need to check the length + + // The currentSequenceNumber is stored at offset 32 bytes in the storage slot + // Extract the last 8 bytes (u64) from the 32-byte value + let mut u64_bytes = [0u8; 8]; + u64_bytes.copy_from_slice(&storage_value.as_bytes()[24..32]); + + Ok(u64::from_be_bytes(u64_bytes)) + } +} diff --git a/apps/argus/src/chain/reader.rs b/apps/argus/src/chain/reader.rs new file mode 100644 index 0000000000..bdae9776e9 --- /dev/null +++ b/apps/argus/src/chain/reader.rs @@ -0,0 +1,174 @@ +use { + anyhow::Result, + axum::async_trait, + ethers::types::{Address, U256}, +}; + +pub type BlockNumber = u64; + +#[derive(Clone)] +pub struct RequestedWithCallbackEvent { + pub sequence_number: u64, + pub requester: Address, + pub price_ids: Vec<[u8; 32]>, + pub callback_gas_limit: U256, +} + +/// PulseReader is the read-only interface of the Pulse contract. +#[async_trait] +pub trait PulseReader: Send + Sync { + /// Get an in-flight request (if it exists) + async fn get_request(&self, sequence_number: u64) -> Result>; + + /// Get the latest block number + async fn get_block_number(&self) -> Result; + + async fn get_price_update_requested_events( + &self, + from_block: BlockNumber, + to_block: BlockNumber, + ) -> Result>; + + /// Alias for get_price_update_requested_events to maintain compatibility with existing code + async fn get_request_with_callback_events( + &self, + from_block: BlockNumber, + to_block: BlockNumber, + ) -> Result> { + self.get_price_update_requested_events(from_block, to_block) + .await + } + + /// Get active requests directly from contract storage + /// This is more efficient than searching for events in the backlog + async fn get_active_requests(&self, count: usize) -> Result>; + + /// Estimate the gas required to execute a callback for price updates. + async fn estimate_execute_callback_gas( + &self, + sender: Address, + sequence_number: u64, + update_data: Vec>, + price_ids: Vec<[u8; 32]>, + ) -> Result; +} + +/// An in-flight request stored in the contract. +/// (This struct is missing many fields that are defined in the contract, as they +/// aren't used in argus anywhere. Feel free to add any missing fields as necessary.) +#[derive(Clone, Debug)] +pub struct Request { + pub requester: Address, + pub sequence_number: u64, + pub callback_gas_limit: U256, + pub price_ids: Vec<[u8; 32]>, + pub publish_time: U256, + pub num_price_ids: u8, +} + +#[cfg(test)] +pub mod mock { + use { + crate::chain::reader::{BlockNumber, PulseReader, Request, RequestedWithCallbackEvent}, + anyhow::Result, + axum::async_trait, + ethers::types::{Address, U256}, + std::sync::RwLock, + }; + + /// Mock version of the pulse contract intended for testing. + /// This class is internally locked to allow tests to modify the in-flight requests while + /// the API is also holding a pointer to the same data structure. + pub struct MockPulseReader { + block_number: RwLock, + /// The set of requests that are currently in-flight. + requests: RwLock>, + } + + impl MockPulseReader { + pub fn with_requests( + block_number: BlockNumber, + requests: &[(Address, u64, BlockNumber, U256, Vec<[u8; 32]>, U256)], + ) -> MockPulseReader { + MockPulseReader { + block_number: RwLock::new(block_number), + requests: RwLock::new( + requests + .iter() + .map(|&(a, s, _, c, ref p, t)| Request { + requester: a, + sequence_number: s, + callback_gas_limit: c, + price_ids: p.clone(), + publish_time: t, + num_price_ids: p.len().min(255) as u8, + }) + .collect(), + ), + } + } + + /// Insert a new request into the set of in-flight requests. + pub fn insert( + &self, + requester: Address, + sequence: u64, + callback_gas_limit: U256, + price_ids: Vec<[u8; 32]>, + publish_time: U256, + ) -> &Self { + self.requests.write().unwrap().push(Request { + requester, + sequence_number: sequence, + callback_gas_limit, + price_ids: price_ids.clone(), + publish_time, + num_price_ids: price_ids.len().min(255) as u8, + }); + self + } + + pub fn set_block_number(&self, block_number: BlockNumber) -> &Self { + *(self.block_number.write().unwrap()) = block_number; + self + } + } + + #[async_trait] + impl PulseReader for MockPulseReader { + async fn get_request(&self, sequence_number: u64) -> Result> { + let requests = self.requests.read().unwrap(); + Ok(requests + .iter() + .find(|r| r.sequence_number == sequence_number) + .cloned()) + } + + async fn get_block_number(&self) -> Result { + Ok(*self.block_number.read().unwrap()) + } + + async fn get_active_requests(&self, count: usize) -> Result> { + let requests = self.requests.read().unwrap(); + Ok(requests.iter().take(count).cloned().collect()) + } + + async fn get_price_update_requested_events( + &self, + _from_block: BlockNumber, + _to_block: BlockNumber, + ) -> Result> { + Ok(vec![]) + } + + async fn estimate_execute_callback_gas( + &self, + _sender: Address, + _sequence_number: u64, + _update_data: Vec>, + _price_ids: Vec<[u8; 32]>, + ) -> Result { + Ok(U256::from(5)) + } + } +} diff --git a/apps/argus/src/command.rs b/apps/argus/src/command.rs new file mode 100644 index 0000000000..66d4d8bddb --- /dev/null +++ b/apps/argus/src/command.rs @@ -0,0 +1,14 @@ +mod generate; +mod get_request; +mod inspect; +mod register_provider; +mod request_price_update; +mod run; +mod setup_provider; +mod withdraw_fees; + +pub use { + generate::generate, get_request::get_request, inspect::inspect, + register_provider::register_provider, request_price_update::request_price_update, run::run, + setup_provider::setup_provider, withdraw_fees::withdraw_fees, +}; diff --git a/apps/argus/src/command/generate.rs b/apps/argus/src/command/generate.rs new file mode 100644 index 0000000000..7cd6540a83 --- /dev/null +++ b/apps/argus/src/command/generate.rs @@ -0,0 +1,80 @@ +use { + crate::{ + chain::ethereum::{PulseEvents, SignablePythContract}, + config::{Config, GenerateOptions}, + }, + anyhow::{anyhow, Result}, + ethers::{contract::EthLogDecode, types::Bytes}, + std::sync::Arc, +}; + +/// Request a price update from the Pulse contract and execute the callback. +pub async fn generate(opts: &GenerateOptions) -> Result<()> { + let contract = Arc::new( + SignablePythContract::from_config( + &Config::load(&opts.config.config)?.get_chain_config(&opts.chain_id)?, + &opts.private_key, + ) + .await?, + ); + + // Define the price IDs we want to update + // In a real implementation, these would come from configuration or command line arguments + let price_ids: Vec<[u8; 32]> = vec![]; + if price_ids.is_empty() { + return Err(anyhow!("No price IDs specified for update")); + } + + // Request a price update on the contract + // The publish_time would typically be the current time or a specific time in the future + let publish_time = chrono::Utc::now().timestamp() as u64; + let callback_gas_limit = 500000; // Example gas limit for the callback + + let sequence_number = contract + .request_price_updates_with_callback( + publish_time.into(), + price_ids.clone(), + callback_gas_limit.into(), + ) + .send() + .await? + .await? + .ok_or_else(|| anyhow!("Failed to get transaction receipt"))? + .logs + .iter() + .find_map(|log| { + let raw_log = ethers::abi::RawLog::from(log.clone()); + if let Ok(PulseEvents::PriceUpdateRequestedFilter(event_data)) = + PulseEvents::decode_log(&raw_log) + { + Some(event_data.request.sequence_number) + } else { + None + } + }) + .ok_or_else(|| anyhow!("Failed to find sequence number in transaction logs"))?; + + tracing::info!(sequence_number = sequence_number, "Price update requested"); + + // In a real implementation, we would fetch price data from a source + // For this example, we'll use empty update data + let update_data: Vec = vec![]; + + // Execute the callback with the price data + let result = contract + .execute_callback(sequence_number, update_data, price_ids) + .send() + .await? + .await?; + + if let Some(receipt) = result { + tracing::info!( + transaction_hash = ?receipt.transaction_hash, + "Price update callback executed successfully" + ); + } else { + tracing::error!("Price update callback failed: no receipt returned"); + } + + Ok(()) +} diff --git a/apps/argus/src/command/get_request.rs b/apps/argus/src/command/get_request.rs new file mode 100644 index 0000000000..cc00ef7598 --- /dev/null +++ b/apps/argus/src/command/get_request.rs @@ -0,0 +1,24 @@ +use { + crate::{ + chain::ethereum::PythContract, + config::{Config, GetRequestOptions}, + }, + anyhow::Result, + std::sync::Arc, +}; + +/// Get the on-chain request metadata for a provider and sequence number. +pub async fn get_request(opts: &GetRequestOptions) -> Result<()> { + // Initialize a Provider to interface with the EVM contract. + let contract = Arc::new(PythContract::from_config( + &Config::load(&opts.config.config)?.get_chain_config(&opts.chain_id)?, + )?); + + let r = contract + .get_request(opts.sequence) + .call() + .await?; + tracing::info!("Found request: {:?}", r); + + Ok(()) +} diff --git a/apps/argus/src/command/inspect.rs b/apps/argus/src/command/inspect.rs new file mode 100644 index 0000000000..47b236e3c6 --- /dev/null +++ b/apps/argus/src/command/inspect.rs @@ -0,0 +1,122 @@ +use { + crate::{ + chain::ethereum::{PythContract, Request}, + config::{Config, EthereumConfig, InspectOptions}, + }, + anyhow::Result, + ethers::{ + contract::Multicall, + middleware::Middleware, + prelude::{Http, Provider}, + }, + std::time::{Duration, SystemTime}, +}; + +pub async fn inspect(opts: &InspectOptions) -> Result<()> { + match opts.chain_id.clone() { + Some(chain_id) => { + let chain_config = &Config::load(&opts.config.config)?.get_chain_config(&chain_id)?; + inspect_chain(chain_config, opts.num_requests, opts.multicall_batch_size).await?; + } + None => { + let config = Config::load(&opts.config.config)?; + for (chain_id, chain_config) in config.chains.iter() { + println!("Inspecting chain: {}", chain_id); + inspect_chain(chain_config, opts.num_requests, opts.multicall_batch_size).await?; + } + } + } + Ok(()) +} + +async fn inspect_chain( + chain_config: &EthereumConfig, + num_requests: u64, + multicall_batch_size: u64, +) -> Result<()> { + let rpc_provider = Provider::::try_from(&chain_config.geth_rpc_addr)?; + let multicall_exists = rpc_provider + .get_code(ethers::contract::MULTICALL_ADDRESS, None) + .await + .expect("Failed to get code") + .len() + > 0; + + let contract = PythContract::from_config(chain_config)?; + + // Get the current sequence number directly from the contract's storage + let current_sequence_number = contract.get_current_sequence_number().await?; + + // The current sequence number is the next one to be assigned, so subtract 1 to get the latest + let latest_sequence_number = current_sequence_number.saturating_sub(1); + let mut current_request_number = latest_sequence_number; + + println!("Latest sequence number: {}", current_request_number); + + if current_request_number == 0 { + println!("No requests found"); + return Ok(()); + } + + let last_request_number = current_request_number.saturating_sub(num_requests); + + if multicall_exists { + println!("Using multicall"); + let mut multicall = Multicall::new( + rpc_provider.clone(), + Some(ethers::contract::MULTICALL_ADDRESS), + ) + .await?; + while current_request_number > last_request_number { + multicall.clear_calls(); + for _ in 0..multicall_batch_size { + if current_request_number == 0 { + break; + } + multicall.add_call(contract.get_request(current_request_number), false); + current_request_number -= 1; + } + let return_data: Vec = multicall.call_array().await?; + for request in return_data { + process_request(request).await?; + } + println!("Current request number: {}", current_request_number); + } + } else { + println!("Multicall not deployed in this chain, fetching requests one by one"); + while current_request_number > last_request_number { + let request = contract.get_request(current_request_number).call().await?; + process_request(request).await?; + current_request_number -= 1; + if current_request_number % 100 == 0 { + println!("Current request number: {}", current_request_number); + } + } + } + Ok(()) +} + +async fn process_request(request: Request) -> Result<()> { + if request.sequence_number != 0 { + // Convert publish_time to a datetime + let publish_time = request.publish_time.as_u64(); + let datetime = if publish_time > 0 { + match SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(publish_time)) { + Some(time) => format!("{:?}", time), + None => "Invalid time".to_string(), + } + } else { + "N/A".to_string() + }; + + println!( + "{} sequence_number:{} publish_time:{} requester:{} price_ids:{}", + datetime, + request.sequence_number, + request.publish_time, + request.requester, + request.price_ids.len() + ); + } + Ok(()) +} diff --git a/apps/argus/src/command/register_provider.rs b/apps/argus/src/command/register_provider.rs new file mode 100644 index 0000000000..c8ad1d2955 --- /dev/null +++ b/apps/argus/src/command/register_provider.rs @@ -0,0 +1,54 @@ +use { + crate::{ + chain::ethereum::SignablePythContract, + config::{Config, EthereumConfig, ProviderConfig, RegisterProviderOptions}, + }, + anyhow::{anyhow, Result}, + ethers::signers::{LocalWallet, Signer}, + std::sync::Arc, +}; + +/// Register as a price update provider. This method will register the provider with the Pulse contract. +pub async fn register_provider(opts: &RegisterProviderOptions) -> Result<()> { + let config = Config::load(&opts.config.config)?; + let chain_config = config.get_chain_config(&opts.chain_id)?; + + register_provider_from_config(&config.provider, &chain_config).await?; + + Ok(()) +} + +pub async fn register_provider_from_config( + provider_config: &ProviderConfig, + chain_config: &EthereumConfig, +) -> Result<()> { + let private_key_string = provider_config.private_key.load()?.ok_or(anyhow!( + "Please specify a provider private key in the config" + ))?; + + // Initialize a Provider to interface with the EVM contract. + let contract = + Arc::new(SignablePythContract::from_config(chain_config, &private_key_string).await?); + + let wallet = private_key_string.parse::()?; + let provider_address = wallet.address(); + + // Register the provider with the contract + tracing::info!("Registering provider with address: {:?}", provider_address); + tracing::info!("Provider fee: {}", chain_config.fee); + + // Register the provider with the fee + if let Some(receipt) = contract + .register_provider(chain_config.fee) + .send() + .await? + .await? + { + tracing::info!("Provider registration successful: {:?}", receipt); + } else { + tracing::error!("Provider registration failed: no receipt returned"); + return Err(anyhow!("Provider registration failed: no receipt returned")); + } + + Ok(()) +} diff --git a/apps/argus/src/command/request_price_update.rs b/apps/argus/src/command/request_price_update.rs new file mode 100644 index 0000000000..6231f653db --- /dev/null +++ b/apps/argus/src/command/request_price_update.rs @@ -0,0 +1,64 @@ +use { + crate::{ + chain::ethereum::SignablePythContract, + config::{Config, RequestPriceUpdateOptions}, + }, + anyhow::{anyhow, Result}, + ethers::types::U256, + std::{sync::Arc, time::{SystemTime, UNIX_EPOCH}}, +}; + +pub async fn request_price_update(opts: &RequestPriceUpdateOptions) -> Result<()> { + let contract = Arc::new( + SignablePythContract::from_config( + &Config::load(&opts.config.config)?.get_chain_config(&opts.chain_id)?, + &opts.private_key, + ) + .await?, + ); + + // Parse price IDs (now required) + let price_ids = parse_price_ids(&opts.price_ids)?; + + // Use current timestamp as publish time + let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let publish_time = U256::from(current_time); + + // Set callback gas limit from options + let callback_gas_limit = U256::from(opts.callback_gas_limit); + + // Request price updates with callback + let sequence_number = contract + .request_price_updates_wrapper(publish_time, price_ids, callback_gas_limit) + .await?; + + tracing::info!("Price update requested with sequence number: {:#?}", sequence_number); + + Ok(()) +} + +// Helper function to parse comma-separated hex strings into price IDs +fn parse_price_ids(price_ids_str: &str) -> Result> { + let ids: Vec<&str> = price_ids_str.split(',').map(|s| s.trim()).collect(); + + if ids.is_empty() { + return Err(anyhow!("No price IDs provided. Please specify at least one price ID.")); + } + + let mut result = Vec::with_capacity(ids.len()); + + for id_str in ids { + let id_str = id_str.trim_start_matches("0x"); + if id_str.len() != 64 { + return Err(anyhow!("Invalid price ID format: {}. Expected 32-byte hex string (64 characters)", id_str)); + } + + let mut id = [0u8; 32]; + hex::decode_to_slice(id_str, &mut id) + .map_err(|e| anyhow!("Failed to decode price ID {}: {}", id_str, e))?; + + result.push(id); + } + + Ok(result) +} diff --git a/apps/argus/src/command/run.rs b/apps/argus/src/command/run.rs new file mode 100644 index 0000000000..d95931eaef --- /dev/null +++ b/apps/argus/src/command/run.rs @@ -0,0 +1,316 @@ +use { + crate::{ + api::{self, BlockchainState, ChainId}, + chain::ethereum::InstrumentedPythContract, + config::{Config, EthereumConfig, RunOptions}, + keeper::{self, keeper_metrics::KeeperMetrics}, + }, + anyhow::{anyhow, Error, Result}, + axum::Router, + ethers::{ + middleware::Middleware, + types::{Address, BlockNumber}, + }, + fortuna::eth_utils::traced_client::{RpcMetrics, TracedClient}, + futures::future::join_all, + prometheus_client::{ + encoding::EncodeLabelSet, + metrics::{family::Family, gauge::Gauge}, + registry::Registry, + }, + std::{ + collections::HashMap, + net::SocketAddr, + sync::Arc, + time::{Duration, SystemTime, UNIX_EPOCH}, + }, + tokio::{ + spawn, + sync::{watch, RwLock}, + time, + }, + tower_http::cors::CorsLayer, + utoipa::OpenApi, + utoipa_swagger_ui::SwaggerUi, +}; + +/// Track metrics in this interval +const TRACK_INTERVAL: Duration = Duration::from_secs(10); + +pub async fn run_api( + socket_addr: SocketAddr, + chains: HashMap, + metrics_registry: Arc>, + mut rx_exit: watch::Receiver, +) -> Result<()> { + #[derive(OpenApi)] + #[openapi( + paths( + crate::api::price_update, + crate::api::chain_ids, + ), + components( + schemas( + crate::api::GetPriceUpdateResponse, + crate::api::PriceUpdateData, + crate::api::ResponseFormat, + ) + ), + tags( + (name = "argus", description = "Price update service for the Pyth Pulse protocol") + ) + )] + struct ApiDoc; + + let api_state = api::ApiState::new(chains, metrics_registry).await; + + // Initialize Axum Router. Note the type here is a `Router` due to the use of the + // `with_state` method which replaces `Body` with `State` in the type signature. + let app = Router::new(); + let app = app + .merge(SwaggerUi::new("/docs").url("/docs/openapi.json", ApiDoc::openapi())) + .merge(api::routes(api_state)) + // Permissive CORS layer to allow all origins + .layer(CorsLayer::permissive()); + + tracing::info!("Starting server on: {:?}", &socket_addr); + // Binds the axum's server to the configured address and port. This is a blocking call and will + // not return until the server is shutdown. + axum::Server::try_bind(&socket_addr)? + .serve(app.into_make_service()) + .with_graceful_shutdown(async { + // It can return an error or an Ok(()). In both cases, we would shut down. + // As Ok(()) means, exit signal (ctrl + c) was received. + // And Err(e) means, the sender was dropped which should not be the case. + let _ = rx_exit.changed().await; + + tracing::info!("Shutting down RPC server..."); + }) + .await?; + + Ok(()) +} + +pub async fn run_keeper( + chains: HashMap, + config: Config, + private_key: String, + metrics_registry: Arc>, + rpc_metrics: Arc, + hermes_base_url: String, +) -> Result<()> { + let mut handles = Vec::new(); + let keeper_metrics: Arc = Arc::new({ + let chain_labels: Vec<(String, Address)> = chains + .iter() + .map(|(id, state)| (id.clone(), state.provider_address)) + .collect(); + KeeperMetrics::new(metrics_registry.clone(), chain_labels).await + }); + for (chain_id, chain_config) in chains { + let chain_eth_config = config + .chains + .get(&chain_id) + .expect("All chains should be present in the config file") + .clone(); + let private_key = private_key.clone(); + handles.push(spawn(keeper::run_keeper_threads( + private_key, + chain_eth_config, + chain_config.clone(), + keeper_metrics.clone(), + rpc_metrics.clone(), + hermes_base_url.clone(), + ))); + } + + Ok(()) +} + +pub async fn run(opts: &RunOptions) -> Result<()> { + let config = Config::load(&opts.config.config)?; + let (tx_exit, rx_exit) = watch::channel(false); + let metrics_registry = Arc::new(RwLock::new(Registry::default())); + let rpc_metrics = Arc::new(RpcMetrics::new(metrics_registry.clone()).await); + + // Store the hermes base URL from the command-line argument + let hermes_base_url = opts.hermes_base_url.clone(); + + let mut tasks = Vec::new(); + for (chain_id, chain_config) in config.chains.clone() { + let rpc_metrics = rpc_metrics.clone(); + tasks.push(spawn(async move { + let state = setup_chain_state( + &config.provider.address, + &chain_id, + &chain_config, + rpc_metrics, + ) + .await; + + (chain_id, state) + })); + } + let states = join_all(tasks).await; + + let mut chains: HashMap = HashMap::new(); + for result in states { + let (chain_id, state) = result?; + + match state { + Ok(state) => { + chains.insert(chain_id.clone(), state); + } + Err(e) => { + tracing::error!("Failed to setup {} {}", chain_id, e); + } + } + } + if chains.is_empty() { + return Err(anyhow!("No chains were successfully setup")); + } + + // Listen for Ctrl+C so we can set the exit flag and wait for a graceful shutdown. + spawn(async move { + tracing::info!("Registered shutdown signal handler..."); + tokio::signal::ctrl_c().await.unwrap(); + tracing::info!("Shut down signal received, waiting for tasks..."); + // no need to handle error here, as it will only occur when all the + // receiver has been dropped and that's what we want to do + tx_exit.send(true)?; + + Ok::<(), Error>(()) + }); + + if let Some(keeper_private_key) = config.keeper.private_key.load()? { + spawn(run_keeper( + chains.clone(), + config.clone(), + keeper_private_key, + metrics_registry.clone(), + rpc_metrics.clone(), + hermes_base_url.clone(), + )); + } else { + tracing::info!("Not starting keeper service: no keeper private key specified. Please add one to the config if you would like to run the keeper service.") + } + + // Spawn a thread to track latest block lag. This helps us know if the rpc is up and updated with the latest block. + spawn(track_block_timestamp_lag( + config, + metrics_registry.clone(), + rpc_metrics.clone(), + )); + + run_api(opts.addr, chains, metrics_registry, rx_exit).await?; + + Ok(()) +} + +async fn setup_chain_state( + provider: &Address, + chain_id: &ChainId, + chain_config: &EthereumConfig, + rpc_metrics: Arc, +) -> Result { + let contract = Arc::new(InstrumentedPythContract::from_config( + chain_config, + chain_id.clone(), + rpc_metrics, + )?); + + // Verify the provider is registered + let provider_info = contract.get_provider_info(*provider).call().await?; + if !provider_info.is_registered { + return Err(anyhow!( + "Provider {} is not registered on chain {}", + provider, + chain_id + )); + } + + tracing::info!("Provider {} is registered on chain {}", provider, chain_id); + + let state = BlockchainState { + id: chain_id.clone(), + contract, + provider_address: *provider, + }; + Ok(state) +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] +pub struct ChainLabel { + pub chain_id: String, +} + +#[tracing::instrument(name = "block_timestamp_lag", skip_all, fields(chain_id = chain_id))] +pub async fn check_block_timestamp_lag( + chain_id: String, + chain_config: EthereumConfig, + metrics: Family, + rpc_metrics: Arc, +) { + let provider = + match TracedClient::new(chain_id.clone(), &chain_config.geth_rpc_addr, rpc_metrics) { + Ok(r) => r, + Err(e) => { + tracing::error!("Failed to create provider for chain id - {:?}", e); + return; + } + }; + + const INF_LAG: i64 = 1000000; // value that definitely triggers an alert + let lag = match provider.get_block(BlockNumber::Latest).await { + Ok(block) => match block { + Some(block) => { + let block_timestamp = block.timestamp; + let server_timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + let lag: i64 = (server_timestamp as i64) - (block_timestamp.as_u64() as i64); + lag + } + None => { + tracing::error!("Block is None"); + INF_LAG + } + }, + Err(e) => { + tracing::error!("Failed to get block - {:?}", e); + INF_LAG + } + }; + metrics + .get_or_create(&ChainLabel { + chain_id: chain_id.clone(), + }) + .set(lag); +} + +/// Tracks the difference between the server timestamp and the latest block timestamp for each chain +pub async fn track_block_timestamp_lag( + config: Config, + metrics_registry: Arc>, + rpc_metrics: Arc, +) { + let metrics = Family::::default(); + metrics_registry.write().await.register( + "block_timestamp_lag", + "The difference between server timestamp and latest block timestamp", + metrics.clone(), + ); + loop { + for (chain_id, chain_config) in &config.chains { + spawn(check_block_timestamp_lag( + chain_id.clone(), + chain_config.clone(), + metrics.clone(), + rpc_metrics.clone(), + )); + } + + time::sleep(TRACK_INTERVAL).await; + } +} diff --git a/apps/argus/src/command/setup_provider.rs b/apps/argus/src/command/setup_provider.rs new file mode 100644 index 0000000000..8f4f9105ad --- /dev/null +++ b/apps/argus/src/command/setup_provider.rs @@ -0,0 +1,136 @@ +use { + crate::{ + api::ChainId, + chain::ethereum::{ProviderInfo, SignablePythContract}, + command::register_provider::register_provider_from_config, + config::{Config, EthereumConfig, SetupProviderOptions}, + }, + anyhow::{anyhow, Result}, + ethers::{ + signers::{LocalWallet, Signer}, + types::Address, + }, + futures::future::join_all, + std::sync::Arc, + tokio::spawn, + tracing::Instrument, +}; + +/// Setup provider for all the chains. +pub async fn setup_provider(opts: &SetupProviderOptions) -> Result<()> { + let config = Config::load(&opts.config.config)?; + let setup_tasks = config + .chains + .clone() + .into_iter() + .map(|(chain_id, chain_config)| { + let config = config.clone(); + spawn(async move { + ( + setup_chain_provider(&config, &chain_id, &chain_config).await, + chain_id, + ) + }) + }) + .collect::>(); + let join_results = join_all(setup_tasks).await; + let mut all_ok = true; + for join_result in join_results { + let (setup_result, chain_id) = join_result?; + match setup_result { + Ok(()) => {} + Err(e) => { + tracing::error!("Failed to setup {} {}", chain_id, e); + all_ok = false; + } + } + } + + match all_ok { + true => Ok(()), + false => Err(anyhow!("Failed to setup provider for all chains")), + } +} + +/// Setup provider for a single chain. +/// 1. Register if there was no previous registration. +/// 2. Update provider fee if there is a mismatch with the fee set on contract. +/// 3. Update fee manager if there is a mismatch with the fee manager set on contract. +#[tracing::instrument(name = "setup_chain_provider", skip_all, fields(chain_id = chain_id))] +async fn setup_chain_provider( + config: &Config, + chain_id: &ChainId, + chain_config: &EthereumConfig, +) -> Result<()> { + tracing::info!("Setting up provider for chain: {0}", chain_id); + let provider_config = &config.provider; + let private_key = provider_config.private_key.load()?.ok_or(anyhow!( + "Please specify a provider private key in the config file." + ))?; + let provider_address = private_key.clone().parse::()?.address(); + // Initialize a Provider to interface with the EVM contract. + let contract = Arc::new(SignablePythContract::from_config(chain_config, &private_key).await?); + + tracing::info!("Fetching provider info"); + let provider_info = contract.get_provider_info(provider_address).call().await?; + tracing::info!("Provider info: {:?}", provider_info); + + // Register if the provider is not already registered + if !provider_info.is_registered { + tracing::info!("Provider not registered, registering now"); + register_provider_from_config(provider_config, chain_config) + .await + .map_err(|e| anyhow!("Chain: {} - Failed to register provider: {}", &chain_id, e))?; + tracing::info!("Registered"); + } + + // Fetch updated provider info after registration + let provider_info = contract.get_provider_info(provider_address).call().await?; + + sync_fee(&contract, &provider_info, chain_config.fee) + .in_current_span() + .await?; + + sync_fee_manager( + &contract, + &provider_info, + provider_config.fee_manager.unwrap_or(Address::zero()), + ) + .in_current_span() + .await?; + + Ok(()) +} + +async fn sync_fee( + contract: &Arc, + provider_info: &ProviderInfo, + provider_fee: u128, +) -> Result<()> { + if provider_info.fee_in_wei != provider_fee { + tracing::info!("Updating provider fee {}", provider_fee); + if let Some(r) = contract + .set_provider_fee(provider_fee) + .send() + .await? + .await? + { + tracing::info!("Updated provider fee: {:?}", r); + } + } + Ok(()) +} + +async fn sync_fee_manager( + contract: &Arc, + provider_info: &ProviderInfo, + fee_manager: Address, +) -> Result<()> { + if provider_info.fee_manager != fee_manager { + tracing::info!("Updating provider fee manager to {:?}", fee_manager); + if let Some(receipt) = contract.set_fee_manager(fee_manager).send().await?.await? { + tracing::info!("Updated provider fee manager: {:?}", receipt); + } + } + Ok(()) +} diff --git a/apps/argus/src/command/withdraw_fees.rs b/apps/argus/src/command/withdraw_fees.rs new file mode 100644 index 0000000000..c037d0bfcd --- /dev/null +++ b/apps/argus/src/command/withdraw_fees.rs @@ -0,0 +1,90 @@ +use { + crate::{ + chain::ethereum::SignablePythContract, + config::{Config, WithdrawFeesOptions}, + }, + anyhow::{anyhow, Result}, + ethers::{signers::Signer, types::Address}, +}; + +pub async fn withdraw_fees(opts: &WithdrawFeesOptions) -> Result<()> { + let config = Config::load(&opts.config.config)?; + + let private_key_string = if opts.keeper { + config.keeper.private_key.load()?.ok_or(anyhow!("Please specify a keeper private key in the config or omit the --keeper option to use the provider private key"))? + } else { + config.provider.private_key.load()?.ok_or(anyhow!( + "Please specify a provider private key in the config or provide the --keeper option to use the keeper private key instead." + ))? + }; + + match opts.chain_id.clone() { + Some(chain_id) => { + let chain_config = &config.get_chain_config(&chain_id)?; + let contract = + SignablePythContract::from_config(chain_config, &private_key_string).await?; + + withdraw_fees_for_chain( + contract, + config.provider.address, + opts.keeper, + opts.retain_balance_wei, + ) + .await?; + } + None => { + for (chain_id, chain_config) in config.chains.iter() { + tracing::info!("Withdrawing fees for chain: {}", chain_id); + let contract = + SignablePythContract::from_config(chain_config, &private_key_string).await?; + + withdraw_fees_for_chain( + contract, + config.provider.address, + opts.keeper, + opts.retain_balance_wei, + ) + .await?; + } + } + } + Ok(()) +} + +pub async fn withdraw_fees_for_chain( + contract: SignablePythContract, + provider_address: Address, + is_fee_manager: bool, + retained_balance: u128, +) -> Result<()> { + tracing::info!("Fetching fees for provider: {:?}", provider_address); + let provider_info = contract.get_provider_info(provider_address).call().await?; + let fees = provider_info.accrued_fees_in_wei; + tracing::info!("Accrued fees: {} wei", fees); + + let withdrawal_amount_wei = fees.saturating_sub(retained_balance); + if withdrawal_amount_wei > 0 { + tracing::info!( + "Withdrawing {} wei to {}...", + withdrawal_amount_wei, + contract.wallet().address() + ); + + let call = match is_fee_manager { + true => contract.withdraw_as_fee_manager(provider_address, withdrawal_amount_wei), + false => contract.withdraw_fees(withdrawal_amount_wei), + }; + let tx_result = call.send().await?.await?; + + match &tx_result { + Some(receipt) => { + tracing::info!("Withdrawal transaction hash {:?}", receipt.transaction_hash); + } + None => { + tracing::warn!("No transaction receipt. Unclear what happened to the transaction"); + } + } + } + + Ok(()) +} diff --git a/apps/argus/src/config.rs b/apps/argus/src/config.rs new file mode 100644 index 0000000000..e499d08509 --- /dev/null +++ b/apps/argus/src/config.rs @@ -0,0 +1,325 @@ +use { + crate::api::ChainId, + anyhow::{anyhow, Result}, + clap::{crate_authors, crate_description, crate_name, crate_version, Args, Parser}, + ethers::types::Address, + fortuna::eth_utils::utils::EscalationPolicy, + std::{collections::HashMap, fs}, +}; +pub use { + generate::GenerateOptions, get_request::GetRequestOptions, inspect::InspectOptions, + register_provider::RegisterProviderOptions, request_price_update::RequestPriceUpdateOptions, + run::RunOptions, setup_provider::SetupProviderOptions, withdraw_fees::WithdrawFeesOptions, +}; + +mod generate; +mod get_request; +mod inspect; +mod register_provider; +mod request_price_update; +mod run; +mod setup_provider; +mod withdraw_fees; + +const DEFAULT_RPC_ADDR: &str = "127.0.0.1:34000"; +const DEFAULT_HTTP_ADDR: &str = "http://127.0.0.1:34000"; +const DEFAULT_HERMES_BASE_URL: &str = "https://hermes.pyth.network"; + +#[derive(Parser, Debug)] +#[command(name = crate_name!())] +#[command(author = crate_authors!())] +#[command(about = crate_description!())] +#[command(version = crate_version!())] +#[allow(clippy::large_enum_variant)] +pub enum Options { + /// Run the Price Update Service. + Run(RunOptions), + + /// Register a new provider with the Pyth Pulse contract. + RegisterProvider(RegisterProviderOptions), + + /// Set up the provider for all the provided chains. + /// It registers, re-registers, or updates provider config on chain. + SetupProvider(SetupProviderOptions), + + /// Request a price update from the contract. + RequestPriceUpdate(RequestPriceUpdateOptions), + + /// Inspect recent requests and find unfulfilled price update requests. + Inspect(InspectOptions), + + /// Generate a price update by running the entire protocol end-to-end + Generate(GenerateOptions), + + /// Get the status of a pending request for a price update. + GetRequest(GetRequestOptions), + + /// Withdraw any of the provider's accumulated fees from the contract. + WithdrawFees(WithdrawFeesOptions), +} + +#[derive(Args, Clone, Debug)] +#[command(next_help_heading = "Config Options")] +#[group(id = "Config")] +pub struct ConfigOptions { + /// Path to a configuration file containing the list of supported blockchains + #[arg(long = "config")] + #[arg(env = "ARGUS_CONFIG")] + #[arg(default_value = "config.yaml")] + pub config: String, +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct Config { + pub chains: HashMap, + pub provider: ProviderConfig, + pub keeper: KeeperConfig, +} + +impl Config { + pub fn load(path: &str) -> Result { + // Open and read the YAML file + // TODO: the default serde deserialization doesn't enforce unique keys + let yaml_content = fs::read_to_string(path)?; + let config: Config = serde_yaml::from_str(&yaml_content)?; + + // Run correctness checks for the config and fail if there are any issues. + for (chain_id, config) in config.chains.iter() { + if !(config.min_profit_pct <= config.target_profit_pct + && config.target_profit_pct <= config.max_profit_pct) + { + return Err(anyhow!("chain id {:?} configuration is invalid. Config must satisfy min_profit_pct <= target_profit_pct <= max_profit_pct.", chain_id)); + } + } + + Ok(config) + } + + pub fn get_chain_config(&self, chain_id: &ChainId) -> Result { + self.chains.get(chain_id).cloned().ok_or(anyhow!( + "Could not find chain id {} in the configuration", + &chain_id + )) + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct EthereumConfig { + /// URL of a Geth RPC endpoint to use for interacting with the blockchain. + /// TODO: Change type from String to Url + pub geth_rpc_addr: String, + + /// URL of a Geth RPC wss endpoint to use for subscribing to blockchain events. + pub geth_rpc_wss: Option, + + /// Address of a Pyth Pulse contract to interact with. + pub contract_addr: Address, + + /// Use the legacy transaction format (for networks without EIP 1559) + #[serde(default)] + pub legacy_tx: bool, + + /// The gas limit to use for price update callback transactions. + pub gas_limit: u64, + + /// The percentage multiplier to apply to priority fee estimates (100 = no change, e.g. 150 = 150% of base fee) + #[serde(default = "default_priority_fee_multiplier_pct")] + pub priority_fee_multiplier_pct: u64, + + /// The escalation policy governs how the gas limit and fee are increased during backoff retries. + #[serde(default)] + pub escalation_policy: EscalationPolicyConfig, + + /// The minimum percentage profit to earn as a function of the callback cost. + /// For example, 20 means a profit of 20% over the cost of a callback that uses the full gas limit. + /// The fee will be raised if the profit is less than this number. + /// The minimum value for this is -100. If set to < 0, it means the keeper may lose money on callbacks that use the full gas limit. + pub min_profit_pct: i64, + + /// The target percentage profit to earn as a function of the callback cost. + /// For example, 20 means a profit of 20% over the cost of a callback that uses the full gas limit. + /// The fee will be set to this target whenever it falls outside the min/max bounds. + /// The minimum value for this is -100. If set to < 0, it means the keeper may lose money on callbacks that use the full gas limit. + pub target_profit_pct: i64, + + /// The maximum percentage profit to earn as a function of the callback cost. + /// For example, 100 means a profit of 100% over the cost of a callback that uses the full gas limit. + /// The fee will be lowered if it is more profitable than specified here. + /// Must be larger than min_profit_pct. + /// The minimum value for this is -100. If set to < 0, it means the keeper may lose money on callbacks that use the full gas limit. + pub max_profit_pct: i64, + + /// Minimum wallet balance for the keeper. If the balance falls below this level, the keeper will + /// withdraw fees from the contract to top up. This functionality requires the keeper to be the fee + /// manager for the provider. + #[serde(default)] + pub min_keeper_balance: u128, + + /// How much the provider charges for a request on this chain. + #[serde(default)] + pub fee: u128, + + /// Maximum number of hashes to record in a request. + /// This should be set according to the maximum gas limit the provider supports for callbacks. + pub max_num_hashes: Option, + + /// A list of delays (in blocks) that indicates how many blocks should be delayed + /// before we process a block. For retry logic, we can process blocks multiple times + /// at each specified delay. For example: [5, 10, 20]. + #[serde(default = "default_block_delays")] + pub block_delays: Vec, +} + +fn default_block_delays() -> Vec { + vec![5] +} + +fn default_priority_fee_multiplier_pct() -> u64 { + 100 +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct EscalationPolicyConfig { + // The keeper will perform the callback as long as the tx is within this percentage of the configured gas limit. + // Default value is 110, meaning a 10% tolerance over the configured value. + #[serde(default = "default_gas_limit_tolerance_pct")] + pub gas_limit_tolerance_pct: u64, + + /// The initial gas multiplier to apply to the tx gas estimate + #[serde(default = "default_initial_gas_multiplier_pct")] + pub initial_gas_multiplier_pct: u64, + + /// The gas multiplier to apply to the tx gas estimate during backoff retries. + /// The gas on each successive retry is multiplied by this value, with the maximum multiplier capped at `gas_multiplier_cap_pct`. + #[serde(default = "default_gas_multiplier_pct")] + pub gas_multiplier_pct: u64, + /// The maximum gas multiplier to apply to the tx gas estimate during backoff retries. + #[serde(default = "default_gas_multiplier_cap_pct")] + pub gas_multiplier_cap_pct: u64, + + /// The fee multiplier to apply to the fee during backoff retries. + /// The initial fee is 100% of the estimate (which itself may be padded based on our chain configuration) + /// The fee on each successive retry is multiplied by this value, with the maximum multiplier capped at `fee_multiplier_cap_pct`. + #[serde(default = "default_fee_multiplier_pct")] + pub fee_multiplier_pct: u64, + #[serde(default = "default_fee_multiplier_cap_pct")] + pub fee_multiplier_cap_pct: u64, +} + +fn default_gas_limit_tolerance_pct() -> u64 { + 110 +} + +fn default_initial_gas_multiplier_pct() -> u64 { + 125 +} + +fn default_gas_multiplier_pct() -> u64 { + 110 +} + +fn default_gas_multiplier_cap_pct() -> u64 { + 600 +} + +fn default_fee_multiplier_pct() -> u64 { + 110 +} + +fn default_fee_multiplier_cap_pct() -> u64 { + 200 +} + +impl Default for EscalationPolicyConfig { + fn default() -> Self { + Self { + gas_limit_tolerance_pct: default_gas_limit_tolerance_pct(), + initial_gas_multiplier_pct: default_initial_gas_multiplier_pct(), + gas_multiplier_pct: default_gas_multiplier_pct(), + gas_multiplier_cap_pct: default_gas_multiplier_cap_pct(), + fee_multiplier_pct: default_fee_multiplier_pct(), + fee_multiplier_cap_pct: default_fee_multiplier_cap_pct(), + } + } +} + +impl EscalationPolicyConfig { + pub fn to_policy(&self) -> EscalationPolicy { + EscalationPolicy { + gas_limit_tolerance_pct: self.gas_limit_tolerance_pct, + initial_gas_multiplier_pct: self.initial_gas_multiplier_pct, + gas_multiplier_pct: self.gas_multiplier_pct, + gas_multiplier_cap_pct: self.gas_multiplier_cap_pct, + fee_multiplier_pct: self.fee_multiplier_pct, + fee_multiplier_cap_pct: self.fee_multiplier_cap_pct, + } + } +} + +/// Configuration values that are common to a single provider (and shared across chains). +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct ProviderConfig { + /// The URI where clients can retrieve price updates from this provider, + /// i.e., wherever Argus for this provider will be hosted. + pub uri: String, + + /// The public key of the provider whose requests the server will respond to. + pub address: Address, + + /// The provider's private key, which is required to register, update provider information, + /// or claim fees. This argument *will not* be loaded for commands that do not need + /// the private key (e.g., running the server). + pub private_key: SecretString, + + /// The length of the hash chain to generate. + pub chain_length: u64, + + /// How frequently the hash chain is sampled -- increase this value to tradeoff more + /// compute per request for less RAM use. + #[serde(default = "default_chain_sample_interval")] + pub chain_sample_interval: u64, + + /// The address of the fee manager for the provider. Set this value to the keeper wallet address to + /// enable keeper balance top-ups. + pub fee_manager: Option
, +} + +fn default_chain_sample_interval() -> u64 { + 1 +} + +/// Configuration values for the keeper service that are shared across chains. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct KeeperConfig { + /// If provided, the keeper will run alongside the Argus API service. + /// The private key is a 20-byte (40 char) hex encoded Ethereum private key. + /// This key is required to submit transactions for price update callback requests. + /// This key *does not need to be a registered provider*. In particular, production deployments + /// should ensure this is a different key in order to reduce the severity of security breaches. + pub private_key: SecretString, +} + +// A secret is a string that can be provided either as a literal in the config, +// or in a separate file. (The separate file option is useful for 1password mounting in production.) +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct SecretString { + pub value: Option, + + // The name of a file containing the string to read. Note that the file contents is trimmed + // of leading/trailing whitespace when read. + pub file: Option, +} + +impl SecretString { + pub fn load(&self) -> Result> { + if let Some(v) = &self.value { + return Ok(Some(v.clone())); + } + + if let Some(v) = &self.file { + return Ok(Some(fs::read_to_string(v)?.trim().to_string())); + } + + Ok(None) + } +} diff --git a/apps/argus/src/config/generate.rs b/apps/argus/src/config/generate.rs new file mode 100644 index 0000000000..198b15eee4 --- /dev/null +++ b/apps/argus/src/config/generate.rs @@ -0,0 +1,37 @@ +use { + crate::{api::ChainId, config::ConfigOptions}, + clap::Args, + ethers::types::Address, + reqwest::Url, +}; + +#[derive(Args, Clone, Debug)] +#[command(next_help_heading = "Generate Options")] +#[group(id = "Generate")] +pub struct GenerateOptions { + #[command(flatten)] + pub config: ConfigOptions, + + /// The chain on which to submit the random number generation request. + #[arg(long = "chain-id")] + #[arg(env = "FORTUNA_CHAIN_ID")] + pub chain_id: ChainId, + + /// A 20-byte (40 char) hex encoded Ethereum private key. + /// This key is required to submit transactions (such as registering with the contract). + #[arg(long = "private-key")] + #[arg(env = "PRIVATE_KEY")] + #[arg(default_value = None)] + pub private_key: String, + + /// Submit a randomness request to this provider + #[arg(long = "provider")] + pub provider: Address, + + #[arg(long = "url")] + #[arg(default_value = super::DEFAULT_HTTP_ADDR)] + pub url: Url, + + #[arg(short = 'b')] + pub blockhash: bool, +} diff --git a/apps/argus/src/config/get_request.rs b/apps/argus/src/config/get_request.rs new file mode 100644 index 0000000000..9377ba369f --- /dev/null +++ b/apps/argus/src/config/get_request.rs @@ -0,0 +1,29 @@ +use { + crate::{api::ChainId, config::ConfigOptions}, + clap::Args, + ethers::types::Address, +}; + +#[derive(Args, Clone, Debug)] +#[command(next_help_heading = "Get Request Options")] +#[group(id = "GetRequest")] +pub struct GetRequestOptions { + #[command(flatten)] + pub config: ConfigOptions, + + /// Retrieve a randomness request to this provider + #[arg(long = "chain-id")] + #[arg(env = "FORTUNA_CHAIN_ID")] + pub chain_id: ChainId, + + /// Retrieve a randomness request to this provider + #[arg(long = "provider")] + #[arg(env = "FORTUNA_PROVIDER")] + pub provider: Address, + + /// The sequence number of the request to retrieve + #[arg(long = "sequence")] + #[arg(env = "FORTUNA_SEQUENCE")] + #[arg(default_value = "0")] + pub sequence: u64, +} diff --git a/apps/argus/src/config/inspect.rs b/apps/argus/src/config/inspect.rs new file mode 100644 index 0000000000..c38469da24 --- /dev/null +++ b/apps/argus/src/config/inspect.rs @@ -0,0 +1,24 @@ +use { + crate::{api::ChainId, config::ConfigOptions}, + clap::Args, +}; + +#[derive(Args, Clone, Debug)] +#[command(next_help_heading = "Inspect Options")] +#[group(id = "Inspect")] +pub struct InspectOptions { + #[command(flatten)] + pub config: ConfigOptions, + + /// Check the requests on this chain, or all chains if not specified. + #[arg(long = "chain-id")] + pub chain_id: Option, + + /// The number of requests to inspect starting from the most recent request. + #[arg(long = "num-requests", default_value = "1000")] + pub num_requests: u64, + + /// The number of calls to make in each batch when using multicall. + #[arg(long = "multicall-batch-size", default_value = "100")] + pub multicall_batch_size: u64, +} diff --git a/apps/argus/src/config/register_provider.rs b/apps/argus/src/config/register_provider.rs new file mode 100644 index 0000000000..2d26ff5985 --- /dev/null +++ b/apps/argus/src/config/register_provider.rs @@ -0,0 +1,17 @@ +use { + crate::{api::ChainId, config::ConfigOptions}, + clap::Args, +}; + +#[derive(Args, Clone, Debug)] +#[command(next_help_heading = "Register Provider Options")] +#[group(id = "RegisterProvider")] +pub struct RegisterProviderOptions { + #[command(flatten)] + pub config: ConfigOptions, + + /// Register the provider on this chain + #[arg(long = "chain-id")] + #[arg(env = "FORTUNA_CHAIN_ID")] + pub chain_id: ChainId, +} diff --git a/apps/argus/src/config/request_price_update.rs b/apps/argus/src/config/request_price_update.rs new file mode 100644 index 0000000000..7399571416 --- /dev/null +++ b/apps/argus/src/config/request_price_update.rs @@ -0,0 +1,41 @@ +use { + crate::{api::ChainId, config::ConfigOptions}, + clap::Args, + ethers::types::Address, +}; + +#[derive(Args, Clone, Debug)] +#[command(next_help_heading = "Request Price Update Options")] +#[group(id = "RequestPriceUpdate")] +pub struct RequestPriceUpdateOptions { + #[command(flatten)] + pub config: ConfigOptions, + + /// Request price updates on this blockchain. + #[arg(long = "chain-id")] + #[arg(env = "ARGUS_CHAIN_ID")] + pub chain_id: ChainId, + + /// A 20-byte (40 char) hex encoded Ethereum private key. + /// This key is required to submit transactions (such as registering with the contract). + #[arg(long = "private-key")] + #[arg(env = "PRIVATE_KEY")] + pub private_key: String, + + /// Submit a price update request to this provider + #[arg(long = "provider")] + #[arg(env = "ARGUS_PROVIDER")] + pub provider: Address, + + /// Price IDs to update (comma-separated hex strings, e.g. "0x1234...5678,0xabcd...ef01") + #[arg(long = "price-ids")] + #[arg(env = "ARGUS_PRICE_IDS")] + #[arg(required = true)] + pub price_ids: String, + + /// Callback gas limit for the price update request + #[arg(long = "callback-gas-limit")] + #[arg(env = "ARGUS_CALLBACK_GAS_LIMIT")] + #[arg(default_value = "500000")] + pub callback_gas_limit: u64, +} diff --git a/apps/argus/src/config/run.rs b/apps/argus/src/config/run.rs new file mode 100644 index 0000000000..bfd1581dc6 --- /dev/null +++ b/apps/argus/src/config/run.rs @@ -0,0 +1,20 @@ +use {crate::config::ConfigOptions, clap::Args, std::net::SocketAddr}; + +/// Run the webservice +#[derive(Args, Clone, Debug)] +pub struct RunOptions { + #[command(flatten)] + pub config: ConfigOptions, + + /// Address and port the HTTP server will bind to. + #[arg(long = "rpc-listen-addr")] + #[arg(default_value = super::DEFAULT_RPC_ADDR)] + #[arg(env = "RPC_ADDR")] + pub addr: SocketAddr, + + /// Base URL for the Hermes API + #[arg(long = "hermes-base-url")] + #[arg(default_value = super::DEFAULT_HERMES_BASE_URL)] + #[arg(env = "HERMES_BASE_URL")] + pub hermes_base_url: String, +} diff --git a/apps/argus/src/config/setup_provider.rs b/apps/argus/src/config/setup_provider.rs new file mode 100644 index 0000000000..70683da21b --- /dev/null +++ b/apps/argus/src/config/setup_provider.rs @@ -0,0 +1,9 @@ +use {crate::config::ConfigOptions, clap::Args}; + +#[derive(Args, Clone, Debug)] +#[command(next_help_heading = "Setup Provider Options")] +#[group(id = "SetupProviderOptions")] +pub struct SetupProviderOptions { + #[command(flatten)] + pub config: ConfigOptions, +} diff --git a/apps/argus/src/config/withdraw_fees.rs b/apps/argus/src/config/withdraw_fees.rs new file mode 100644 index 0000000000..1d708c9e82 --- /dev/null +++ b/apps/argus/src/config/withdraw_fees.rs @@ -0,0 +1,28 @@ +use { + crate::{api::ChainId, config::ConfigOptions}, + clap::Args, +}; + +#[derive(Args, Clone, Debug)] +#[command(next_help_heading = "Withdraw Fees Options")] +#[group(id = "Withdraw Fees")] +pub struct WithdrawFeesOptions { + #[command(flatten)] + pub config: ConfigOptions, + + /// Withdraw the fees on this chain, or all chains if not specified. + #[arg(long = "chain-id")] + pub chain_id: Option, + + /// If provided, run the command using the keeper wallet. By default, the command uses the provider wallet. + /// If this option is provided, the keeper wallet must be configured and set as the fee manager for the provider. + #[arg(long = "keeper")] + #[arg(default_value = "false")] + pub keeper: bool, + + /// If specified, only withdraw fees over the given balance from the contract. + /// If omitted, all accrued fees are withdrawn. + #[arg(long = "retain-balance")] + #[arg(default_value = "0")] + pub retain_balance_wei: u128, +} diff --git a/apps/argus/src/keeper.rs b/apps/argus/src/keeper.rs new file mode 100644 index 0000000000..42a3520aa2 --- /dev/null +++ b/apps/argus/src/keeper.rs @@ -0,0 +1,228 @@ +use { + crate::{ + api::{BlockchainState, ChainId}, + chain::ethereum::{InstrumentedPythContract, InstrumentedSignablePythContract}, + config::EthereumConfig, + keeper::request::{ + get_latest_safe_block, process_active_requests, + }, + keeper::fee::adjust_fee_wrapper, + keeper::fee::withdraw_fees_wrapper, + keeper::track::track_balance, + keeper::track::track_provider, + }, + fortuna::eth_utils::traced_client::RpcMetrics, + ethers::{signers::Signer, types::U256}, + keeper_metrics::{AccountLabel, KeeperMetrics}, + std::{collections::HashSet, sync::Arc}, + tokio::{ + spawn, + sync::RwLock, + time::{self, Duration}, + }, + tracing::{self, Instrument}, +}; + +pub(crate) mod request; +pub(crate) mod fee; +pub(crate) mod keeper_metrics; +pub(crate) mod track; +pub(crate) mod hermes; + +/// Track metrics in this interval +const TRACK_INTERVAL: Duration = Duration::from_secs(10); +/// Check whether we need to conduct a withdrawal at this interval. +const WITHDRAW_INTERVAL: Duration = Duration::from_secs(300); +/// Check whether we need to adjust the fee at this interval. +const ADJUST_FEE_INTERVAL: Duration = Duration::from_secs(30); +/// Check for active requests at this interval +const ACTIVE_REQUESTS_INTERVAL: Duration = Duration::from_secs(2); +/// Maximum number of active requests to process in a single batch +const MAX_ACTIVE_REQUESTS: usize = 100; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RequestState { + /// Fulfilled means that the request was either revealed or we are sure we + /// will not be able to reveal it. + Fulfilled, + /// We have already processed the request but couldn't fulfill it and we are + /// unsure if we can fulfill it or not. + Processed, +} + +/// Run threads to handle active requests, periodically check for new requests, +/// and manage fees and balance. +#[tracing::instrument(name = "keeper", skip_all, fields(chain_id = chain_state.id))] +pub async fn run_keeper_threads( + private_key: String, + chain_eth_config: EthereumConfig, + chain_state: BlockchainState, + metrics: Arc, + rpc_metrics: Arc, + hermes_base_url: String, +) { + tracing::info!("starting keeper"); + let latest_safe_block = get_latest_safe_block(&chain_state).in_current_span().await; + tracing::info!("latest safe block: {}", &latest_safe_block); + + let contract = Arc::new( + InstrumentedSignablePythContract::from_config( + &chain_eth_config, + &private_key, + chain_state.id.clone(), + rpc_metrics.clone(), + ) + .await + .expect("Chain config should be valid"), + ); + let keeper_address = contract.wallet().address(); + + let fulfilled_requests_cache = Arc::new(RwLock::new(HashSet::::new())); + + // Spawn a thread to handle active requests initially + let gas_limit: U256 = chain_eth_config.gas_limit.into(); + spawn( + process_active_requests( + MAX_ACTIVE_REQUESTS, + contract.clone(), + gas_limit, + chain_eth_config.escalation_policy.to_policy(), + chain_state.clone(), + metrics.clone(), + fulfilled_requests_cache.clone(), + hermes_base_url.clone(), + ) + .in_current_span(), + ); + + // Clone values needed for the periodic request checking thread + let request_check_contract = contract.clone(); + let request_check_chain_state = chain_state.clone(); + let request_check_metrics = metrics.clone(); + let request_check_escalation_policy = chain_eth_config.escalation_policy.to_policy(); + let request_check_fulfilled_requests_cache = fulfilled_requests_cache.clone(); + let request_check_hermes_base_url = hermes_base_url.clone(); + + // Spawn a thread to periodically check for active requests + spawn( + async move { + loop { + time::sleep(ACTIVE_REQUESTS_INTERVAL).await; + + process_active_requests( + MAX_ACTIVE_REQUESTS, + request_check_contract.clone(), + gas_limit, + request_check_escalation_policy.clone(), + request_check_chain_state.clone(), + request_check_metrics.clone(), + request_check_fulfilled_requests_cache.clone(), + request_check_hermes_base_url.clone(), + ) + .in_current_span() + .await; + } + } + .in_current_span(), + ); + + // Spawn a thread that watches the keeper wallet balance and submits withdrawal transactions as needed to top-up the balance. + spawn( + withdraw_fees_wrapper( + contract.clone(), + chain_state.provider_address, + WITHDRAW_INTERVAL, + U256::from(chain_eth_config.min_keeper_balance), + ) + .in_current_span(), + ); + + // Clone values needed for the fee adjustment thread + let fee_adjust_contract = contract.clone(); + let fee_adjust_chain_state = chain_state.clone(); + let fee_adjust_metrics = metrics.clone(); + + // Spawn a thread that periodically adjusts the provider fee. + spawn( + adjust_fee_wrapper( + fee_adjust_contract, + fee_adjust_chain_state.clone(), + fee_adjust_chain_state.provider_address, + ADJUST_FEE_INTERVAL, + chain_eth_config.legacy_tx, + // NOTE: we are adjusting the fees based on the maximum configured gas for user transactions. + // However, the keeper will pad the gas limit for transactions (per the escalation policy) to ensure reliable submission. + // Consequently, fees can be adjusted such that transactions are still unprofitable. + // While we could scale up this value based on the padding, that ends up overcharging users as most transactions cost nowhere + // near the maximum gas limit. + // In the unlikely event that the keeper fees aren't sufficient, the solution to this is to configure the target + // fee percentage to be higher on that specific chain. + chain_eth_config.gas_limit, + // NOTE: unwrap() here so we panic early if someone configures these values below -100. + u64::try_from(100 + chain_eth_config.min_profit_pct) + .expect("min_profit_pct must be >= -100"), + u64::try_from(100 + chain_eth_config.target_profit_pct) + .expect("target_profit_pct must be >= -100"), + u64::try_from(100 + chain_eth_config.max_profit_pct) + .expect("max_profit_pct must be >= -100"), + chain_eth_config.fee, + fee_adjust_metrics, + ) + .in_current_span(), + ); + + // Clone values needed for the tracking thread + let track_chain_id = chain_state.id.clone(); + let track_chain_config = chain_eth_config.clone(); + let track_provider_address = chain_state.provider_address; + let track_keeper_metrics = metrics.clone(); + let track_rpc_metrics = rpc_metrics.clone(); + + // Spawn a thread to track the provider info and the balance of the keeper + spawn( + async move { + let chain_id = track_chain_id; + let chain_config = track_chain_config; + let provider_address = track_provider_address; + let keeper_metrics = track_keeper_metrics; + let contract = match InstrumentedPythContract::from_config( + &chain_config, + chain_id.clone(), + track_rpc_metrics, + ) { + Ok(r) => r, + Err(e) => { + tracing::error!("Error while connecting to pythnet contract. error: {:?}", e); + return; + } + }; + + loop { + // There isn't a loop for indefinite trials. There is a new thread being spawned every `TRACK_INTERVAL` seconds. + // If rpc start fails all of these threads will just exit, instead of retrying. + // We are tracking rpc failures elsewhere, so it's fine. + spawn( + track_provider( + chain_id.clone(), + contract.clone(), + provider_address, + keeper_metrics.clone(), + ) + .in_current_span(), + ); + spawn( + track_balance( + chain_id.clone(), + contract.client(), + keeper_address, + keeper_metrics.clone(), + ) + .in_current_span(), + ); + + time::sleep(TRACK_INTERVAL).await; + } + } + .in_current_span(), + ); +} diff --git a/apps/argus/src/keeper/fee.rs b/apps/argus/src/keeper/fee.rs new file mode 100644 index 0000000000..c5d683f35b --- /dev/null +++ b/apps/argus/src/keeper/fee.rs @@ -0,0 +1,255 @@ +use { + crate::{ + api::BlockchainState, chain::ethereum::InstrumentedSignablePythContract, + keeper::AccountLabel, keeper::ChainId, keeper::KeeperMetrics, + }, + anyhow::{anyhow, Result}, + ethers::{ + middleware::Middleware, + signers::Signer, + types::{Address, U256}, + }, + fortuna::eth_utils::utils::{estimate_tx_cost, send_and_confirm}, + std::sync::Arc, + tokio::time::{self, Duration}, + tracing::{self, Instrument}, +}; + +#[tracing::instrument(name = "withdraw_fees", skip_all, fields())] +pub async fn withdraw_fees_wrapper( + contract: Arc, + provider_address: Address, + poll_interval: Duration, + min_balance: U256, +) { + loop { + if let Err(e) = withdraw_fees_if_necessary(contract.clone(), provider_address, min_balance) + .in_current_span() + .await + { + tracing::error!("Withdrawing fees. error: {:?}", e); + } + time::sleep(poll_interval).await; + } +} + +/// Withdraws accumulated fees in the contract as needed to maintain the balance of the keeper wallet. +pub async fn withdraw_fees_if_necessary( + contract: Arc, + provider_address: Address, + min_balance: U256, +) -> Result<()> { + let provider = contract.provider(); + let wallet = contract.wallet(); + + let keeper_balance = provider + .get_balance(wallet.address(), None) + .await + .map_err(|e| anyhow!("Error while getting balance. error: {:?}", e))?; + + let provider_info = contract + .get_provider_info(provider_address) + .call() + .await + .map_err(|e| anyhow!("Error while getting provider info. error: {:?}", e))?; + + if provider_info.fee_manager != wallet.address() { + return Err(anyhow!("Fee manager for provider {:?} is not the keeper wallet. Fee manager: {:?} Keeper: {:?}", provider, provider_info.fee_manager, wallet.address())); + } + + let fees = provider_info.accrued_fees_in_wei; + + if keeper_balance < min_balance && U256::from(fees) > min_balance { + tracing::info!("Claiming accrued fees..."); + let contract_call = contract.withdraw_as_fee_manager(provider_address, fees); + send_and_confirm(contract_call).await?; + } else if keeper_balance < min_balance { + tracing::warn!("Keeper balance {:?} is too low (< {:?}) but provider fees are not sufficient to top-up.", keeper_balance, min_balance) + } + + Ok(()) +} + +#[tracing::instrument(name = "adjust_fee", skip_all)] +#[allow(clippy::too_many_arguments)] +pub async fn adjust_fee_wrapper( + contract: Arc, + chain_state: BlockchainState, + provider_address: Address, + poll_interval: Duration, + legacy_tx: bool, + gas_limit: u64, + min_profit_pct: u64, + target_profit_pct: u64, + max_profit_pct: u64, + min_fee_wei: u128, + metrics: Arc, +) { + // The maximum balance of accrued fees + provider wallet balance. None if we haven't observed a value yet. + let mut high_water_pnl: Option = None; + // Track when we last updated the fee + let mut last_fee_update_time: Option = None; + loop { + if let Err(e) = adjust_fee_if_necessary( + contract.clone(), + chain_state.id.clone(), + provider_address, + legacy_tx, + gas_limit, + min_profit_pct, + target_profit_pct, + max_profit_pct, + min_fee_wei, + &mut high_water_pnl, + &mut last_fee_update_time, + metrics.clone(), + ) + .in_current_span() + .await + { + tracing::error!("Adjusting fees. error: {:?}", e); + } + time::sleep(poll_interval).await; + } +} + +/// Adjust the fee charged by the provider to ensure that it is profitable at the prevailing gas price. +/// This method targets a fee as a function of the maximum cost of the callback, +/// c = (gas_limit) * (current gas price), with min_fee_wei as a lower bound on the fee. +/// +/// The method then updates the on-chain fee if all of the following are satisfied: +/// - the on-chain fee does not fall into an interval [c*min_profit, c*max_profit]. The tolerance +/// factor prevents the on-chain fee from changing with every single gas price fluctuation. +/// Profit scalars are specified in percentage units, min_profit = (min_profit_pct + 100) / 100 +/// - either the fee is increasing or the keeper is earning a profit -- i.e., fees only decrease when the keeper is profitable +/// - at least some time has passed since the last fee update +/// +/// These conditions are intended to make sure that the keeper is profitable while also minimizing the number of fee +/// update transactions. +#[allow(clippy::too_many_arguments)] +pub async fn adjust_fee_if_necessary( + contract: Arc, + chain_id: ChainId, + provider_address: Address, + legacy_tx: bool, + gas_limit: u64, + min_profit_pct: u64, + target_profit_pct: u64, + max_profit_pct: u64, + min_fee_wei: u128, + high_water_pnl: &mut Option, + last_fee_update_time: &mut Option, + metrics: Arc, +) -> Result<()> { + let provider_info = contract + .get_provider_info(provider_address) + .call() + .await + .map_err(|e| anyhow!("Error while getting provider info. error: {:?}", e))?; + + if provider_info.fee_manager != contract.wallet().address() { + return Err(anyhow!("Fee manager for provider {:?} is not the keeper wallet. Fee manager: {:?} Keeper: {:?}", contract.provider(), provider_info.fee_manager, contract.wallet().address())); + } + + // Calculate target window for the on-chain fee. + let middleware = contract.client(); + let max_callback_cost: u128 = estimate_tx_cost(middleware, legacy_tx, gas_limit.into()) + .await + .map_err(|e| anyhow!("Could not estimate transaction cost. error {:?}", e))?; + + let account_label = AccountLabel { + chain_id: chain_id.clone(), + address: provider_address.to_string(), + }; + + metrics + .gas_price_estimate + .get_or_create(&account_label) + .set((max_callback_cost / u128::from(gas_limit)) as f64 / 1e9); + + let target_fee_min = std::cmp::max( + (max_callback_cost * u128::from(min_profit_pct)) / 100, + min_fee_wei, + ); + let target_fee = std::cmp::max( + (max_callback_cost * u128::from(target_profit_pct)) / 100, + min_fee_wei, + ); + metrics + .target_provider_fee + .get_or_create(&account_label) + .set(((max_callback_cost * u128::from(target_profit_pct)) / 100) as f64 / 1e18); + + let target_fee_max = std::cmp::max( + (max_callback_cost * u128::from(max_profit_pct)) / 100, + min_fee_wei, + ); + + // Calculate current P&L to determine if we can reduce fees. + let current_keeper_balance = contract + .provider() + .get_balance(contract.wallet().address(), None) + .await + .map_err(|e| anyhow!("Error while getting balance. error: {:?}", e))?; + let current_keeper_fees = U256::from(provider_info.accrued_fees_in_wei); + let current_pnl = current_keeper_balance + current_keeper_fees; + + let can_reduce_fees = match high_water_pnl { + Some(x) => current_pnl >= *x, + None => false, + }; + + // Get current timestamp to determine if enough time has passed since last update + let current_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + // Determine if enough time has passed since the last fee update + // We'll use a 10-minute minimum interval between fee updates + const MIN_FEE_UPDATE_INTERVAL: u64 = 600; // 10 minutes + let is_update_time = match last_fee_update_time { + Some(last_time) => current_time.saturating_sub(*last_time) >= MIN_FEE_UPDATE_INTERVAL, + None => true, // First run, allow update + }; + + let provider_fee: u128 = provider_info.fee_in_wei; + if is_update_time + && ((provider_fee > target_fee_max && can_reduce_fees) || provider_fee < target_fee_min) + { + tracing::info!( + "Adjusting fees. Current: {:?} Target: {:?}", + provider_fee, + target_fee + ); + let contract_call = contract.set_provider_fee(target_fee); + send_and_confirm(contract_call).await?; + + *last_fee_update_time = Some(current_time); + } else { + tracing::info!( + "Skipping fee adjustment. Current: {:?} Target: {:?} [{:?}, {:?}] Last update time: {:?} Current time: {:?} Current pnl: {:?} High water pnl: {:?}", + provider_fee, + target_fee, + target_fee_min, + target_fee_max, + last_fee_update_time, + current_time, + current_pnl, + high_water_pnl + ) + } + + // Update high water pnl + *high_water_pnl = Some(std::cmp::max( + current_pnl, + high_water_pnl.unwrap_or(U256::from(0)), + )); + + // Initialize last_fee_update_time on first run + if last_fee_update_time.is_none() { + *last_fee_update_time = Some(current_time); + } + + Ok(()) +} diff --git a/apps/argus/src/keeper/hermes.rs b/apps/argus/src/keeper/hermes.rs new file mode 100644 index 0000000000..4d38d2d9f1 --- /dev/null +++ b/apps/argus/src/keeper/hermes.rs @@ -0,0 +1,413 @@ +/// Hermes Integration +/// +/// This module provides integration with the Hermes API for fetching price updates to fulfill Pulse requests. +/// +/// # Overview +/// +/// The Hermes API is used to fetch price updates for specific price IDs at a given publish time. +/// These updates are then used to execute callbacks on the Pulse contract. +/// +/// # API Endpoints +/// +/// The main endpoint used is: +/// +/// GET /v2/updates/price/{publish_time} +/// +/// This endpoint returns price updates for the specified price IDs at the given publish time. +/// +/// ## Parameters +/// +/// - `publish_time`: The timestamp for which to fetch price updates +/// - `ids`: Comma-separated list of price IDs (hex-encoded) +/// +/// ## Response +/// +/// The response contains binary data that can be passed directly to the `executeCallback` function on the Pulse contract: +/// +/// { +/// "binary": { +/// "data": ["0x..."], +/// "encoding": "hex" +/// } +/// } +/// +use { + anyhow::{anyhow, Result}, + ethers::types::Bytes, + reqwest::Client, + serde::{Deserialize, Serialize}, + std::time::Duration, + tracing, +}; + +const HERMES_TIMEOUT: Duration = Duration::from_secs(10); +const DEFAULT_HERMES_BASE_URL: &str = "https://hermes.pyth.network"; + +/// Binary data response from Hermes API +#[derive(Debug, Serialize, Deserialize)] +pub struct BinaryData { + pub data: Vec, + pub encoding: String, +} + +/// Response from Hermes API for price updates +#[derive(Debug, Serialize, Deserialize)] +pub struct HermesResponse { + pub binary: BinaryData, +} + +/// Client for interacting with the Hermes API +/// +/// This client provides a simple interface for fetching price updates from the Hermes API. +/// It handles: +/// +/// - Converting price IDs to the correct format +/// - Making HTTP requests to the Hermes API +/// - Parsing the response and converting it to the format expected by the Pulse contract +/// - Error handling and retries +pub struct HermesClient { + client: Client, + base_url: String, +} + +impl HermesClient { + /// Create a new Hermes client with default configuration + pub fn new() -> Self { + // Use the default base URL + Self::with_base_url(DEFAULT_HERMES_BASE_URL.to_string()) + } + + /// Create a new Hermes client with a custom base URL + pub fn with_base_url(base_url: String) -> Self { + let client = Client::builder() + .timeout(HERMES_TIMEOUT) + .build() + .expect("Failed to build HTTP client"); + + Self { client, base_url } + } + + /// Fetch price updates for the given price IDs at the specified publish time + /// + /// Returns the binary update data that can be passed to the executeCallback function + pub async fn get_price_updates( + &self, + publish_time: u64, + price_ids: &[[u8; 32]], + ) -> Result> { + let price_ids_hex: Vec = price_ids + .iter() + .map(|id| format!("0x{}", hex::encode(id))) + .collect(); + + let url = format!("{}/v2/updates/price/{}", self.base_url, publish_time); + tracing::debug!( + "Fetching price updates from Hermes for publish_time={} price_ids={:?}", + publish_time, + price_ids_hex + ); + + let mut query_params = Vec::new(); + for id in &price_ids_hex { + query_params.push(("ids[]", id.as_str())); + } + + let response = self.client.get(&url).query(&query_params).send().await?; + println!("Full URL: {}", response.url()); + + if !response.status().is_success() { + let status = response.status(); + let text = response.text().await?; + return Err(anyhow!( + "Failed to fetch price updates: status={}, body={}", + status, + text + )); + } + + let hermes_response: HermesResponse = response.json().await?; + + // Convert hex-encoded strings to bytes + let update_data = hermes_response + .binary + .data + .into_iter() + .map(|hex_str| { + let hex_str = hex_str.trim_start_matches("0x"); + let bytes = hex::decode(hex_str) + .map_err(|e| anyhow!("Failed to decode hex string: {}", e))?; + Ok(Bytes::from(bytes)) + }) + .collect::>>()?; + + tracing::debug!( + "Received {} update data entries from Hermes", + update_data.len() + ); + + Ok(update_data) + } +} + +impl Default for HermesClient { + fn default() -> Self { + Self::new() + } +} + +/// Fetch price updates from Hermes for the given price IDs at the specified publish time +/// +/// This function handles retries and error handling for Hermes API requests +#[tracing::instrument(skip_all, fields(publish_time))] +pub async fn fetch_price_updates_from_hermes( + publish_time: u64, + price_ids: &[[u8; 32]], + hermes_base_url: String, +) -> Result> { + const MAX_RETRIES: usize = 3; + const RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(500); + + // Create a client with the provided base URL + let hermes_client = HermesClient::with_base_url(hermes_base_url); + + let mut last_error = None; + + for retry in 0..MAX_RETRIES { + match hermes_client + .get_price_updates(publish_time, price_ids) + .await + { + Ok(update_data) => { + if update_data.is_empty() { + tracing::warn!( + "Hermes returned empty update data for publish_time={}", + publish_time + ); + return Err(anyhow!("Hermes returned empty update data")); + } + + tracing::info!( + "Successfully fetched price updates from Hermes: {} entries", + update_data.len() + ); + return Ok(update_data); + } + Err(e) => { + last_error = Some(e); + if retry < MAX_RETRIES - 1 { + tracing::warn!( + "Failed to fetch price updates from Hermes (retry {}/{}): {:?}", + retry + 1, + MAX_RETRIES, + last_error + ); + tokio::time::sleep(RETRY_DELAY).await; + } + } + } + } + + Err(anyhow!( + "Failed to fetch price updates from Hermes after {} retries: {:?}", + MAX_RETRIES, + last_error + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use mockito::{self, Matcher}; + + #[tokio::test] + async fn test_get_price_updates_success() { + // Setup mock server + let mut mock_server = mockito::Server::new_async().await; + let url = mock_server.url(); + + // Create a test price ID + let price_id = [ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, + 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, + 0x90, 0xab, 0xcd, 0xef, + ]; + + // Create a mock for the Hermes API endpoint + let mock = mock_server + .mock("GET", "/v2/updates/price/1234567890") + .match_query(Matcher::UrlEncoded( + "ids".into(), + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".into(), + )) + .with_status(200) + .with_header("content-type", "application/json") + .with_body( + r#"{ + "binary": { + "data": ["0xabcdef1234567890", "0x123456"], + "encoding": "hex" + } + }"#, + ) + .create_async() + .await; + + // Create a client with the mock server URL + let client = HermesClient::with_base_url(url); + + // Call the method + let result = client.get_price_updates(1234567890, &[price_id]).await; + + // Verify the result + assert!(result.is_ok()); + let updates = result.unwrap(); + assert_eq!(updates.len(), 2); + + // Check the first update data + let expected_data1 = hex::decode("abcdef1234567890").unwrap(); + assert_eq!(updates[0].to_vec(), expected_data1); + + // Check the second update data + let expected_data2 = hex::decode("123456").unwrap(); + assert_eq!(updates[1].to_vec(), expected_data2); + + // Verify the mock was called + mock.assert_async().await; + } + + #[tokio::test] + async fn test_get_price_updates_error_response() { + // Setup mock server + let mut mock_server = mockito::Server::new_async().await; + let url = mock_server.url(); + + // Create a test price ID + let price_id = [ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, + 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, + 0x90, 0xab, 0xcd, 0xef, + ]; + + // Create a mock for the Hermes API endpoint with an error response + let mock = mock_server + .mock("GET", "/v2/updates/price/1234567890") + .match_query(Matcher::UrlEncoded( + "ids".into(), + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".into(), + )) + .with_status(404) + .with_body("Not found") + .create_async() + .await; + + // Create a client with the mock server URL + let client = HermesClient::with_base_url(url); + + // Call the method + let result = client.get_price_updates(1234567890, &[price_id]).await; + + // Verify the result is an error + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(error.to_string().contains("Failed to fetch price updates")); + + // Verify the mock was called + mock.assert_async().await; + } + + #[tokio::test] + async fn test_get_price_updates_empty_response() { + // Setup mock server + let mut mock_server = mockito::Server::new_async().await; + let url = mock_server.url(); + + // Create a test price ID + let price_id = [ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, + 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, + 0x90, 0xab, 0xcd, 0xef, + ]; + + // Create a mock for the Hermes API endpoint with an empty data array + let mock = mock_server + .mock("GET", "/v2/updates/price/1234567890") + .match_query(Matcher::UrlEncoded( + "ids".into(), + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".into(), + )) + .with_status(200) + .with_header("content-type", "application/json") + .with_body( + r#"{ + "binary": { + "data": [], + "encoding": "hex" + } + }"#, + ) + .create_async() + .await; + + // Create a client with the mock server URL + let client = HermesClient::with_base_url(url); + + // Call the method + let result = client.get_price_updates(1234567890, &[price_id]).await; + + // Verify the result + assert!(result.is_ok()); + let updates = result.unwrap(); + assert_eq!(updates.len(), 0); + + // Verify the mock was called + mock.assert_async().await; + } + + #[tokio::test] + async fn test_get_price_updates_invalid_hex() { + // Setup mock server + let mut mock_server = mockito::Server::new_async().await; + let url = mock_server.url(); + + // Create a test price ID + let price_id = [ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, + 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, + 0x90, 0xab, 0xcd, 0xef, + ]; + + // Create a mock for the Hermes API endpoint with invalid hex data + let mock = mock_server + .mock("GET", "/v2/updates/price/1234567890") + .match_query(Matcher::UrlEncoded( + "ids".into(), + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".into(), + )) + .with_status(200) + .with_header("content-type", "application/json") + .with_body( + r#"{ + "binary": { + "data": ["0xZZZZ"], + "encoding": "hex" + } + }"#, + ) + .create_async() + .await; + + // Create a client with the mock server URL + let client = HermesClient::with_base_url(url); + + // Call the method + let result = client.get_price_updates(1234567890, &[price_id]).await; + + // Verify the result is an error about invalid hex + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(error.to_string().contains("Failed to decode hex string")); + + // Verify the mock was called + mock.assert_async().await; + } +} diff --git a/apps/argus/src/keeper/keeper_metrics.rs b/apps/argus/src/keeper/keeper_metrics.rs new file mode 100644 index 0000000000..769b6f4b67 --- /dev/null +++ b/apps/argus/src/keeper/keeper_metrics.rs @@ -0,0 +1,240 @@ +use { + ethers::types::Address, + prometheus_client::{ + encoding::EncodeLabelSet, + metrics::{counter::Counter, family::Family, gauge::Gauge, histogram::Histogram}, + registry::Registry, + }, + std::sync::atomic::AtomicU64, + std::sync::Arc, + tokio::sync::RwLock, +}; + +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] +pub struct AccountLabel { + pub chain_id: String, + pub address: String, +} + +pub struct KeeperMetrics { + pub balance: Family>, + pub collected_fee: Family>, + pub current_fee: Family>, + pub target_provider_fee: Family>, + pub total_gas_spent: Family>, + pub total_gas_fee_spent: Family>, + pub requests: Family, + pub requests_processed: Family, + pub requests_processed_success: Family, + pub requests_processed_failure: Family, + pub requests_reprocessed: Family, + pub callbacks_executed: Family, + pub request_duration_ms: Family, + pub retry_count: Family, + pub final_gas_multiplier: Family, + pub final_fee_multiplier: Family, + pub gas_price_estimate: Family>, +} + +impl Default for KeeperMetrics { + fn default() -> Self { + Self { + balance: Family::default(), + collected_fee: Family::default(), + current_fee: Family::default(), + target_provider_fee: Family::default(), + total_gas_spent: Family::default(), + total_gas_fee_spent: Family::default(), + requests: Family::default(), + requests_processed: Family::default(), + requests_processed_success: Family::default(), + requests_processed_failure: Family::default(), + requests_reprocessed: Family::default(), + callbacks_executed: Family::default(), + request_duration_ms: Family::new_with_constructor(|| { + Histogram::new( + vec![ + 1000.0, 2500.0, 5000.0, 7500.0, 10000.0, 20000.0, 30000.0, 40000.0, + 50000.0, 60000.0, 120000.0, 180000.0, 240000.0, 300000.0, 600000.0, + ] + .into_iter(), + ) + }), + retry_count: Family::new_with_constructor(|| { + Histogram::new(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 10.0, 15.0, 20.0].into_iter()) + }), + final_gas_multiplier: Family::new_with_constructor(|| { + Histogram::new( + vec![100.0, 125.0, 150.0, 200.0, 300.0, 400.0, 500.0, 600.0].into_iter(), + ) + }), + final_fee_multiplier: Family::new_with_constructor(|| { + Histogram::new(vec![100.0, 110.0, 120.0, 140.0, 160.0, 180.0, 200.0].into_iter()) + }), + gas_price_estimate: Family::default(), + } + } +} + +impl KeeperMetrics { + pub async fn new( + registry: Arc>, + chain_labels: Vec<(String, Address)>, + ) -> Self { + let mut writable_registry = registry.write().await; + let keeper_metrics = KeeperMetrics::default(); + + + writable_registry.register( + "requests", + "Number of price update requests received through events", + keeper_metrics.requests.clone(), + ); + + writable_registry.register( + "requests_processed", + "Number of price update requests processed", + keeper_metrics.requests_processed.clone(), + ); + + writable_registry.register( + "requests_processed_success", + "Number of price update requests processed successfully", + keeper_metrics.requests_processed_success.clone(), + ); + + writable_registry.register( + "requests_processed_failure", + "Number of price update requests processed with failure", + keeper_metrics.requests_processed_failure.clone(), + ); + + writable_registry.register( + "callbacks_executed", + "Number of price update callbacks executed", + keeper_metrics.callbacks_executed.clone(), + ); + + writable_registry.register( + "balance", + "Balance of the keeper", + keeper_metrics.balance.clone(), + ); + + writable_registry.register( + "collected_fee", + "Collected fee on the contract", + keeper_metrics.collected_fee.clone(), + ); + + writable_registry.register( + "current_fee", + "Current fee charged by the provider", + keeper_metrics.current_fee.clone(), + ); + + writable_registry.register( + "target_provider_fee", + "Target fee in ETH -- differs from current_fee in that this is the goal, and current_fee is the on-chain value.", + keeper_metrics.target_provider_fee.clone(), + ); + + writable_registry.register( + "total_gas_spent", + "Total gas spent executing callbacks", + keeper_metrics.total_gas_spent.clone(), + ); + + writable_registry.register( + "total_gas_fee_spent", + "Total amount of ETH spent on gas for executing callbacks", + keeper_metrics.total_gas_fee_spent.clone(), + ); + + writable_registry.register( + "requests_reprocessed", + "Number of price update requests reprocessed", + keeper_metrics.requests_reprocessed.clone(), + ); + + writable_registry.register( + "request_duration_ms", + "Time taken to process each successful callback request in milliseconds", + keeper_metrics.request_duration_ms.clone(), + ); + + writable_registry.register( + "retry_count", + "Number of retries for successful transactions", + keeper_metrics.retry_count.clone(), + ); + + writable_registry.register( + "final_gas_multiplier", + "Final gas multiplier percentage for successful transactions", + keeper_metrics.final_gas_multiplier.clone(), + ); + + writable_registry.register( + "final_fee_multiplier", + "Final fee multiplier percentage for successful transactions", + keeper_metrics.final_fee_multiplier.clone(), + ); + + writable_registry.register( + "gas_price_estimate", + "Gas price estimate for the blockchain (in gwei)", + keeper_metrics.gas_price_estimate.clone(), + ); + + // *Important*: When adding a new metric: + // 1. Register it above using `writable_registry.register(...)` + // 2. Add a get_or_create call in the loop below to initialize it for each chain/provider pair + for (chain_id, provider_address) in chain_labels { + let account_label = AccountLabel { + chain_id, + address: provider_address.to_string(), + }; + + let _ = keeper_metrics.balance.get_or_create(&account_label); + let _ = keeper_metrics.collected_fee.get_or_create(&account_label); + let _ = keeper_metrics.current_fee.get_or_create(&account_label); + let _ = keeper_metrics + .target_provider_fee + .get_or_create(&account_label); + let _ = keeper_metrics.total_gas_spent.get_or_create(&account_label); + let _ = keeper_metrics + .total_gas_fee_spent + .get_or_create(&account_label); + let _ = keeper_metrics.requests.get_or_create(&account_label); + let _ = keeper_metrics + .requests_processed + .get_or_create(&account_label); + let _ = keeper_metrics + .requests_processed_success + .get_or_create(&account_label); + let _ = keeper_metrics + .requests_processed_failure + .get_or_create(&account_label); + let _ = keeper_metrics + .requests_reprocessed + .get_or_create(&account_label); + let _ = keeper_metrics.callbacks_executed.get_or_create(&account_label); + let _ = keeper_metrics + .request_duration_ms + .get_or_create(&account_label); + let _ = keeper_metrics.retry_count.get_or_create(&account_label); + let _ = keeper_metrics + .final_gas_multiplier + .get_or_create(&account_label); + let _ = keeper_metrics + .final_fee_multiplier + .get_or_create(&account_label); + let _ = keeper_metrics + .gas_price_estimate + .get_or_create(&account_label); + } + + keeper_metrics + } +} diff --git a/apps/argus/src/keeper/request.rs b/apps/argus/src/keeper/request.rs new file mode 100644 index 0000000000..603320841d --- /dev/null +++ b/apps/argus/src/keeper/request.rs @@ -0,0 +1,262 @@ +use { + crate::{ + api::{self, BlockchainState}, + chain::{ethereum::InstrumentedSignablePythContract, reader::BlockNumber}, + keeper::hermes::fetch_price_updates_from_hermes, + keeper::keeper_metrics::{AccountLabel, KeeperMetrics}, + }, + anyhow::Result, + ethers::types::U256, + fortuna::eth_utils::utils::{submit_tx_with_backoff, EscalationPolicy}, + std::{collections::HashSet, sync::Arc}, + tokio::{ + spawn, + sync::RwLock, + time::{self, Duration}, + }, + tracing::{self, Instrument}, +}; + +/// How much to wait before retrying in case of an RPC error +const RETRY_INTERVAL: Duration = Duration::from_secs(5); + +/// Get the latest safe block number for the chain. Retry internally if there is an error. +/// This is still needed for logging and initialization purposes. +pub async fn get_latest_safe_block(chain_state: &BlockchainState) -> BlockNumber { + loop { + match chain_state.contract.get_block_number().await { + Ok(latest_confirmed_block) => { + tracing::info!("Fetched latest safe block {}", latest_confirmed_block); + return latest_confirmed_block; + } + Err(e) => { + tracing::error!("Error while getting block number. error: {:?}", e); + time::sleep(RETRY_INTERVAL).await; + } + } + } +} + +/// Process active requests fetched directly from contract storage +#[tracing::instrument(name = "process_active_requests", skip_all)] +pub async fn process_active_requests( + max_requests: usize, + contract: Arc, + gas_limit: U256, + escalation_policy: EscalationPolicy, + chain_state: api::BlockchainState, + metrics: Arc, + fulfilled_requests_cache: Arc>>, + hermes_base_url: String, +) { + tracing::info!("Processing active requests from contract storage"); + + loop { + let active_requests_res = chain_state.contract.get_active_requests(max_requests).await; + + match active_requests_res { + Ok(requests) => { + tracing::info!( + num_of_requests = &requests.len(), + "Processing active requests" + ); + + for request in &requests { + // The write lock guarantees we spawn only one task per sequence number + let newly_inserted = fulfilled_requests_cache + .write() + .await + .insert(request.sequence_number); + + if newly_inserted { + spawn( + process_request_with_backoff( + request.clone(), + chain_state.clone(), + contract.clone(), + gas_limit, + escalation_policy.clone(), + metrics.clone(), + hermes_base_url.clone(), + ) + .in_current_span(), + ); + } + } + + tracing::info!( + num_of_requests = &requests.len(), + "Processed active requests" + ); + break; + } + Err(e) => { + tracing::error!( + "Error while getting active requests. Waiting for {} seconds before retry. error: {:?}", + RETRY_INTERVAL.as_secs(), + e + ); + time::sleep(RETRY_INTERVAL).await; + } + } + } +} + +/// Process a request with backoff. It will retry the callback execution on failure for 5 minutes. +#[tracing::instrument(name = "process_request_with_backoff", skip_all, fields( + sequence_number = request.sequence_number +))] +pub async fn process_request_with_backoff( + request: crate::chain::reader::Request, + chain_state: BlockchainState, + contract: Arc, + gas_limit: U256, + escalation_policy: EscalationPolicy, + metrics: Arc, + hermes_base_url: String, +) -> Result<()> { + // We process all price update requests for our provider + let account_label = AccountLabel { + chain_id: chain_state.id.clone(), + address: chain_state.provider_address.to_string(), + }; + + metrics.requests.get_or_create(&account_label).inc(); + tracing::info!("Started processing request"); + + // Get the request details from the contract to get the publish_time and num_price_ids + let request_details = match chain_state + .contract + .get_request(request.sequence_number) + .await? + { + Some(req) => req, + None => { + tracing::warn!("Request not found on-chain, it may have been already fulfilled"); + return Ok(()); + } + }; + + // Filter price_ids to only include the first num_price_ids elements + let filtered_price_ids = if request_details.num_price_ids as usize <= request.price_ids.len() { + request.price_ids[..request_details.num_price_ids as usize].to_vec() + } else { + tracing::warn!( + "num_price_ids ({}) exceeds price_ids length ({}), using all available price_ids", + request_details.num_price_ids, + request.price_ids.len() + ); + request.price_ids.clone() + }; + + tracing::info!( + "Filtered price_ids length: {:?}, Request num_price_ids: {:?}", + filtered_price_ids.len(), + request_details.num_price_ids + ); + + // Fetch price update data from Hermes for the filtered price IDs + let update_data = fetch_price_updates_from_hermes( + request_details.publish_time.as_u64(), + &filtered_price_ids, + hermes_base_url, + ) + .await?; + tracing::info!("Fetched price update data from Hermes"); + + tracing::info!("Executing callback"); + let contract_call = contract.execute_callback( + request.sequence_number, + update_data, + filtered_price_ids, + ); + + let success = submit_tx_with_backoff( + contract.client(), + contract_call, + gas_limit, + escalation_policy, + ) + .await; + tracing::info!("Submitted callback"); + + metrics + .requests_processed + .get_or_create(&account_label) + .inc(); + + match success { + Ok(res) => { + tracing::info!("Processed request successfully in {:?}", res.duration); + + metrics + .requests_processed_success + .get_or_create(&account_label) + .inc(); + + metrics + .request_duration_ms + .get_or_create(&account_label) + .observe(res.duration.as_millis() as f64); + + // Track retry count, gas multiplier, and fee multiplier for successful transactions + metrics + .retry_count + .get_or_create(&account_label) + .observe(res.num_retries as f64); + + metrics + .final_gas_multiplier + .get_or_create(&account_label) + .observe(res.gas_multiplier as f64); + + metrics + .final_fee_multiplier + .get_or_create(&account_label) + .observe(res.fee_multiplier as f64); + + if let Ok(receipt) = res.receipt { + if let Some(gas_used) = receipt.gas_used { + let gas_used_float = gas_used.as_u128() as f64 / 1e18; + metrics + .total_gas_spent + .get_or_create(&account_label) + .inc_by(gas_used_float); + + if let Some(gas_price) = receipt.effective_gas_price { + let gas_fee = (gas_used * gas_price).as_u128() as f64 / 1e18; + metrics + .total_gas_fee_spent + .get_or_create(&account_label) + .inc_by(gas_fee); + } + } + } + metrics + .callbacks_executed + .get_or_create(&account_label) + .inc(); + } + Err(e) => { + // In case the callback did not succeed, we double-check that the request is still on-chain. + // If the request is no longer on-chain, one of the transactions we sent likely succeeded, but + // the RPC gave us an error anyway. + let req = chain_state + .contract + .get_request(request.sequence_number) + .await; + + tracing::error!("Failed to process request: {:?}. Request: {:?}", e, req); + + // We only count failures for cases where we are completely certain that the callback failed. + if req.is_ok_and(|x| x.is_some()) { + metrics + .requests_processed_failure + .get_or_create(&account_label) + .inc(); + } + } + } + + Ok(()) +} diff --git a/apps/argus/src/keeper/track.rs b/apps/argus/src/keeper/track.rs new file mode 100644 index 0000000000..6197ee225f --- /dev/null +++ b/apps/argus/src/keeper/track.rs @@ -0,0 +1,80 @@ +use { + super::keeper_metrics::{AccountLabel, KeeperMetrics}, + crate::{api::ChainId, chain::ethereum::InstrumentedPythContract}, + ethers::middleware::Middleware, + ethers::{providers::Provider, types::Address}, + fortuna::eth_utils::traced_client::TracedClient, + std::sync::Arc, + tracing, +}; + +/// tracks the balance of the given address on the given chain +/// if there was an error, the function will just return +#[tracing::instrument(skip_all)] +pub async fn track_balance( + chain_id: String, + provider: Arc>, + address: Address, + metrics: Arc, +) { + let balance = match provider.get_balance(address, None).await { + // This conversion to u128 is fine as the total balance will never cross the limits + // of u128 practically. + Ok(r) => r.as_u128(), + Err(e) => { + tracing::error!("Error while getting balance. error: {:?}", e); + return; + } + }; + // The f64 conversion is made to be able to serve metrics within the constraints of Prometheus. + // The balance is in wei, so we need to divide by 1e18 to convert it to eth. + let balance = balance as f64 / 1e18; + + metrics + .balance + .get_or_create(&AccountLabel { + chain_id: chain_id.clone(), + address: address.to_string(), + }) + .set(balance); +} + +/// tracks the collected fees and provider information on the given chain +/// if there is a error the function will just return +#[tracing::instrument(skip_all)] +pub async fn track_provider( + chain_id: ChainId, + contract: InstrumentedPythContract, + provider_address: Address, + metrics: Arc, +) { + let provider_info = match contract.get_provider_info(provider_address).call().await { + Ok(info) => info, + Err(e) => { + tracing::error!("Error while getting provider info. error: {:?}", e); + return; + } + }; + + // The f64 conversion is made to be able to serve metrics with the constraints of Prometheus. + // The fee is in wei, so we divide by 1e18 to convert it to eth. + let collected_fee = provider_info.accrued_fees_in_wei as f64 / 1e18; + let current_fee: f64 = provider_info.fee_in_wei as f64 / 1e18; + + // Track the provider's fee information + metrics + .collected_fee + .get_or_create(&AccountLabel { + chain_id: chain_id.clone(), + address: provider_address.to_string(), + }) + .set(collected_fee); + + metrics + .current_fee + .get_or_create(&AccountLabel { + chain_id: chain_id.clone(), + address: provider_address.to_string(), + }) + .set(current_fee); +} diff --git a/apps/argus/src/lib.rs b/apps/argus/src/lib.rs new file mode 100644 index 0000000000..784dac00c0 --- /dev/null +++ b/apps/argus/src/lib.rs @@ -0,0 +1,5 @@ +pub mod api; +pub mod chain; +pub mod command; +pub mod config; +pub mod keeper; diff --git a/apps/argus/src/main.rs b/apps/argus/src/main.rs new file mode 100644 index 0000000000..7e7ce6a5d0 --- /dev/null +++ b/apps/argus/src/main.rs @@ -0,0 +1,41 @@ +#![allow(clippy::just_underscores_and_digits)] + +use { + anyhow::Result, + clap::Parser, + argus::{command, config}, + std::io::IsTerminal, +}; + +// Server TODO list: +// - Tests +// - Reduce memory requirements for storing hash chains to increase scalability +// - Name things nicely (API resource names) +// - README +// - Choose data formats for binary data +#[tokio::main] +#[tracing::instrument] +async fn main() -> Result<()> { + // Initialize a Tracing Subscriber + tracing::subscriber::set_global_default( + tracing_subscriber::fmt() + .compact() + .with_file(false) + .with_line_number(true) + .with_thread_ids(true) + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .with_ansi(std::io::stderr().is_terminal()) + .finish(), + )?; + + match config::Options::parse() { + config::Options::GetRequest(opts) => command::get_request(&opts).await, + config::Options::Generate(opts) => command::generate(&opts).await, + config::Options::Run(opts) => command::run(&opts).await, + config::Options::RegisterProvider(opts) => command::register_provider(&opts).await, + config::Options::SetupProvider(opts) => command::setup_provider(&opts).await, + config::Options::RequestPriceUpdate(opts) => command::request_price_update(&opts).await, + config::Options::Inspect(opts) => command::inspect(&opts).await, + config::Options::WithdrawFees(opts) => command::withdraw_fees(&opts).await, + } +}