فهرست منبع

feat(argus): Port fortuna to Argus & delete unnecessary stuff (#2464)

* add argus as fortuna

* use fortuna eth_utils

* delete unnecessary API endpoints

* delete broken tests

* Argus init

* fix
Jayant Krishnamurthy 8 ماه پیش
والد
کامیت
eb076b9d88
41فایلهای تغییر یافته به همراه8887 افزوده شده و 0 حذف شده
  1. 4 0
      apps/argus/.gitignore
  2. 4882 0
      apps/argus/Cargo.lock
  3. 46 0
      apps/argus/Cargo.toml
  4. 18 0
      apps/argus/Dockerfile
  5. 3 0
      apps/argus/README.md
  6. 85 0
      apps/argus/config.sample.yaml
  7. 1 0
      apps/argus/rust-toolchain
  8. 164 0
      apps/argus/src/api.rs
  9. 8 0
      apps/argus/src/api/index.rs
  10. 8 0
      apps/argus/src/api/live.rs
  11. 17 0
      apps/argus/src/api/metrics.rs
  12. 10 0
      apps/argus/src/api/ready.rs
  13. 2 0
      apps/argus/src/chain.rs
  14. 284 0
      apps/argus/src/chain/ethereum.rs
  15. 184 0
      apps/argus/src/chain/reader.rs
  16. 12 0
      apps/argus/src/command.rs
  17. 24 0
      apps/argus/src/command/get_request.rs
  18. 106 0
      apps/argus/src/command/inspect.rs
  19. 94 0
      apps/argus/src/command/register_provider.rs
  20. 359 0
      apps/argus/src/command/run.rs
  21. 249 0
      apps/argus/src/command/setup_provider.rs
  22. 90 0
      apps/argus/src/command/withdraw_fees.rs
  23. 356 0
      apps/argus/src/config.rs
  24. 37 0
      apps/argus/src/config/generate.rs
  25. 29 0
      apps/argus/src/config/get_request.rs
  26. 24 0
      apps/argus/src/config/inspect.rs
  27. 17 0
      apps/argus/src/config/register_provider.rs
  28. 29 0
      apps/argus/src/config/request_randomness.rs
  29. 14 0
      apps/argus/src/config/run.rs
  30. 9 0
      apps/argus/src/config/setup_provider.rs
  31. 28 0
      apps/argus/src/config/withdraw_fees.rs
  32. 226 0
      apps/argus/src/keeper.rs
  33. 383 0
      apps/argus/src/keeper/block.rs
  34. 64 0
      apps/argus/src/keeper/commitment.rs
  35. 253 0
      apps/argus/src/keeper/fee.rs
  36. 284 0
      apps/argus/src/keeper/keeper_metrics.rs
  37. 135 0
      apps/argus/src/keeper/process_event.rs
  38. 130 0
      apps/argus/src/keeper/track.rs
  39. 6 0
      apps/argus/src/lib.rs
  40. 41 0
      apps/argus/src/main.rs
  41. 172 0
      apps/argus/src/state.rs

+ 4 - 0
apps/argus/.gitignore

@@ -0,0 +1,4 @@
+/target
+*config.yaml
+*secret*
+*private-key*

+ 4882 - 0
apps/argus/Cargo.lock

@@ -0,0 +1,4882 @@
+# 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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
+
+[[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",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628"
+dependencies = [
+ "anstyle",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
+
+[[package]]
+name = "argus"
+version = "0.1.0"
+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",
+ "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",
+]
+
+[[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 = "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.66",
+]
+
+[[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 = "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",
+ "http-body",
+ "hyper",
+ "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",
+ "http-body",
+ "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",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.66",
+]
+
+[[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",
+ "hyper",
+ "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.66",
+]
+
+[[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.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.66",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
+
+[[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 = "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",
+ "syn 2.0.66",
+]
+
+[[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.66",
+]
+
+[[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",
+]
+
+[[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",
+]
+
+[[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.66",
+ "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.66",
+]
+
+[[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.66",
+ "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",
+ "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.7"
+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.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
+
+[[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.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.66",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
+
+[[package]]
+name = "futures-task"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
+
+[[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.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
+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",
+ "indexmap 1.9.3",
+ "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.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12"
+
+[[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 = "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",
+]
+
+[[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-body"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
+dependencies = [
+ "bytes",
+ "http",
+ "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",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2 0.4.9",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97"
+dependencies = [
+ "futures-util",
+ "http",
+ "hyper",
+ "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",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[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.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.1",
+ "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",
+]
+
+[[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.148"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
+
+[[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 = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys",
+]
+
+[[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.66",
+]
+
+[[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.66",
+]
+
+[[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.0.2",
+]
+
+[[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.66",
+]
+
+[[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.66",
+]
+
+[[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.66",
+]
+
+[[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.66",
+]
+
+[[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",
+ "http",
+ "http-body",
+ "hyper",
+ "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.66",
+ "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",
+]
+
+[[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",
+]
+
+[[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.66",
+]
+
+[[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.188"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.188"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.66",
+]
+
+[[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.66",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
+dependencies = [
+ "itoa",
+ "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.0.2",
+ "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.66",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574"
+dependencies = [
+ "indexmap 2.0.2",
+ "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 = "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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[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 = "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",
+ "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",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 2.0.66",
+]
+
+[[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.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
+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",
+]
+
+[[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.66",
+]
+
+[[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.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "num_cpus",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2 0.5.4",
+ "tokio-macros",
+ "windows-sys",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.66",
+]
+
+[[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.0.2",
+ "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.0.2",
+ "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",
+ "http-body",
+ "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.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if",
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.66",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
+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.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
+dependencies = [
+ "lazy_static",
+ "log",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
+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",
+ "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.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa"
+
+[[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.0.2",
+ "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.66",
+]
+
+[[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.66",
+ "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.66",
+ "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-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",
+]
+
+[[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",
+]

+ 46 - 0
apps/argus/Cargo.toml

@@ -0,0 +1,46 @@
+[package]
+name = "argus"
+version = "0.1.0"
+edition = "2021"
+
+[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"] }
+fortuna = { path = "../fortuna" }
+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"] }
+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"
+
+
+[dev-dependencies]
+axum-test = "13.1.1"

+ 18 - 0
apps/argus/Dockerfile

@@ -0,0 +1,18 @@
+ARG RUST_VERSION=1.82.0
+
+FROM rust:${RUST_VERSION} AS build
+
+# Build
+WORKDIR /src
+COPY apps/fortuna apps/fortuna
+COPY pythnet pythnet
+COPY target_chains/ethereum/entropy_sdk/solidity/abis target_chains/ethereum/entropy_sdk/solidity/abis
+
+WORKDIR /src/apps/fortuna
+
+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/fortuna/target/release/fortuna /usr/local/bin/

+ 3 - 0
apps/argus/README.md

@@ -0,0 +1,3 @@
+# Argus
+
+TODO

+ 85 - 0
apps/argus/config.sample.yaml

@@ -0,0 +1,85 @@
+chains:
+  lightlink_pegasus:
+    geth_rpc_addr: https://replicator.pegasus.lightlink.io/rpc/v1
+    contract_addr: 0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a
+
+    # Keeper configuration for the chain
+    reveal_delay_blocks: 0
+    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]
+
+    # Historical commitments -- delete this block for local development purposes
+    commitments:
+      # prettier-ignore
+      - seed: [219,125,217,197,234,88,208,120,21,181,172,143,239,102,41,233,167,212,237,106,37,255,184,165,238,121,230,155,116,158,173,48]
+        chain_length: 10000
+        original_commitment_sequence_number: 104
+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
+  # A 32 byte random value in hexadecimal
+  # Generate with `openssl rand -hex 32`
+  secret:
+    # For local development, you can hardcode the value here
+    value: abcd
+    # For production, you can store the private key in a file.
+    # file: secret.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

+ 1 - 0
apps/argus/rust-toolchain

@@ -0,0 +1 @@
+1.82.0

+ 164 - 0
apps/argus/src/api.rs

@@ -0,0 +1,164 @@
+use {
+    crate::{
+        chain::reader::{BlockNumber, BlockStatus, EntropyReader},
+        state::HashChainState,
+    },
+    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 {index::*, live::*, metrics::*, ready::*};
+
+mod index;
+mod live;
+mod metrics;
+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<RequestLabel, Counter>,
+}
+
+#[derive(Clone)]
+pub struct ApiState {
+    pub chains: Arc<HashMap<ChainId, BlockchainState>>,
+
+    pub metrics_registry: Arc<RwLock<Registry>>,
+
+    /// Prometheus metrics
+    pub metrics: Arc<ApiMetrics>,
+}
+
+impl ApiState {
+    pub async fn new(
+        chains: HashMap<ChainId, BlockchainState>,
+        metrics_registry: Arc<RwLock<Registry>>,
+    ) -> 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 randomness service for a single blockchain.
+#[derive(Clone)]
+pub struct BlockchainState {
+    /// The chain id for this blockchain, useful for logging
+    pub id: ChainId,
+    /// The hash chain(s) required to serve random numbers for this blockchain
+    pub state: Arc<HashChainState>,
+    /// The contract that the server is fulfilling requests for.
+    pub contract: Arc<dyn EntropyReader>,
+    /// The address of the provider that this server is operating for.
+    pub provider_address: Address,
+    /// The server will wait for this many block confirmations of a request before revealing
+    /// the random number.
+    pub reveal_delay_blocks: BlockNumber,
+    /// The BlockStatus of the block that is considered to be confirmed on the blockchain.
+    /// For eg., Finalized, Safe
+    pub confirmed_block_status: BlockStatus,
+}
+
+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 a random value that can't currently be revealed (because it
+    /// hasn't been committed to on-chain)
+    NoPendingRequest,
+    /// The request exists, but the server is waiting for more confirmations (more blocks
+    /// to be mined) before revealing the random number.
+    PendingConfirmation,
+    /// The server cannot currently communicate with the blockchain, so is not able to verify
+    /// which random values 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 random value has already been revealed on chain.",
+            ).into_response(),
+            RestError::PendingConfirmation => (
+                StatusCode::FORBIDDEN,
+                "The request needs additional confirmations before the random value can be retrieved. Try your request again later.",
+            )
+                .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))
+        .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<String> {
+    let base_uri = Url::parse(base_uri)?;
+    let path = format!("/v1/chains/{}", chain_id);
+    let uri = base_uri.join(&path)?;
+    Ok(uri.to_string())
+}

+ 8 - 0
apps/argus/src/api/index.rs

@@ -0,0 +1,8 @@
+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::<[&str; 0]>([])
+}

+ 8 - 0
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()
+}

+ 17 - 0
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<crate::api::ApiState>) -> 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, &registry).unwrap();
+
+    buffer
+}

+ 10 - 0
apps/argus/src/api/ready.rs

@@ -0,0 +1,10 @@
+use axum::{
+    http::StatusCode,
+    response::{IntoResponse, Response},
+};
+
+pub async fn ready() -> Response {
+    // TODO: are there useful checks here? At the moment, everything important (specifically hash
+    // chain computation) occurs synchronously on startup.
+    (StatusCode::OK, "OK").into_response()
+}

+ 2 - 0
apps/argus/src/chain.rs

@@ -0,0 +1,2 @@
+pub mod ethereum;
+pub mod reader;

+ 284 - 0
apps/argus/src/chain/ethereum.rs

@@ -0,0 +1,284 @@
+use {
+    crate::{
+        api::ChainId,
+        chain::reader::{
+            self, BlockNumber, BlockStatus, EntropyReader, RequestedWithCallbackEvent,
+        },
+        config::EthereumConfig,
+    },
+    fortuna::eth_utils::{
+        eth_gas_oracle::EthProviderOracle,
+        legacy_tx_middleware::LegacyTxMiddleware,
+        nonce_manager::NonceManagerMiddleware,
+        traced_client::{RpcMetrics, TracedClient},
+    },
+    anyhow::{anyhow, Error, 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::{BlockNumber as EthersBlockNumber, U256},
+    },
+    sha3::{Digest, Keccak256},
+    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!(
+    PythRandom,
+    "../../target_chains/ethereum/entropy_sdk/solidity/abis/IEntropy.json"
+);
+
+pub type MiddlewaresWrapper<T> = LegacyTxMiddleware<
+    GasOracleMiddleware<
+        NonceManagerMiddleware<SignerMiddleware<Provider<T>, LocalWallet>>,
+        EthProviderOracle<Provider<T>>,
+    >,
+>;
+
+pub type SignablePythContractInner<T> = PythRandom<MiddlewaresWrapper<T>>;
+pub type SignablePythContract = SignablePythContractInner<Http>;
+pub type InstrumentedSignablePythContract = SignablePythContractInner<TracedClient>;
+
+pub type PythContract = PythRandom<Provider<Http>>;
+pub type InstrumentedPythContract = PythRandom<Provider<TracedClient>>;
+
+impl<T: JsonRpcClient + 'static + Clone> SignablePythContractInner<T> {
+    /// 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<T> {
+        self.client().inner().inner().inner().provider().clone()
+    }
+
+    /// Submit a request for a random number to the contract.
+    ///
+    /// This method is a version of the autogenned `request` method that parses the emitted logs
+    /// to return the sequence number of the created Request.
+    pub async fn request_wrapper(
+        &self,
+        provider: &Address,
+        user_randomness: &[u8; 32],
+        use_blockhash: bool,
+    ) -> Result<u64> {
+        let fee = self.get_fee(*provider).call().await?;
+
+        let hashed_randomness: [u8; 32] = Keccak256::digest(user_randomness).into();
+
+        if let Some(r) = self
+            .request(*provider, hashed_randomness, use_blockhash)
+            .value(fee)
+            .send()
+            .await?
+            .await?
+        {
+            // Extract Log from TransactionReceipt.
+            let l: RawLog = r.logs[0].clone().into();
+            if let PythRandomEvents::RequestedFilter(r) = PythRandomEvents::decode_log(&l)? {
+                Ok(r.request.sequence_number)
+            } else {
+                Err(anyhow!("No log with sequence number"))
+            }
+        } else {
+            Err(anyhow!("Request failed"))
+        }
+    }
+
+    /// Reveal the generated random number to the contract.
+    ///
+    /// This method is a version of the autogenned `reveal` method that parses the emitted logs
+    /// to return the generated random number.
+    pub async fn reveal_wrapper(
+        &self,
+        provider: &Address,
+        sequence_number: u64,
+        user_randomness: &[u8; 32],
+        provider_randomness: &[u8; 32],
+    ) -> Result<[u8; 32]> {
+        if let Some(r) = self
+            .reveal(
+                *provider,
+                sequence_number,
+                *user_randomness,
+                *provider_randomness,
+            )
+            .send()
+            .await?
+            .await?
+        {
+            if let PythRandomEvents::RevealedFilter(r) =
+                PythRandomEvents::decode_log(&r.logs[0].clone().into())?
+            {
+                Ok(r.random_number)
+            } else {
+                Err(anyhow!("No log with randomnumber"))
+            }
+        } else {
+            Err(anyhow!("Request failed"))
+        }
+    }
+
+    pub async fn from_config_and_provider(
+        chain_config: &EthereumConfig,
+        private_key: &str,
+        provider: Provider<T>,
+    ) -> Result<SignablePythContractInner<T>> {
+        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::<LocalWallet>()?
+            .with_chain_id(chain_id.as_u64());
+
+        let address = wallet__.address();
+
+        Ok(PythRandom::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<Self> {
+        let provider = Provider::<Http>::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<RpcMetrics>,
+    ) -> Result<Self> {
+        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<Self> {
+        let provider = Provider::<Http>::try_from(&chain_config.geth_rpc_addr)?;
+
+        Ok(PythRandom::new(
+            chain_config.contract_addr,
+            Arc::new(provider),
+        ))
+    }
+}
+
+impl InstrumentedPythContract {
+    pub fn from_config(
+        chain_config: &EthereumConfig,
+        chain_id: ChainId,
+        metrics: Arc<RpcMetrics>,
+    ) -> Result<Self> {
+        let provider = TracedClient::new(chain_id, &chain_config.geth_rpc_addr, metrics)?;
+
+        Ok(PythRandom::new(
+            chain_config.contract_addr,
+            Arc::new(provider),
+        ))
+    }
+}
+
+#[async_trait]
+impl<T: JsonRpcClient + 'static> EntropyReader for PythRandom<Provider<T>> {
+    async fn get_request(
+        &self,
+        provider_address: Address,
+        sequence_number: u64,
+    ) -> Result<Option<reader::Request>> {
+        let r = self
+            .get_request(provider_address, 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 {
+                provider: r.provider,
+                sequence_number: r.sequence_number,
+                block_number: r.block_number,
+                use_blockhash: r.use_blockhash,
+            }))
+        } else {
+            Ok(None)
+        }
+    }
+
+    async fn get_block_number(&self, confirmed_block_status: BlockStatus) -> Result<BlockNumber> {
+        let block_number: EthersBlockNumber = confirmed_block_status.into();
+        let block = self
+            .client()
+            .get_block(block_number)
+            .await?
+            .ok_or_else(|| Error::msg("pending block confirmation"))?;
+
+        Ok(block
+            .number
+            .ok_or_else(|| Error::msg("pending confirmation"))?
+            .as_u64())
+    }
+
+    async fn get_request_with_callback_events(
+        &self,
+        from_block: BlockNumber,
+        to_block: BlockNumber,
+    ) -> Result<Vec<RequestedWithCallbackEvent>> {
+        let mut event = self.requested_with_callback_filter();
+        event.filter = event.filter.from_block(from_block).to_block(to_block);
+
+        let res: Vec<RequestedWithCallbackFilter> = event.query().await?;
+
+        Ok(res
+            .iter()
+            .map(|r| RequestedWithCallbackEvent {
+                sequence_number: r.sequence_number,
+                user_random_number: r.user_random_number,
+                provider_address: r.request.provider,
+            })
+            .collect())
+    }
+
+    async fn estimate_reveal_with_callback_gas(
+        &self,
+        sender: Address,
+        provider: Address,
+        sequence_number: u64,
+        user_random_number: [u8; 32],
+        provider_revelation: [u8; 32],
+    ) -> Result<U256> {
+        let result = self
+            .reveal_with_callback(
+                provider,
+                sequence_number,
+                user_random_number,
+                provider_revelation,
+            )
+            .from(sender)
+            .estimate_gas()
+            .await;
+
+        result.map_err(|e| e.into())
+    }
+}

+ 184 - 0
apps/argus/src/chain/reader.rs

@@ -0,0 +1,184 @@
+use {
+    anyhow::Result,
+    axum::async_trait,
+    ethers::types::{Address, BlockNumber as EthersBlockNumber, U256},
+};
+
+pub type BlockNumber = u64;
+
+#[derive(
+    Copy, Clone, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
+)]
+pub enum BlockStatus {
+    /// Latest block
+    #[default]
+    Latest,
+    /// Finalized block accepted as canonical
+    Finalized,
+    /// Safe head block
+    Safe,
+}
+
+impl From<BlockStatus> for EthersBlockNumber {
+    fn from(val: BlockStatus) -> Self {
+        match val {
+            BlockStatus::Latest => EthersBlockNumber::Latest,
+            BlockStatus::Finalized => EthersBlockNumber::Finalized,
+            BlockStatus::Safe => EthersBlockNumber::Safe,
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct RequestedWithCallbackEvent {
+    pub sequence_number: u64,
+    pub user_random_number: [u8; 32],
+    pub provider_address: Address,
+}
+
+/// EntropyReader is the read-only interface of the Entropy contract.
+#[async_trait]
+pub trait EntropyReader: Send + Sync {
+    /// Get an in-flight request (if it exists)
+    /// Note that if we support additional blockchains in the future, the type of `provider` may
+    /// need to become more generic.
+    async fn get_request(&self, provider: Address, sequence_number: u64)
+        -> Result<Option<Request>>;
+
+    async fn get_block_number(&self, confirmed_block_status: BlockStatus) -> Result<BlockNumber>;
+
+    async fn get_request_with_callback_events(
+        &self,
+        from_block: BlockNumber,
+        to_block: BlockNumber,
+    ) -> Result<Vec<RequestedWithCallbackEvent>>;
+
+    /// Estimate the gas required to reveal a random number with a callback.
+    async fn estimate_reveal_with_callback_gas(
+        &self,
+        sender: Address,
+        provider: Address,
+        sequence_number: u64,
+        user_random_number: [u8; 32],
+        provider_revelation: [u8; 32],
+    ) -> Result<U256>;
+}
+
+/// 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 fortuna anywhere. Feel free to add any missing fields as necessary.)
+#[derive(Clone, Debug)]
+pub struct Request {
+    pub provider: Address,
+    pub sequence_number: u64,
+    // The block number where this request was created
+    pub block_number: BlockNumber,
+    pub use_blockhash: bool,
+}
+
+#[cfg(test)]
+pub mod mock {
+    use {
+        crate::chain::reader::{BlockNumber, BlockStatus, EntropyReader, Request},
+        anyhow::Result,
+        axum::async_trait,
+        ethers::types::{Address, U256},
+        std::sync::RwLock,
+    };
+
+    /// Mock version of the entropy 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 MockEntropyReader {
+        block_number: RwLock<BlockNumber>,
+        /// The set of requests that are currently in-flight.
+        requests: RwLock<Vec<Request>>,
+    }
+
+    impl MockEntropyReader {
+        pub fn with_requests(
+            block_number: BlockNumber,
+            requests: &[(Address, u64, BlockNumber, bool)],
+        ) -> MockEntropyReader {
+            MockEntropyReader {
+                block_number: RwLock::new(block_number),
+                requests: RwLock::new(
+                    requests
+                        .iter()
+                        .map(|&(a, s, b, u)| Request {
+                            provider: a,
+                            sequence_number: s,
+                            block_number: b,
+                            use_blockhash: u,
+                        })
+                        .collect(),
+                ),
+            }
+        }
+
+        /// Insert a new request into the set of in-flight requests.
+        pub fn insert(
+            &self,
+            provider: Address,
+            sequence: u64,
+            block_number: BlockNumber,
+            use_blockhash: bool,
+        ) -> &Self {
+            self.requests.write().unwrap().push(Request {
+                provider,
+                sequence_number: sequence,
+                block_number,
+                use_blockhash,
+            });
+            self
+        }
+
+        pub fn set_block_number(&self, block_number: BlockNumber) -> &Self {
+            *(self.block_number.write().unwrap()) = block_number;
+            self
+        }
+    }
+
+    #[async_trait]
+    impl EntropyReader for MockEntropyReader {
+        async fn get_request(
+            &self,
+            provider: Address,
+            sequence_number: u64,
+        ) -> Result<Option<Request>> {
+            Ok(self
+                .requests
+                .read()
+                .unwrap()
+                .iter()
+                .find(|&r| r.sequence_number == sequence_number && r.provider == provider)
+                .map(|r| (*r).clone()))
+        }
+
+        async fn get_block_number(
+            &self,
+            _confirmed_block_status: BlockStatus,
+        ) -> Result<BlockNumber> {
+            Ok(*self.block_number.read().unwrap())
+        }
+
+        async fn get_request_with_callback_events(
+            &self,
+            _from_block: BlockNumber,
+            _to_block: BlockNumber,
+        ) -> Result<Vec<super::RequestedWithCallbackEvent>> {
+            Ok(vec![])
+        }
+
+        async fn estimate_reveal_with_callback_gas(
+            &self,
+            _sender: Address,
+            _provider: Address,
+            _sequence_number: u64,
+            _user_random_number: [u8; 32],
+            _provider_revelation: [u8; 32],
+        ) -> Result<U256> {
+            Ok(U256::from(5))
+        }
+    }
+}

+ 12 - 0
apps/argus/src/command.rs

@@ -0,0 +1,12 @@
+mod get_request;
+mod inspect;
+mod register_provider;
+mod run;
+mod setup_provider;
+mod withdraw_fees;
+
+pub use {
+    get_request::get_request, inspect::inspect,
+    register_provider::register_provider, run::run,
+    setup_provider::setup_provider, withdraw_fees::withdraw_fees,
+};

+ 24 - 0
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.provider, opts.sequence)
+        .call()
+        .await?;
+    tracing::info!("Found request: {:?}", r);
+
+    Ok(())
+}

+ 106 - 0
apps/argus/src/command/inspect.rs

@@ -0,0 +1,106 @@
+use {
+    crate::{
+        chain::ethereum::{PythContract, Request},
+        config::{Config, EthereumConfig, InspectOptions},
+    },
+    anyhow::Result,
+    ethers::{
+        contract::Multicall,
+        middleware::Middleware,
+        prelude::{Http, Provider},
+    },
+};
+
+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::<Http>::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)?;
+    let entropy_provider = contract.get_default_provider().call().await?;
+    let provider_info = contract.get_provider_info(entropy_provider).call().await?;
+    let mut current_request_number = provider_info.sequence_number;
+    println!("Initial request number: {}", current_request_number);
+    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(entropy_provider, current_request_number),
+                    false,
+                );
+                current_request_number -= 1;
+            }
+            let return_data: Vec<Request> = multicall.call_array().await?;
+            for request in return_data {
+                process_request(rpc_provider.clone(), 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(entropy_provider, current_request_number)
+                .call()
+                .await?;
+            process_request(rpc_provider.clone(), request).await?;
+            current_request_number -= 1;
+            if current_request_number % 100 == 0 {
+                println!("Current request number: {}", current_request_number);
+            }
+        }
+    }
+    Ok(())
+}
+
+async fn process_request(rpc_provider: Provider<Http>, request: Request) -> Result<()> {
+    if request.sequence_number != 0 && request.is_request_with_callback {
+        let block = rpc_provider
+            .get_block(request.block_number)
+            .await?
+            .expect("Block not found");
+        let datetime = chrono::DateTime::from_timestamp(block.timestamp.as_u64() as i64, 0)
+            .expect("Invalid timestamp");
+        println!(
+            "{} sequence_number:{} block_number:{} requester:{}",
+            datetime, request.sequence_number, request.block_number, request.requester
+        );
+    }
+    Ok(())
+}

+ 94 - 0
apps/argus/src/command/register_provider.rs

@@ -0,0 +1,94 @@
+use {
+    crate::{
+        api::{get_register_uri, ChainId},
+        chain::ethereum::SignablePythContract,
+        config::{Config, EthereumConfig, ProviderConfig, RegisterProviderOptions},
+        state::PebbleHashChain,
+    },
+    anyhow::{anyhow, Result},
+    ethers::{
+        abi::Bytes,
+        signers::{LocalWallet, Signer},
+        types::U256,
+    },
+    std::sync::Arc,
+};
+
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
+pub struct CommitmentMetadata {
+    pub seed: [u8; 32],
+    pub chain_length: u64,
+}
+
+/// Register as a randomness provider. This method will generate and commit to a new random
+/// hash chain from the configured secret & a newly generated random value.
+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, &opts.chain_id, &chain_config).await?;
+
+    Ok(())
+}
+
+pub async fn register_provider_from_config(
+    provider_config: &ProviderConfig,
+    chain_id: &ChainId,
+    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?);
+    // Create a new random hash chain.
+    let random = rand::random::<[u8; 32]>();
+    let secret = provider_config
+        .secret
+        .load()?
+        .ok_or(anyhow!("Please specify a provider secret in the config"))?;
+
+    let commitment_length = provider_config.chain_length;
+    tracing::info!("Generating hash chain");
+    let chain = PebbleHashChain::from_config(
+        &secret,
+        chain_id,
+        &private_key_string.parse::<LocalWallet>()?.address(),
+        &chain_config.contract_addr,
+        &random,
+        commitment_length,
+        provider_config.chain_sample_interval,
+    )?;
+    tracing::info!("Done generating hash chain");
+
+    // Arguments to the contract to register our new provider.
+    let fee_in_wei = chain_config.fee;
+    let commitment = chain.reveal_ith(0)?;
+    // Store the random seed and chain length in the metadata field so that we can regenerate the hash
+    // chain at-will. (This is secure because you can't generate the chain unless you also have the secret)
+    let commitment_metadata = CommitmentMetadata {
+        seed: random,
+        chain_length: commitment_length,
+    };
+    let uri = get_register_uri(&provider_config.uri, chain_id)?;
+    let call = contract.register(
+        fee_in_wei,
+        commitment,
+        bincode::serialize(&commitment_metadata)?.into(),
+        commitment_length,
+        // Use Bytes to serialize the uri. Most users will be using JS/TS to deserialize this uri.
+        // Bincode is a different encoding mechanisms, and I didn't find any JS/TS library to parse bincode.
+        Bytes::from(uri.as_str()).into(),
+    );
+    let mut gas_estimate = call.estimate_gas().await?;
+    let gas_multiplier = U256::from(2); //TODO: smarter gas estimation
+    gas_estimate *= gas_multiplier;
+    let call_with_gas = call.gas(gas_estimate);
+    if let Some(r) = call_with_gas.send().await?.await? {
+        tracing::info!("Registered provider: {:?}", r);
+    }
+
+    Ok(())
+}

+ 359 - 0
apps/argus/src/command/run.rs

@@ -0,0 +1,359 @@
+use {
+    crate::{
+        api::{self, BlockchainState, ChainId},
+        chain::ethereum::InstrumentedPythContract,
+        command::register_provider::CommitmentMetadata,
+        config::{Commitment, Config, EthereumConfig, RunOptions},
+        keeper::{self, keeper_metrics::KeeperMetrics},
+        state::{HashChainState, PebbleHashChain},
+    },
+    fortuna::eth_utils::traced_client::{RpcMetrics, TracedClient},
+    anyhow::{anyhow, Error, Result},
+    axum::Router,
+    ethers::{
+        middleware::Middleware,
+        types::{Address, BlockNumber},
+    },
+    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,
+};
+
+/// Track metrics in this interval
+const TRACK_INTERVAL: Duration = Duration::from_secs(10);
+
+pub async fn run_api(
+    socket_addr: SocketAddr,
+    chains: HashMap<String, api::BlockchainState>,
+    metrics_registry: Arc<RwLock<Registry>>,
+    mut rx_exit: watch::Receiver<bool>,
+) -> Result<()> {
+    let api_state = api::ApiState::new(chains, metrics_registry).await;
+
+    // Initialize Axum Router. Note the type here is a `Router<State>` 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(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<String, api::BlockchainState>,
+    config: Config,
+    private_key: String,
+    metrics_registry: Arc<RwLock<Registry>>,
+    rpc_metrics: Arc<RpcMetrics>,
+) -> Result<()> {
+    let mut handles = Vec::new();
+    let keeper_metrics: Arc<KeeperMetrics> = 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(),
+        )));
+    }
+
+    Ok(())
+}
+
+pub async fn run(opts: &RunOptions) -> Result<()> {
+    let config = Config::load(&opts.config.config)?;
+    let secret = config.provider.secret.load()?.ok_or(anyhow!(
+        "Please specify a provider secret in the config file."
+    ))?;
+    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);
+
+    let mut tasks = Vec::new();
+    for (chain_id, chain_config) in config.chains.clone() {
+        let secret_copy = secret.clone();
+        let rpc_metrics = rpc_metrics.clone();
+        tasks.push(spawn(async move {
+            let state = setup_chain_state(
+                &config.provider.address,
+                &secret_copy,
+                config.provider.chain_sample_interval,
+                &chain_id,
+                &chain_config,
+                rpc_metrics,
+            )
+            .await;
+
+            (chain_id, state)
+        }));
+    }
+    let states = join_all(tasks).await;
+
+    let mut chains: HashMap<ChainId, BlockchainState> = 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(),
+        ));
+    } 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,
+    secret: &str,
+    chain_sample_interval: u64,
+    chain_id: &ChainId,
+    chain_config: &EthereumConfig,
+    rpc_metrics: Arc<RpcMetrics>,
+) -> Result<BlockchainState> {
+    let contract = Arc::new(InstrumentedPythContract::from_config(
+        chain_config,
+        chain_id.clone(),
+        rpc_metrics,
+    )?);
+    let mut provider_commitments = chain_config.commitments.clone().unwrap_or_default();
+    provider_commitments.sort_by(|c1, c2| {
+        c1.original_commitment_sequence_number
+            .cmp(&c2.original_commitment_sequence_number)
+    });
+
+    let provider_info = contract.get_provider_info(*provider).call().await?;
+    let latest_metadata = bincode::deserialize::<CommitmentMetadata>(
+        &provider_info.commitment_metadata,
+    )
+    .map_err(|e| {
+        anyhow!(
+            "Chain: {} - Failed to deserialize commitment metadata: {}",
+            &chain_id,
+            e
+        )
+    })?;
+
+    let last_prior_commitment = provider_commitments.last();
+    if last_prior_commitment.is_some()
+        && last_prior_commitment
+            .unwrap()
+            .original_commitment_sequence_number
+            >= provider_info.original_commitment_sequence_number
+    {
+        return Err(anyhow!("The current hash chain for chain id {} has configured commitments for sequence numbers greater than the current on-chain sequence number. Are the commitments configured correctly?", &chain_id));
+    }
+
+    provider_commitments.push(Commitment {
+        seed: latest_metadata.seed,
+        chain_length: latest_metadata.chain_length,
+        original_commitment_sequence_number: provider_info.original_commitment_sequence_number,
+    });
+
+    // TODO: we may want to load the hash chain in a lazy/fault-tolerant way. If there are many blockchains,
+    // then it's more likely that some RPC fails. We should tolerate these faults and generate the hash chain
+    // later when a user request comes in for that chain.
+
+    let mut offsets = Vec::<usize>::new();
+    let mut hash_chains = Vec::<PebbleHashChain>::new();
+
+    for commitment in &provider_commitments {
+        let offset = commitment.original_commitment_sequence_number.try_into()?;
+        offsets.push(offset);
+
+        let pebble_hash_chain = PebbleHashChain::from_config(
+            secret,
+            chain_id,
+            provider,
+            &chain_config.contract_addr,
+            &commitment.seed,
+            commitment.chain_length,
+            chain_sample_interval,
+        )
+        .map_err(|e| anyhow!("Failed to create hash chain: {}", e))?;
+        hash_chains.push(pebble_hash_chain);
+    }
+
+    let chain_state = HashChainState {
+        offsets,
+        hash_chains,
+    };
+
+    if chain_state.reveal(provider_info.original_commitment_sequence_number)?
+        != provider_info.original_commitment
+    {
+        return Err(anyhow!("The root of the generated hash chain for chain id {} does not match the commitment. Are the secret and chain length configured correctly?", &chain_id));
+    } else {
+        tracing::info!("Root of chain id {} matches commitment", &chain_id);
+    }
+
+    let state = BlockchainState {
+        id: chain_id.clone(),
+        state: Arc::new(chain_state),
+        contract,
+        provider_address: *provider,
+        reveal_delay_blocks: chain_config.reveal_delay_blocks,
+        confirmed_block_status: chain_config.confirmed_block_status,
+    };
+    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<ChainLabel, Gauge>,
+    rpc_metrics: Arc<RpcMetrics>,
+) {
+    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<RwLock<Registry>>,
+    rpc_metrics: Arc<RpcMetrics>,
+) {
+    let metrics = Family::<ChainLabel, Gauge>::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;
+    }
+}

+ 249 - 0
apps/argus/src/command/setup_provider.rs

@@ -0,0 +1,249 @@
+use {
+    crate::{
+        api::{get_register_uri, ChainId},
+        chain::ethereum::{ProviderInfo, SignablePythContract},
+        command::register_provider::{register_provider_from_config, CommitmentMetadata},
+        config::{Config, EthereumConfig, SetupProviderOptions},
+        state::{HashChainState, PebbleHashChain},
+    },
+    anyhow::{anyhow, Result},
+    ethers::{
+        abi::Bytes as AbiBytes,
+        signers::{LocalWallet, Signer},
+        types::{Address, Bytes},
+    },
+    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::<Vec<_>>();
+    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. Re-register if there are no more random numbers to request on the contract.
+/// 3. Re-register if there is a mismatch in generated hash chain.
+/// 4. Update provider fee if there is a mismatch with the fee set on contract.
+/// 5. Update provider uri if there is a mismatch with the uri 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::<LocalWallet>()?.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);
+
+    let mut register = false;
+
+    // This condition satisfies for both when there is no registration and when there are no
+    // more random numbers left to request
+    if provider_info.end_sequence_number <= provider_info.sequence_number {
+        tracing::info!(
+            "endSequenceNumber <= sequenceNumber. endSequenceNumber={}, sequenceNumber={}",
+            provider_info.end_sequence_number,
+            provider_info.sequence_number
+        );
+        register = true;
+    } else {
+        let metadata =
+            bincode::deserialize::<CommitmentMetadata>(&provider_info.commitment_metadata)
+                .map_err(|e| {
+                    anyhow!(
+                        "Chain: {} - Failed to deserialize commitment metadata: {}",
+                        &chain_id,
+                        e
+                    )
+                })?;
+
+        let secret = provider_config.secret.load()?.ok_or(anyhow!(
+            "Please specify a provider secret in the config file."
+        ))?;
+        if metadata.chain_length != provider_config.chain_length {
+            tracing::info!(
+                "Chain length mismatch. metadata.chain_length={}, provider_config.chain_length={}",
+                metadata.chain_length,
+                provider_config.chain_length
+            );
+            register = true;
+        } else {
+            let hash_chain = PebbleHashChain::from_config(
+                &secret,
+                chain_id,
+                &provider_address,
+                &chain_config.contract_addr,
+                &metadata.seed,
+                provider_config.chain_length,
+                provider_config.chain_sample_interval,
+            )?;
+            let chain_state = HashChainState {
+                offsets: vec![provider_info
+                    .original_commitment_sequence_number
+                    .try_into()?],
+                hash_chains: vec![hash_chain],
+            };
+
+            if chain_state.reveal(provider_info.original_commitment_sequence_number)?
+                != provider_info.original_commitment
+            {
+                tracing::info!(
+                    "The root of the generated hash chain does not match the commitment",
+                );
+                register = true;
+            }
+        }
+    }
+    if register {
+        tracing::info!("Registering");
+        register_provider_from_config(provider_config, chain_id, chain_config)
+            .await
+            .map_err(|e| anyhow!("Chain: {} - Failed to register provider: {}", &chain_id, e))?;
+        tracing::info!("Registered");
+    }
+
+    let provider_info = contract.get_provider_info(provider_address).call().await?;
+
+    sync_fee(&contract, &provider_info, chain_config.fee)
+        .in_current_span()
+        .await?;
+
+    let uri = get_register_uri(&provider_config.uri, chain_id)?;
+    sync_uri(&contract, &provider_info, uri)
+        .in_current_span()
+        .await?;
+
+    sync_fee_manager(
+        &contract,
+        &provider_info,
+        provider_config.fee_manager.unwrap_or(Address::zero()),
+    )
+    .in_current_span()
+    .await?;
+
+    sync_max_num_hashes(
+        &contract,
+        &provider_info,
+        chain_config.max_num_hashes.unwrap_or(0),
+    )
+    .in_current_span()
+    .await?;
+
+    Ok(())
+}
+
+async fn sync_uri(
+    contract: &Arc<SignablePythContract>,
+    provider_info: &ProviderInfo,
+    uri: String,
+) -> Result<()> {
+    let uri_as_bytes: Bytes = AbiBytes::from(uri.as_str()).into();
+    if provider_info.uri != uri_as_bytes {
+        tracing::info!("Updating provider uri to {}", uri);
+        if let Some(receipt) = contract
+            .set_provider_uri(uri_as_bytes)
+            .send()
+            .await?
+            .await?
+        {
+            tracing::info!("Updated provider uri: {:?}", receipt);
+        }
+    }
+    Ok(())
+}
+
+async fn sync_fee(
+    contract: &Arc<SignablePythContract>,
+    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<SignablePythContract>,
+    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(())
+}
+
+async fn sync_max_num_hashes(
+    contract: &Arc<SignablePythContract>,
+    provider_info: &ProviderInfo,
+    max_num_hashes: u32,
+) -> Result<()> {
+    if provider_info.max_num_hashes != max_num_hashes {
+        tracing::info!("Updating provider max num hashes to {:?}", max_num_hashes);
+        if let Some(receipt) = contract
+            .set_max_num_hashes(max_num_hashes)
+            .send()
+            .await?
+            .await?
+        {
+            tracing::info!("Updated provider max num hashes to : {:?}", receipt);
+        }
+    }
+    Ok(())
+}

+ 90 - 0
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(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(())
+}

+ 356 - 0
apps/argus/src/config.rs

@@ -0,0 +1,356 @@
+use {
+    crate::{
+        api::ChainId,
+        chain::reader::{BlockNumber, BlockStatus},
+    },
+    fortuna::eth_utils::utils::EscalationPolicy,
+    anyhow::{anyhow, Result},
+    clap::{crate_authors, crate_description, crate_name, crate_version, Args, Parser},
+    ethers::types::Address,
+    std::{collections::HashMap, fs},
+};
+pub use {
+    generate::GenerateOptions, get_request::GetRequestOptions, inspect::InspectOptions,
+    register_provider::RegisterProviderOptions, request_randomness::RequestRandomnessOptions,
+    run::RunOptions, setup_provider::SetupProviderOptions, withdraw_fees::WithdrawFeesOptions,
+};
+
+mod generate;
+mod get_request;
+mod inspect;
+mod register_provider;
+mod request_randomness;
+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";
+
+#[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 Randomness Service.
+    Run(RunOptions),
+
+    /// Register a new provider with the Pyth Random oracle.
+    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 random number from the contract.
+    RequestRandomness(RequestRandomnessOptions),
+
+    /// Inspect recent requests and find unfulfilled requests with callback.
+    Inspect(InspectOptions),
+
+    /// Generate a random number by running the entire protocol end-to-end
+    Generate(GenerateOptions),
+
+    /// Get the status of a pending request for a random number.
+    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 = "FORTUNA_CONFIG")]
+    #[arg(default_value = "config.yaml")]
+    pub config: String,
+}
+
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
+pub struct Config {
+    pub chains: HashMap<ChainId, EthereumConfig>,
+    pub provider: ProviderConfig,
+    pub keeper: KeeperConfig,
+}
+
+impl Config {
+    pub fn load(path: &str) -> Result<Config> {
+        // 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<EthereumConfig> {
+        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<String>,
+
+    /// Address of a Pyth Randomness contract to interact with.
+    pub contract_addr: Address,
+
+    /// reveal_delay_blocks - The difference between the block number with the
+    /// confirmed_block_status(see below) and the block number of a request to
+    /// Entropy should be greater than `reveal_delay_blocks` for Fortuna to reveal
+    /// its commitment.
+    pub reveal_delay_blocks: BlockNumber,
+
+    /// The BlockStatus of the block that is considered confirmed.
+    /// For example, Finalized, Safe, Latest
+    #[serde(default)]
+    pub confirmed_block_status: BlockStatus,
+
+    /// Use the legacy transaction format (for networks without EIP 1559)
+    #[serde(default)]
+    pub legacy_tx: bool,
+
+    /// The gas limit to use for entropy 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,
+
+    /// Historical commitments made by the provider.
+    pub commitments: Option<Vec<Commitment>>,
+
+    /// 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<u32>,
+
+    /// 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<u64>,
+}
+
+fn default_block_delays() -> Vec<u64> {
+    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,
+        }
+    }
+}
+
+/// A commitment that the provider used to generate random numbers at some point in the past.
+/// These historical commitments need to be stored in the configuration to support transition points where
+/// the commitment changes. In theory, this information is stored on the blockchain, but unfortunately it
+/// is hard to retrieve from there.
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
+pub struct Commitment {
+    pub seed: [u8; 32],
+    pub chain_length: u64,
+    pub original_commitment_sequence_number: u64,
+}
+
+/// 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 random values from this provider,
+    /// i.e., wherever fortuna 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 the commitment,
+    /// 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 provider's secret which is a 64-char hex string.
+    /// The secret is used for generating new hash chains
+    pub secret: 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<Address>,
+}
+
+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 Fortuna API service.
+    /// The private key is a 20-byte (40 char) hex encoded Ethereum private key.
+    /// This key is required to submit transactions for entropy 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<String>,
+
+    // 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<String>,
+}
+
+impl SecretString {
+    pub fn load(&self) -> Result<Option<String>> {
+        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)
+    }
+}

+ 37 - 0
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,
+}

+ 29 - 0
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,
+}

+ 24 - 0
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<ChainId>,
+
+    /// 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,
+}

+ 17 - 0
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,
+}

+ 29 - 0
apps/argus/src/config/request_randomness.rs

@@ -0,0 +1,29 @@
+use {
+    crate::{api::ChainId, config::ConfigOptions},
+    clap::Args,
+    ethers::types::Address,
+};
+
+#[derive(Args, Clone, Debug)]
+#[command(next_help_heading = "Request Randomness Options")]
+#[group(id = "RequestRandomness")]
+pub struct RequestRandomnessOptions {
+    #[command(flatten)]
+    pub config: ConfigOptions,
+
+    /// Request randomness on this blockchain.
+    #[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")]
+    pub private_key: String,
+
+    /// Submit a randomness request to this provider
+    #[arg(long = "provider")]
+    #[arg(env = "FORTUNA_PROVIDER")]
+    pub provider: Address,
+}

+ 14 - 0
apps/argus/src/config/run.rs

@@ -0,0 +1,14 @@
+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,
+}

+ 9 - 0
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,
+}

+ 28 - 0
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<ChainId>,
+
+    /// 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,
+}

+ 226 - 0
apps/argus/src/keeper.rs

@@ -0,0 +1,226 @@
+use {
+    crate::{
+        api::{BlockchainState, ChainId},
+        chain::ethereum::{InstrumentedPythContract, InstrumentedSignablePythContract},
+        config::EthereumConfig,
+        keeper::block::{
+            get_latest_safe_block, process_backlog, process_new_blocks, watch_blocks_wrapper,
+            BlockRange,
+        },
+        keeper::commitment::update_commitments_loop,
+        keeper::fee::adjust_fee_wrapper,
+        keeper::fee::withdraw_fees_wrapper,
+        keeper::track::track_accrued_pyth_fees,
+        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::{mpsc, RwLock},
+        time::{self, Duration},
+    },
+    tracing::{self, Instrument},
+};
+
+pub(crate) mod block;
+pub(crate) mod commitment;
+pub(crate) mod fee;
+pub(crate) mod keeper_metrics;
+pub(crate) mod process_event;
+pub(crate) mod track;
+
+/// How many blocks to look back for events that might be missed when starting the keeper
+const BACKLOG_RANGE: u64 = 1000;
+/// 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);
+
+#[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 events for the last `BACKLOG_RANGE` blocks, watch for new blocks and
+/// handle any events for the new blocks.
+#[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<KeeperMetrics>,
+    rpc_metrics: Arc<RpcMetrics>,
+) {
+    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::<u64>::new()));
+
+    // Spawn a thread to handle the events from last BACKLOG_RANGE blocks.
+    let gas_limit: U256 = chain_eth_config.gas_limit.into();
+    spawn(
+        process_backlog(
+            BlockRange {
+                from: latest_safe_block.saturating_sub(BACKLOG_RANGE),
+                to: latest_safe_block,
+            },
+            contract.clone(),
+            gas_limit,
+            chain_eth_config.escalation_policy.to_policy(),
+            chain_state.clone(),
+            metrics.clone(),
+            fulfilled_requests_cache.clone(),
+            chain_eth_config.block_delays.clone(),
+        )
+        .in_current_span(),
+    );
+
+    let (tx, rx) = mpsc::channel::<BlockRange>(1000);
+    // Spawn a thread to watch for new blocks and send the range of blocks for which events has not been handled to the `tx` channel.
+    spawn(
+        watch_blocks_wrapper(
+            chain_state.clone(),
+            latest_safe_block,
+            tx,
+            chain_eth_config.geth_rpc_wss.clone(),
+        )
+        .in_current_span(),
+    );
+
+    // Spawn a thread for block processing with configured delays
+    spawn(
+        process_new_blocks(
+            chain_state.clone(),
+            rx,
+            Arc::clone(&contract),
+            gas_limit,
+            chain_eth_config.escalation_policy.to_policy(),
+            metrics.clone(),
+            fulfilled_requests_cache.clone(),
+            chain_eth_config.block_delays.clone(),
+        )
+        .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(),
+    );
+
+    // Spawn a thread that periodically adjusts the provider fee.
+    spawn(
+        adjust_fee_wrapper(
+            contract.clone(),
+            chain_state.clone(),
+            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,
+            metrics.clone(),
+        )
+        .in_current_span(),
+    );
+
+    spawn(update_commitments_loop(contract.clone(), chain_state.clone()).in_current_span());
+
+    // Spawn a thread to track the provider info and the balance of the keeper
+    spawn(
+        async move {
+            let chain_id = chain_state.id.clone();
+            let chain_config = chain_eth_config.clone();
+            let provider_address = chain_state.provider_address;
+            let keeper_metrics = metrics.clone();
+            let contract = match InstrumentedPythContract::from_config(
+                &chain_config,
+                chain_id.clone(),
+                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(),
+                );
+                spawn(
+                    track_accrued_pyth_fees(
+                        chain_id.clone(),
+                        contract.clone(),
+                        keeper_metrics.clone(),
+                    )
+                    .in_current_span(),
+                );
+
+                time::sleep(TRACK_INTERVAL).await;
+            }
+        }
+        .in_current_span(),
+    );
+}

+ 383 - 0
apps/argus/src/keeper/block.rs

@@ -0,0 +1,383 @@
+use {
+    crate::{
+        api::{self, BlockchainState},
+        chain::{ethereum::InstrumentedSignablePythContract, reader::BlockNumber},
+        keeper::keeper_metrics::KeeperMetrics,
+        keeper::process_event::process_event_with_backoff,
+    },
+    fortuna::eth_utils::utils::EscalationPolicy,
+    anyhow::{anyhow, Result},
+    ethers::{
+        providers::{Middleware, Provider, Ws},
+        types::U256,
+    },
+    futures::StreamExt,
+    std::{collections::HashSet, sync::Arc},
+    tokio::{
+        spawn,
+        sync::{mpsc, 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);
+/// How many blocks to fetch events for in a single rpc call
+const BLOCK_BATCH_SIZE: u64 = 100;
+/// How much to wait before polling the next latest block
+const POLL_INTERVAL: Duration = Duration::from_secs(2);
+/// Retry last N blocks
+const RETRY_PREVIOUS_BLOCKS: u64 = 100;
+
+#[derive(Debug, Clone)]
+pub struct BlockRange {
+    pub from: BlockNumber,
+    pub to: BlockNumber,
+}
+
+/// Get the latest safe block number for the chain. Retry internally if there is an error.
+pub async fn get_latest_safe_block(chain_state: &BlockchainState) -> BlockNumber {
+    loop {
+        match chain_state
+            .contract
+            .get_block_number(chain_state.confirmed_block_status)
+            .await
+        {
+            Ok(latest_confirmed_block) => {
+                tracing::info!(
+                    "Fetched latest safe block {}",
+                    latest_confirmed_block - chain_state.reveal_delay_blocks
+                );
+                return latest_confirmed_block - chain_state.reveal_delay_blocks;
+            }
+            Err(e) => {
+                tracing::error!("Error while getting block number. error: {:?}", e);
+                time::sleep(RETRY_INTERVAL).await;
+            }
+        }
+    }
+}
+
+/// Process a range of blocks in batches. It calls the `process_single_block_batch` method for each batch.
+#[tracing::instrument(skip_all, fields(
+    range_from_block = block_range.from, range_to_block = block_range.to
+))]
+pub async fn process_block_range(
+    block_range: BlockRange,
+    contract: Arc<InstrumentedSignablePythContract>,
+    gas_limit: U256,
+    escalation_policy: EscalationPolicy,
+    chain_state: api::BlockchainState,
+    metrics: Arc<KeeperMetrics>,
+    fulfilled_requests_cache: Arc<RwLock<HashSet<u64>>>,
+) {
+    let BlockRange {
+        from: first_block,
+        to: last_block,
+    } = block_range;
+    let mut current_block = first_block;
+    while current_block <= last_block {
+        let mut to_block = current_block + BLOCK_BATCH_SIZE;
+        if to_block > last_block {
+            to_block = last_block;
+        }
+
+        // TODO: this is handling all blocks sequentially we might want to handle them in parallel in future.
+        process_single_block_batch(
+            BlockRange {
+                from: current_block,
+                to: to_block,
+            },
+            contract.clone(),
+            gas_limit,
+            escalation_policy.clone(),
+            chain_state.clone(),
+            metrics.clone(),
+            fulfilled_requests_cache.clone(),
+        )
+        .in_current_span()
+        .await;
+
+        current_block = to_block + 1;
+    }
+}
+
+/// Process a batch of blocks for a chain. It will fetch events for all the blocks in a single call for the provided batch
+/// and then try to process them one by one. It checks the `fulfilled_request_cache`. If the request was already fulfilled.
+/// It won't reprocess it. If the request was already processed, it will reprocess it.
+/// If the process fails, it will retry indefinitely.
+#[tracing::instrument(name = "batch", skip_all, fields(
+    batch_from_block = block_range.from, batch_to_block = block_range.to
+))]
+pub async fn process_single_block_batch(
+    block_range: BlockRange,
+    contract: Arc<InstrumentedSignablePythContract>,
+    gas_limit: U256,
+    escalation_policy: EscalationPolicy,
+    chain_state: api::BlockchainState,
+    metrics: Arc<KeeperMetrics>,
+    fulfilled_requests_cache: Arc<RwLock<HashSet<u64>>>,
+) {
+    loop {
+        let events_res = chain_state
+            .contract
+            .get_request_with_callback_events(block_range.from, block_range.to)
+            .await;
+
+        match events_res {
+            Ok(events) => {
+                tracing::info!(num_of_events = &events.len(), "Processing",);
+                for event in &events {
+                    // the write lock guarantees we spawn only one task per sequence number
+                    let newly_inserted = fulfilled_requests_cache
+                        .write()
+                        .await
+                        .insert(event.sequence_number);
+                    if newly_inserted {
+                        spawn(
+                            process_event_with_backoff(
+                                event.clone(),
+                                chain_state.clone(),
+                                contract.clone(),
+                                gas_limit,
+                                escalation_policy.clone(),
+                                metrics.clone(),
+                            )
+                            .in_current_span(),
+                        );
+                    }
+                }
+                tracing::info!(num_of_events = &events.len(), "Processed",);
+                break;
+            }
+            Err(e) => {
+                tracing::error!(
+                    "Error while getting events. Waiting for {} seconds before retry. error: {:?}",
+                    RETRY_INTERVAL.as_secs(),
+                    e
+                );
+                time::sleep(RETRY_INTERVAL).await;
+            }
+        }
+    }
+}
+
+/// Wrapper for the `watch_blocks` method. If there was an error while watching, it will retry after a delay.
+/// It retries indefinitely.
+#[tracing::instrument(name = "watch_blocks", skip_all, fields(
+    initial_safe_block = latest_safe_block
+))]
+pub async fn watch_blocks_wrapper(
+    chain_state: BlockchainState,
+    latest_safe_block: BlockNumber,
+    tx: mpsc::Sender<BlockRange>,
+    geth_rpc_wss: Option<String>,
+) {
+    let mut last_safe_block_processed = latest_safe_block;
+    loop {
+        if let Err(e) = watch_blocks(
+            chain_state.clone(),
+            &mut last_safe_block_processed,
+            tx.clone(),
+            geth_rpc_wss.clone(),
+        )
+        .in_current_span()
+        .await
+        {
+            tracing::error!("watching blocks. error: {:?}", e);
+            time::sleep(RETRY_INTERVAL).await;
+        }
+    }
+}
+
+/// Watch for new blocks and send the range of blocks for which events have not been handled to the `tx` channel.
+/// We are subscribing to new blocks instead of events. If we miss some blocks, it will be fine as we are sending
+/// block ranges to the `tx` channel. If we have subscribed to events, we could have missed those and won't even
+/// know about it.
+pub async fn watch_blocks(
+    chain_state: BlockchainState,
+    last_safe_block_processed: &mut BlockNumber,
+    tx: mpsc::Sender<BlockRange>,
+    geth_rpc_wss: Option<String>,
+) -> Result<()> {
+    tracing::info!("Watching blocks to handle new events");
+
+    let provider_option = match geth_rpc_wss {
+        Some(wss) => Some(match Provider::<Ws>::connect(wss.clone()).await {
+            Ok(provider) => provider,
+            Err(e) => {
+                tracing::error!("Error while connecting to wss: {}. error: {:?}", wss, e);
+                return Err(e.into());
+            }
+        }),
+        None => {
+            tracing::info!("No wss provided");
+            None
+        }
+    };
+
+    let mut stream_option = match provider_option {
+        Some(ref provider) => Some(match provider.subscribe_blocks().await {
+            Ok(client) => client,
+            Err(e) => {
+                tracing::error!("Error while subscribing to blocks. error {:?}", e);
+                return Err(e.into());
+            }
+        }),
+        None => None,
+    };
+
+    loop {
+        match stream_option {
+            Some(ref mut stream) => {
+                if stream.next().await.is_none() {
+                    tracing::error!("Error blocks subscription stream ended");
+                    return Err(anyhow!("Error blocks subscription stream ended"));
+                }
+            }
+            None => {
+                time::sleep(POLL_INTERVAL).await;
+            }
+        }
+
+        let latest_safe_block = get_latest_safe_block(&chain_state).in_current_span().await;
+        if latest_safe_block > *last_safe_block_processed {
+            let mut from = latest_safe_block.saturating_sub(RETRY_PREVIOUS_BLOCKS);
+
+            // In normal situation, the difference between latest and last safe block should not be more than 2-3 (for arbitrum it can be 10)
+            // TODO: add a metric for this in separate PR. We need alerts
+            // But in extreme situation, where we were unable to send the block range multiple times, the difference between latest_safe_block and
+            // last_safe_block_processed can grow. It is fine to not have the retry mechanisms for those earliest blocks as we expect the rpc
+            // to be in consistency after this much time.
+            if from > *last_safe_block_processed {
+                from = *last_safe_block_processed;
+            }
+            match tx
+                .send(BlockRange {
+                    from,
+                    to: latest_safe_block,
+                })
+                .await
+            {
+                Ok(_) => {
+                    tracing::info!(
+                        from_block = from,
+                        to_block = &latest_safe_block,
+                        "Block range sent to handle events",
+                    );
+                    *last_safe_block_processed = latest_safe_block;
+                }
+                Err(e) => {
+                    tracing::error!(
+                        from_block = from,
+                        to_block = &latest_safe_block,
+                        "Error while sending block range to handle events. These will be handled in next call. error: {:?}",
+                        e
+                    );
+                }
+            };
+        }
+    }
+}
+
+/// It waits on rx channel to receive block ranges and then calls process_block_range to process them
+/// for each configured block delay.
+#[tracing::instrument(skip_all)]
+#[allow(clippy::too_many_arguments)]
+pub async fn process_new_blocks(
+    chain_state: BlockchainState,
+    mut rx: mpsc::Receiver<BlockRange>,
+    contract: Arc<InstrumentedSignablePythContract>,
+    gas_limit: U256,
+    escalation_policy: EscalationPolicy,
+    metrics: Arc<KeeperMetrics>,
+    fulfilled_requests_cache: Arc<RwLock<HashSet<u64>>>,
+    block_delays: Vec<u64>,
+) {
+    tracing::info!("Waiting for new block ranges to process");
+    loop {
+        if let Some(block_range) = rx.recv().await {
+            // Process blocks immediately first
+            process_block_range(
+                block_range.clone(),
+                Arc::clone(&contract),
+                gas_limit,
+                escalation_policy.clone(),
+                chain_state.clone(),
+                metrics.clone(),
+                fulfilled_requests_cache.clone(),
+            )
+            .in_current_span()
+            .await;
+
+            // Then process with each configured delay
+            for delay in &block_delays {
+                let adjusted_range = BlockRange {
+                    from: block_range.from.saturating_sub(*delay),
+                    to: block_range.to.saturating_sub(*delay),
+                };
+                process_block_range(
+                    adjusted_range,
+                    Arc::clone(&contract),
+                    gas_limit,
+                    escalation_policy.clone(),
+                    chain_state.clone(),
+                    metrics.clone(),
+                    fulfilled_requests_cache.clone(),
+                )
+                .in_current_span()
+                .await;
+            }
+        }
+    }
+}
+
+/// Processes the backlog_range for a chain.
+/// It processes the backlog range for each configured block delay.
+#[allow(clippy::too_many_arguments)]
+#[tracing::instrument(skip_all)]
+pub async fn process_backlog(
+    backlog_range: BlockRange,
+    contract: Arc<InstrumentedSignablePythContract>,
+    gas_limit: U256,
+    escalation_policy: EscalationPolicy,
+    chain_state: BlockchainState,
+    metrics: Arc<KeeperMetrics>,
+    fulfilled_requests_cache: Arc<RwLock<HashSet<u64>>>,
+    block_delays: Vec<u64>,
+) {
+    tracing::info!("Processing backlog");
+    // Process blocks immediately first
+    process_block_range(
+        backlog_range.clone(),
+        Arc::clone(&contract),
+        gas_limit,
+        escalation_policy.clone(),
+        chain_state.clone(),
+        metrics.clone(),
+        fulfilled_requests_cache.clone(),
+    )
+    .in_current_span()
+    .await;
+
+    // Then process with each configured delay
+    for delay in &block_delays {
+        let adjusted_range = BlockRange {
+            from: backlog_range.from.saturating_sub(*delay),
+            to: backlog_range.to.saturating_sub(*delay),
+        };
+        process_block_range(
+            adjusted_range,
+            Arc::clone(&contract),
+            gas_limit,
+            escalation_policy.clone(),
+            chain_state.clone(),
+            metrics.clone(),
+            fulfilled_requests_cache.clone(),
+        )
+        .in_current_span()
+        .await;
+    }
+    tracing::info!("Backlog processed");
+}

+ 64 - 0
apps/argus/src/keeper/commitment.rs

@@ -0,0 +1,64 @@
+use {
+    crate::{
+        api::BlockchainState, chain::ethereum::InstrumentedSignablePythContract,
+        keeper::block::get_latest_safe_block,
+    },
+    fortuna::eth_utils::utils::send_and_confirm,
+    anyhow::{anyhow, Result},
+    std::sync::Arc,
+    tokio::time::{self, Duration},
+    tracing::{self, Instrument},
+};
+
+/// Check whether we need to manually update the commitments to reduce numHashes for future
+/// requests and reduce the gas cost of the reveal.
+const UPDATE_COMMITMENTS_INTERVAL: Duration = Duration::from_secs(30);
+const UPDATE_COMMITMENTS_THRESHOLD_FACTOR: f64 = 0.95;
+
+#[tracing::instrument(name = "update_commitments", skip_all)]
+pub async fn update_commitments_loop(
+    contract: Arc<InstrumentedSignablePythContract>,
+    chain_state: BlockchainState,
+) {
+    loop {
+        if let Err(e) = update_commitments_if_necessary(contract.clone(), &chain_state)
+            .in_current_span()
+            .await
+        {
+            tracing::error!("Update commitments. error: {:?}", e);
+        }
+        time::sleep(UPDATE_COMMITMENTS_INTERVAL).await;
+    }
+}
+
+pub async fn update_commitments_if_necessary(
+    contract: Arc<InstrumentedSignablePythContract>,
+    chain_state: &BlockchainState,
+) -> Result<()> {
+    //TODO: we can reuse the result from the last call from the watch_blocks thread to reduce RPCs
+    let latest_safe_block = get_latest_safe_block(chain_state).in_current_span().await;
+    let provider_address = chain_state.provider_address;
+    let provider_info = contract
+        .get_provider_info(provider_address)
+        .block(latest_safe_block) // To ensure we are not revealing sooner than we should
+        .call()
+        .await
+        .map_err(|e| anyhow!("Error while getting provider info. error: {:?}", e))?;
+    if provider_info.max_num_hashes == 0 {
+        return Ok(());
+    }
+    let threshold =
+        ((provider_info.max_num_hashes as f64) * UPDATE_COMMITMENTS_THRESHOLD_FACTOR) as u64;
+    if provider_info.sequence_number - provider_info.current_commitment_sequence_number > threshold
+    {
+        let seq_number = provider_info.sequence_number - 1;
+        let provider_revelation = chain_state
+            .state
+            .reveal(seq_number)
+            .map_err(|e| anyhow!("Error revealing: {:?}", e))?;
+        let contract_call =
+            contract.advance_provider_commitment(provider_address, seq_number, provider_revelation);
+        send_and_confirm(contract_call).await?;
+    }
+    Ok(())
+}

+ 253 - 0
apps/argus/src/keeper/fee.rs

@@ -0,0 +1,253 @@
+use {
+    crate::{
+        api::BlockchainState, chain::ethereum::InstrumentedSignablePythContract,
+        keeper::AccountLabel, keeper::ChainId, keeper::KeeperMetrics,
+    },
+    fortuna::eth_utils::utils::{estimate_tx_cost, send_and_confirm},
+    anyhow::{anyhow, Result},
+    ethers::{
+        middleware::Middleware,
+        signers::Signer,
+        types::{Address, U256},
+    },
+    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<InstrumentedSignablePythContract>,
+    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<InstrumentedSignablePythContract>,
+    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<InstrumentedSignablePythContract>,
+    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<KeeperMetrics>,
+) {
+    // The maximum balance of accrued fees + provider wallet balance. None if we haven't observed a value yet.
+    let mut high_water_pnl: Option<U256> = None;
+    // The sequence number where the keeper last updated the on-chain fee. None if we haven't observed it yet.
+    let mut sequence_number_of_last_fee_update: Option<u64> = 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 sequence_number_of_last_fee_update,
+            metrics.clone(),
+        )
+        .in_current_span()
+        .await
+        {
+            tracing::error!("Withdrawing 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 one random number has been requested 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<InstrumentedSignablePythContract>,
+    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<U256>,
+    sequence_number_of_last_fee_update: &mut Option<u64>,
+    metrics: Arc<KeeperMetrics>,
+) -> 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,
+    };
+
+    // Determine if the chain has seen activity since the last fee update.
+    let is_chain_active: bool = match sequence_number_of_last_fee_update {
+        Some(n) => provider_info.sequence_number > *n,
+        None => {
+            // We don't want to adjust the fees on server start for unused chains, hence false here.
+            false
+        }
+    };
+
+    let provider_fee: u128 = provider_info.fee_in_wei;
+    if is_chain_active
+        && ((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_as_fee_manager(provider_address, target_fee);
+        send_and_confirm(contract_call).await?;
+
+        *sequence_number_of_last_fee_update = Some(provider_info.sequence_number);
+    } else {
+        tracing::info!(
+            "Skipping fee adjustment. Current: {:?} Target: {:?} [{:?}, {:?}] Current Sequence Number: {:?} Last updated sequence number {:?} Current pnl: {:?} High water pnl: {:?}",
+            provider_fee,
+            target_fee,
+            target_fee_min,
+            target_fee_max,
+            provider_info.sequence_number,
+            sequence_number_of_last_fee_update,
+            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)),
+    ));
+
+    // Update sequence number on server start.
+    match sequence_number_of_last_fee_update {
+        Some(_) => (),
+        None => {
+            *sequence_number_of_last_fee_update = Some(provider_info.sequence_number);
+        }
+    };
+
+    Ok(())
+}

+ 284 - 0
apps/argus/src/keeper/keeper_metrics.rs

@@ -0,0 +1,284 @@
+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,
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
+pub struct ChainIdLabel {
+    pub chain_id: String,
+}
+
+pub struct KeeperMetrics {
+    pub current_sequence_number: Family<AccountLabel, Gauge>,
+    pub end_sequence_number: Family<AccountLabel, Gauge>,
+    pub balance: Family<AccountLabel, Gauge<f64, AtomicU64>>,
+    pub collected_fee: Family<AccountLabel, Gauge<f64, AtomicU64>>,
+    pub current_fee: Family<AccountLabel, Gauge<f64, AtomicU64>>,
+    pub target_provider_fee: Family<AccountLabel, Gauge<f64, AtomicU64>>,
+    pub total_gas_spent: Family<AccountLabel, Gauge<f64, AtomicU64>>,
+    pub total_gas_fee_spent: Family<AccountLabel, Gauge<f64, AtomicU64>>,
+    pub requests: Family<AccountLabel, Counter>,
+    pub requests_processed: Family<AccountLabel, Counter>,
+    pub requests_processed_success: Family<AccountLabel, Counter>,
+    pub requests_processed_failure: Family<AccountLabel, Counter>,
+    pub requests_reprocessed: Family<AccountLabel, Counter>,
+    pub reveals: Family<AccountLabel, Counter>,
+    pub request_duration_ms: Family<AccountLabel, Histogram>,
+    pub retry_count: Family<AccountLabel, Histogram>,
+    pub final_gas_multiplier: Family<AccountLabel, Histogram>,
+    pub final_fee_multiplier: Family<AccountLabel, Histogram>,
+    pub gas_price_estimate: Family<AccountLabel, Gauge<f64, AtomicU64>>,
+    pub accrued_pyth_fees: Family<ChainIdLabel, Gauge<f64, AtomicU64>>,
+}
+
+impl Default for KeeperMetrics {
+    fn default() -> Self {
+        Self {
+            current_sequence_number: Family::default(),
+            end_sequence_number: Family::default(),
+            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(),
+            reveals: 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(),
+            accrued_pyth_fees: Family::default(),
+        }
+    }
+}
+
+impl KeeperMetrics {
+    pub async fn new(
+        registry: Arc<RwLock<Registry>>,
+        chain_labels: Vec<(String, Address)>,
+    ) -> Self {
+        let mut writable_registry = registry.write().await;
+        let keeper_metrics = KeeperMetrics::default();
+
+        writable_registry.register(
+            "current_sequence_number",
+            "The sequence number for a new request",
+            keeper_metrics.current_sequence_number.clone(),
+        );
+
+        writable_registry.register(
+            "end_sequence_number",
+            "The sequence number for the end request",
+            keeper_metrics.end_sequence_number.clone(),
+        );
+
+        writable_registry.register(
+            "requests",
+            "Number of requests received through events",
+            keeper_metrics.requests.clone(),
+        );
+
+        writable_registry.register(
+            "requests_processed",
+            "Number of requests processed",
+            keeper_metrics.requests_processed.clone(),
+        );
+
+        writable_registry.register(
+            "requests_processed_success",
+            "Number of requests processed successfully",
+            keeper_metrics.requests_processed_success.clone(),
+        );
+
+        writable_registry.register(
+            "requests_processed_failure",
+            "Number of requests processed with failure",
+            keeper_metrics.requests_processed_failure.clone(),
+        );
+
+        writable_registry.register(
+            "reveal",
+            "Number of reveals",
+            keeper_metrics.reveals.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 revealing requests",
+            keeper_metrics.total_gas_spent.clone(),
+        );
+
+        writable_registry.register(
+            "total_gas_fee_spent",
+            "Total amount of ETH spent on gas for revealing requests",
+            keeper_metrics.total_gas_fee_spent.clone(),
+        );
+
+        writable_registry.register(
+            "requests_reprocessed",
+            "Number of 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(),
+        );
+
+        writable_registry.register(
+            "accrued_pyth_fees",
+            "Accrued Pyth fees on the contract",
+            keeper_metrics.accrued_pyth_fees.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
+
+        // Initialize accrued_pyth_fees for each chain_id
+        for (chain_id, _) in chain_labels.iter() {
+            let _ = keeper_metrics
+                .accrued_pyth_fees
+                .get_or_create(&ChainIdLabel {
+                    chain_id: chain_id.clone(),
+                });
+        }
+
+        for (chain_id, provider_address) in chain_labels {
+            let account_label = AccountLabel {
+                chain_id,
+                address: provider_address.to_string(),
+            };
+
+            let _ = keeper_metrics
+                .current_sequence_number
+                .get_or_create(&account_label);
+            let _ = keeper_metrics
+                .end_sequence_number
+                .get_or_create(&account_label);
+            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.reveals.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
+    }
+}

+ 135 - 0
apps/argus/src/keeper/process_event.rs

@@ -0,0 +1,135 @@
+use {
+    super::keeper_metrics::{AccountLabel, KeeperMetrics},
+    crate::{
+        api::BlockchainState,
+        chain::{ethereum::InstrumentedSignablePythContract, reader::RequestedWithCallbackEvent},
+    },
+    fortuna::eth_utils::utils::{submit_tx_with_backoff, EscalationPolicy},
+    anyhow::{anyhow, Result},
+    ethers::types::U256,
+    std::sync::Arc,
+    tracing,
+};
+
+/// Process an event with backoff. It will retry the reveal on failure for 5 minutes.
+#[tracing::instrument(name = "process_event_with_backoff", skip_all, fields(
+    sequence_number = event.sequence_number
+))]
+pub async fn process_event_with_backoff(
+    event: RequestedWithCallbackEvent,
+    chain_state: BlockchainState,
+    contract: Arc<InstrumentedSignablePythContract>,
+    gas_limit: U256,
+    escalation_policy: EscalationPolicy,
+    metrics: Arc<KeeperMetrics>,
+) -> Result<()> {
+    // ignore requests that are not for the configured provider
+    if chain_state.provider_address != event.provider_address {
+        return Ok(());
+    }
+
+    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 event");
+
+    let provider_revelation = chain_state
+        .state
+        .reveal(event.sequence_number)
+        .map_err(|e| anyhow!("Error revealing: {:?}", e))?;
+
+    let contract_call = contract.reveal_with_callback(
+        event.provider_address,
+        event.sequence_number,
+        event.user_random_number,
+        provider_revelation,
+    );
+
+    let success = submit_tx_with_backoff(
+        contract.client(),
+        contract_call,
+        gas_limit,
+        escalation_policy,
+    )
+    .await;
+
+    metrics
+        .requests_processed
+        .get_or_create(&account_label)
+        .inc();
+
+    match success {
+        Ok(res) => {
+            tracing::info!("Processed event 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);
+
+            let 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.reveals.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(event.provider_address, event.sequence_number)
+                .await;
+
+            tracing::error!("Failed to process event: {:?}. 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(())
+}

+ 130 - 0
apps/argus/src/keeper/track.rs

@@ -0,0 +1,130 @@
+use {
+    super::keeper_metrics::{AccountLabel, ChainIdLabel, KeeperMetrics},
+    crate::{
+        api::ChainId, chain::ethereum::InstrumentedPythContract,
+    },
+    fortuna::eth_utils::traced_client::TracedClient,
+    ethers::middleware::Middleware,
+    ethers::{providers::Provider, types::Address},
+    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<Provider<TracedClient>>,
+    address: Address,
+    metrics: Arc<KeeperMetrics>,
+) {
+    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 the hashchain data of the given provider address 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<KeeperMetrics>,
+) {
+    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;
+
+    let current_sequence_number = provider_info.sequence_number;
+    let end_sequence_number = provider_info.end_sequence_number;
+
+    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);
+
+    metrics
+        .current_sequence_number
+        .get_or_create(&AccountLabel {
+            chain_id: chain_id.clone(),
+            address: provider_address.to_string(),
+        })
+        // sequence_number type on chain is u64 but practically it will take
+        // a long time for it to cross the limits of i64.
+        // currently prometheus only supports i64 for Gauge types
+        .set(current_sequence_number as i64);
+    metrics
+        .end_sequence_number
+        .get_or_create(&AccountLabel {
+            chain_id: chain_id.clone(),
+            address: provider_address.to_string(),
+        })
+        .set(end_sequence_number as i64);
+}
+
+/// tracks the accrued pyth fees on the given chain
+/// if there is an error the function will just return
+#[tracing::instrument(skip_all)]
+pub async fn track_accrued_pyth_fees(
+    chain_id: ChainId,
+    contract: InstrumentedPythContract,
+    metrics: Arc<KeeperMetrics>,
+) {
+    let accrued_pyth_fees = match contract.get_accrued_pyth_fees().call().await {
+        Ok(fees) => fees,
+        Err(e) => {
+            tracing::error!("Error while getting accrued pyth fees. 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 accrued_pyth_fees = accrued_pyth_fees as f64 / 1e18;
+
+    metrics
+        .accrued_pyth_fees
+        .get_or_create(&ChainIdLabel {
+            chain_id: chain_id.clone(),
+        })
+        .set(accrued_pyth_fees);
+}

+ 6 - 0
apps/argus/src/lib.rs

@@ -0,0 +1,6 @@
+pub mod api;
+pub mod chain;
+pub mod command;
+pub mod config;
+pub mod keeper;
+pub mod state;

+ 41 - 0
apps/argus/src/main.rs

@@ -0,0 +1,41 @@
+#![allow(clippy::just_underscores_and_digits)]
+
+use {
+    anyhow::Result,
+    clap::Parser,
+    fortuna::{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::RequestRandomness(opts) => command::request_randomness(&opts).await,
+        config::Options::Inspect(opts) => command::inspect(&opts).await,
+        config::Options::WithdrawFees(opts) => command::withdraw_fees(&opts).await,
+    }
+}

+ 172 - 0
apps/argus/src/state.rs

@@ -0,0 +1,172 @@
+use {
+    crate::api::ChainId,
+    anyhow::{ensure, Result},
+    ethers::types::Address,
+    sha3::{Digest, Keccak256},
+};
+
+/// A hash chain of a specific length. The hash chain has the property that
+/// hash(chain.reveal_ith(i)) == chain.reveal_ith(i - 1)
+///
+/// The implementation subsamples the elements of the chain such that it uses less memory
+/// to keep the chain around.
+#[derive(Clone)]
+pub struct PebbleHashChain {
+    hash: Vec<[u8; 32]>,
+    sample_interval: usize,
+    length: usize,
+}
+
+impl PebbleHashChain {
+    // Given a secret, we hash it with Keccak256 len times to get the final hash, this is an S/KEY
+    // like protocol in which revealing the hashes in reverse proves knowledge.
+    pub fn new(secret: [u8; 32], length: usize, sample_interval: usize) -> Self {
+        assert!(sample_interval > 0, "Sample interval must be positive");
+        let mut hash = Vec::<[u8; 32]>::with_capacity(length);
+        let mut current: [u8; 32] = Keccak256::digest(secret).into();
+
+        hash.push(current);
+        for i in 1..length {
+            current = Keccak256::digest(current).into();
+            if i % sample_interval == 0 {
+                hash.push(current);
+            }
+        }
+
+        hash.reverse();
+
+        Self {
+            hash,
+            sample_interval,
+            length,
+        }
+    }
+
+    pub fn from_config(
+        secret: &str,
+        chain_id: &ChainId,
+        provider_address: &Address,
+        contract_address: &Address,
+        random: &[u8; 32],
+        chain_length: u64,
+        sample_interval: u64,
+    ) -> Result<Self> {
+        let mut input: Vec<u8> = vec![];
+        input.extend_from_slice(&hex::decode(secret.trim())?);
+        input.extend_from_slice(chain_id.as_bytes());
+        input.extend_from_slice(provider_address.as_bytes());
+        input.extend_from_slice(contract_address.as_bytes());
+        input.extend_from_slice(random);
+
+        let secret: [u8; 32] = Keccak256::digest(input).into();
+        Ok(Self::new(
+            secret,
+            chain_length.try_into()?,
+            sample_interval.try_into()?,
+        ))
+    }
+
+    pub fn reveal_ith(&self, i: usize) -> Result<[u8; 32]> {
+        ensure!(i < self.len(), "index not in range");
+
+        // Note that subsample_interval may not perfectly divide length, in which case the uneven segment is
+        // actually at the *front* of the list. Thus, it's easier to compute indexes from the end of the list.
+        let index_from_end_of_subsampled_list = ((self.len() - 1) - i) / self.sample_interval;
+        let mut i_index = self.len() - 1 - index_from_end_of_subsampled_list * self.sample_interval;
+        let mut val = self.hash[self.hash.len() - 1 - index_from_end_of_subsampled_list];
+
+        while i_index > i {
+            val = Keccak256::digest(val).into();
+            i_index -= 1;
+        }
+
+        Ok(val)
+    }
+
+    #[allow(clippy::len_without_is_empty)]
+    pub fn len(&self) -> usize {
+        self.length
+    }
+}
+
+/// `HashChainState` tracks the mapping between on-chain sequence numbers to hash chains.
+/// This struct is required to handle the case where the provider rotates their commitment,
+/// which requires tracking multiple hash chains here.
+pub struct HashChainState {
+    // The sequence number where the hash chain starts. Must be stored in sorted order.
+    pub offsets: Vec<usize>,
+    pub hash_chains: Vec<PebbleHashChain>,
+}
+
+impl HashChainState {
+    pub fn from_chain_at_offset(offset: usize, chain: PebbleHashChain) -> HashChainState {
+        HashChainState {
+            offsets: vec![offset],
+            hash_chains: vec![chain],
+        }
+    }
+
+    pub fn reveal(&self, sequence_number: u64) -> Result<[u8; 32]> {
+        let sequence_number: usize = sequence_number.try_into()?;
+        let chain_index = self
+            .offsets
+            .partition_point(|x| x <= &sequence_number)
+            .checked_sub(1)
+            .ok_or(anyhow::anyhow!(
+                "Hash chain for the requested sequence number is not available."
+            ))?;
+        self.hash_chains[chain_index].reveal_ith(sequence_number - self.offsets[chain_index])
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use {
+        crate::state::PebbleHashChain,
+        sha3::{Digest, Keccak256},
+    };
+
+    fn run_hash_chain_test(secret: [u8; 32], length: usize, sample_interval: usize) {
+        // Calculate the hash chain the naive way as a comparison point to the subsampled implementation.
+        let mut basic_chain = Vec::<[u8; 32]>::with_capacity(length);
+        let mut current: [u8; 32] = Keccak256::digest(secret).into();
+        basic_chain.push(current);
+        for _ in 1..length {
+            current = Keccak256::digest(current).into();
+            basic_chain.push(current);
+        }
+
+        basic_chain.reverse();
+
+        let chain = PebbleHashChain::new(secret, length, sample_interval);
+
+        let mut last_val = chain.reveal_ith(0).unwrap();
+
+        #[allow(clippy::needless_range_loop)]
+        for i in 1..length {
+            let cur_val = chain.reveal_ith(i).unwrap();
+            println!("{}", i);
+            assert_eq!(basic_chain[i], cur_val);
+
+            let expected_last_val: [u8; 32] = Keccak256::digest(cur_val).into();
+            assert_eq!(expected_last_val, last_val);
+            last_val = cur_val;
+        }
+    }
+
+    #[test]
+    fn test_hash_chain() {
+        run_hash_chain_test([0u8; 32], 10, 1);
+        run_hash_chain_test([0u8; 32], 10, 2);
+        run_hash_chain_test([0u8; 32], 10, 3);
+        run_hash_chain_test([1u8; 32], 10, 1);
+        run_hash_chain_test([1u8; 32], 10, 2);
+        run_hash_chain_test([1u8; 32], 10, 3);
+        run_hash_chain_test([0u8; 32], 100, 1);
+        run_hash_chain_test([0u8; 32], 100, 2);
+        run_hash_chain_test([0u8; 32], 100, 3);
+        run_hash_chain_test([0u8; 32], 100, 7);
+        run_hash_chain_test([0u8; 32], 100, 50);
+        run_hash_chain_test([0u8; 32], 100, 55);
+    }
+}