From b70f84a13ef1897da395a312eae2abb2084d36a4 Mon Sep 17 00:00:00 2001
From: Artur Puzio <contact@puzio.waw.pl>
Date: Fri, 6 Sep 2024 16:17:11 +0200
Subject: [PATCH 01/11] WIP

---
 core/lib/external_price_api/src/coingecko_api.rs | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/core/lib/external_price_api/src/coingecko_api.rs b/core/lib/external_price_api/src/coingecko_api.rs
index 8fa7514b3684..a710a610497d 100644
--- a/core/lib/external_price_api/src/coingecko_api.rs
+++ b/core/lib/external_price_api/src/coingecko_api.rs
@@ -110,3 +110,8 @@ impl CoinGeckoPriceResponse {
             .and_then(|price| price.get(currency))
     }
 }
+
+#[cfg(test)]
+mod test {
+    fn setup() {}
+}

From 7950d15841db59c09280a9fa244aed70ff3603cb Mon Sep 17 00:00:00 2001
From: Artur Puzio <contact@puzio.waw.pl>
Date: Wed, 25 Sep 2024 17:35:09 +0200
Subject: [PATCH 02/11] get_fraction test

---
 core/lib/external_price_api/src/utils.rs | 29 ++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/core/lib/external_price_api/src/utils.rs b/core/lib/external_price_api/src/utils.rs
index 879be44e1737..f6d00b080fee 100644
--- a/core/lib/external_price_api/src/utils.rs
+++ b/core/lib/external_price_api/src/utils.rs
@@ -13,3 +13,32 @@ pub fn get_fraction(ratio_f64: f64) -> (NonZeroU64, NonZeroU64) {
 
     (numerator, denominator)
 }
+
+#[cfg(test)]
+pub(crate) mod tests {
+    use super::*;
+
+    fn assert_get_fraction_value(f: f64, num: u64, denum: u64) {
+        assert_eq!(
+            get_fraction(f),
+            (
+                NonZeroU64::try_from(num).unwrap(),
+                NonZeroU64::try_from(denum).unwrap()
+            )
+        );
+    }
+
+    #[test]
+    fn test_float_to_fraction_conversion_as_expected() {
+        assert_get_fraction_value(1.0, 1, 1);
+        assert_get_fraction_value(1337.0, 1337, 1);
+        assert_get_fraction_value(0.1, 1, 10);
+        assert_get_fraction_value(3.141, 3141, 1000);
+        assert_get_fraction_value(1_000_000.0, 1_000_000, 1);
+        assert_get_fraction_value(3123.47, 312347, 100);
+        // below tests assume some not necessarily required behaviour of get_fraction
+        assert_get_fraction_value(0.2, 1, 5);
+        assert_get_fraction_value(0.5, 1, 2);
+        assert_get_fraction_value(3.1415, 6283, 2000);
+    }
+}

From 46935a071c5a281cb8fc60568b963bed931e7bcb Mon Sep 17 00:00:00 2001
From: Artur Puzio <contact@puzio.waw.pl>
Date: Wed, 25 Sep 2024 17:35:35 +0200
Subject: [PATCH 03/11] WIP

---
 Cargo.lock                                    | 355 +++++++++++++++++-
 Cargo.toml                                    |   1 +
 core/lib/external_price_api/Cargo.toml        |   1 +
 .../external_price_api/src/coingecko_api.rs   |  19 +-
 core/lib/external_price_api/src/lib.rs        |   1 +
 core/lib/external_price_api/src/tests.rs      | 132 +++++++
 6 files changed, 501 insertions(+), 8 deletions(-)
 create mode 100644 core/lib/external_price_api/src/tests.rs

diff --git a/Cargo.lock b/Cargo.lock
index 8164d412af55..13beced2519e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -233,12 +233,52 @@ version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
 
+[[package]]
+name = "ascii-canvas"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6"
+dependencies = [
+ "term",
+]
+
+[[package]]
+name = "assert-json-diff"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "assert_matches"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
 
+[[package]]
+name = "async-attributes"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5"
+dependencies = [
+ "quote 1.0.36",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener 2.5.3",
+ "futures-core",
+]
+
 [[package]]
 name = "async-channel"
 version = "2.3.1"
@@ -275,6 +315,21 @@ dependencies = [
  "futures-lite",
 ]
 
+[[package]]
+name = "async-global-executor"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
+dependencies = [
+ "async-channel 2.3.1",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "blocking",
+ "futures-lite",
+ "once_cell",
+]
+
 [[package]]
 name = "async-io"
 version = "2.3.4"
@@ -316,13 +371,22 @@ dependencies = [
  "futures-lite",
 ]
 
+[[package]]
+name = "async-object-pool"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "333c456b97c3f2d50604e8b2624253b7f787208cb72eb75e64b0ad11b221652c"
+dependencies = [
+ "async-std",
+]
+
 [[package]]
 name = "async-process"
 version = "2.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8a07789659a4d385b79b18b9127fc27e1a59e1e89117c78c5ea3b806f016374"
 dependencies = [
- "async-channel",
+ "async-channel 2.3.1",
  "async-io",
  "async-lock",
  "async-signal",
@@ -365,6 +429,34 @@ dependencies = [
  "windows-sys 0.59.0",
 ]
 
+[[package]]
+name = "async-std"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615"
+dependencies = [
+ "async-attributes",
+ "async-channel 1.9.0",
+ "async-global-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "crossbeam-utils",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-lite",
+ "gloo-timers 0.3.0",
+ "kv-log-macro",
+ "log",
+ "memchr",
+ "once_cell",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen-futures",
+]
+
 [[package]]
 name = "async-stream"
 version = "0.3.5"
@@ -594,6 +686,17 @@ version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
 
+[[package]]
+name = "basic-cookies"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67bd8fd42c16bdb08688243dc5f0cc117a3ca9efeeaba3a345a18a6159ad96f7"
+dependencies = [
+ "lalrpop",
+ "lalrpop-util",
+ "regex",
+]
+
 [[package]]
 name = "basic-toml"
 version = "0.1.4"
@@ -689,6 +792,15 @@ dependencies = [
  "unicode-normalization",
 ]
 
+[[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"
@@ -862,7 +974,7 @@ version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
 dependencies = [
- "async-channel",
+ "async-channel 2.3.1",
  "async-task",
  "futures-io",
  "futures-lite",
@@ -1980,6 +2092,27 @@ dependencies = [
  "subtle",
 ]
 
+[[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-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
 [[package]]
 name = "dotenvy"
 version = "0.15.7"
@@ -2127,6 +2260,15 @@ dependencies = [
  "stable_deref_trait",
 ]
 
+[[package]]
+name = "ena"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5"
+dependencies = [
+ "log",
+]
+
 [[package]]
 name = "encode_unicode"
 version = "0.3.6"
@@ -2262,6 +2404,12 @@ dependencies = [
  "uint",
 ]
 
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
 [[package]]
 name = "event-listener"
 version = "4.0.3"
@@ -2622,7 +2770,7 @@ version = "3.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
 dependencies = [
- "gloo-timers",
+ "gloo-timers 0.2.6",
  "send_wrapper",
 ]
 
@@ -2758,6 +2906,18 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "gloo-timers"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "gloo-utils"
 version = "0.2.0"
@@ -3147,6 +3307,34 @@ version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
 
+[[package]]
+name = "httpmock"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08ec9586ee0910472dec1a1f0f8acf52f0fdde93aea74d70d4a3107b4be0fd5b"
+dependencies = [
+ "assert-json-diff",
+ "async-object-pool",
+ "async-std",
+ "async-trait",
+ "base64 0.21.5",
+ "basic-cookies",
+ "crossbeam-utils",
+ "form_urlencoded",
+ "futures-util",
+ "hyper 0.14.29",
+ "lazy_static",
+ "levenshtein",
+ "log",
+ "regex",
+ "serde",
+ "serde_json",
+ "serde_regex",
+ "similar",
+ "tokio",
+ "url",
+]
+
 [[package]]
 name = "hyper"
 version = "0.14.29"
@@ -3467,6 +3655,15 @@ 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 = "itertools"
 version = "0.12.0"
@@ -3835,6 +4032,46 @@ dependencies = [
  "cpufeatures",
 ]
 
+[[package]]
+name = "kv-log-macro"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "lalrpop"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca"
+dependencies = [
+ "ascii-canvas",
+ "bit-set",
+ "ena",
+ "itertools 0.11.0",
+ "lalrpop-util",
+ "petgraph",
+ "pico-args",
+ "regex",
+ "regex-syntax 0.8.2",
+ "string_cache",
+ "term",
+ "tiny-keccak 2.0.2",
+ "unicode-xid 0.2.4",
+ "walkdir",
+]
+
+[[package]]
+name = "lalrpop-util"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553"
+dependencies = [
+ "regex-automata 0.4.7",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.5.0"
@@ -3856,6 +4093,12 @@ version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
 
+[[package]]
+name = "levenshtein"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760"
+
 [[package]]
 name = "libc"
 version = "0.2.155"
@@ -3878,6 +4121,16 @@ version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
 
+[[package]]
+name = "libredox"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags 2.6.0",
+ "libc",
+]
+
 [[package]]
 name = "librocksdb-sys"
 version = "0.11.0+8.1.1"
@@ -4023,6 +4276,9 @@ name = "log"
 version = "0.4.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+dependencies = [
+ "value-bag",
+]
 
 [[package]]
 name = "logos"
@@ -4276,6 +4532,12 @@ dependencies = [
  "tempfile",
 ]
 
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
 [[package]]
 name = "nix"
 version = "0.27.1"
@@ -4884,6 +5146,21 @@ dependencies = [
  "indexmap 2.1.0",
 ]
 
+[[package]]
+name = "phf_shared"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+dependencies = [
+ "siphasher 0.3.11",
+]
+
+[[package]]
+name = "pico-args"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
+
 [[package]]
 name = "pin-project"
 version = "1.1.3"
@@ -5042,6 +5319,12 @@ 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 = "pretty_assertions"
 version = "1.4.0"
@@ -5504,6 +5787,17 @@ dependencies = [
  "bitflags 1.3.2",
 ]
 
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror",
+]
+
 [[package]]
 name = "regex"
 version = "1.10.6"
@@ -6481,6 +6775,16 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "serde_regex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf"
+dependencies = [
+ "regex",
+ "serde",
+]
+
 [[package]]
 name = "serde_urlencoded"
 version = "0.7.1"
@@ -6689,6 +6993,12 @@ dependencies = [
  "time",
 ]
 
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
 [[package]]
 name = "siphasher"
 version = "1.0.1"
@@ -6744,7 +7054,7 @@ version = "2.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f"
 dependencies = [
- "async-channel",
+ "async-channel 2.3.1",
  "async-executor",
  "async-fs",
  "async-io",
@@ -6800,7 +7110,7 @@ dependencies = [
  "serde_json",
  "sha2 0.10.8",
  "sha3 0.10.8",
- "siphasher",
+ "siphasher 1.0.1",
  "slab",
  "smallvec",
  "soketto 0.7.1",
@@ -6816,7 +7126,7 @@ version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5496f2d116b7019a526b1039ec2247dd172b8670633b1a64a614c9ea12c9d8c7"
 dependencies = [
- "async-channel",
+ "async-channel 2.3.1",
  "async-lock",
  "base64 0.21.5",
  "blake2-rfc",
@@ -6839,7 +7149,7 @@ dependencies = [
  "rand_chacha",
  "serde",
  "serde_json",
- "siphasher",
+ "siphasher 1.0.1",
  "slab",
  "smol",
  "smoldot",
@@ -7204,6 +7514,19 @@ 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",
+ "precomputed-hash",
+]
+
 [[package]]
 name = "stringprep"
 version = "0.1.4"
@@ -7524,6 +7847,17 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "term"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
+dependencies = [
+ "dirs-next",
+ "rustversion",
+ "winapi",
+]
+
 [[package]]
 name = "termcolor"
 version = "1.4.1"
@@ -8276,6 +8610,12 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
 
+[[package]]
+name = "value-bag"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101"
+
 [[package]]
 name = "vcpkg"
 version = "0.2.15"
@@ -9918,6 +10258,7 @@ dependencies = [
  "bigdecimal",
  "chrono",
  "fraction",
+ "httpmock",
  "rand 0.8.5",
  "reqwest 0.12.5",
  "serde",
diff --git a/Cargo.toml b/Cargo.toml
index 5a8a507b0340..0c83564cbf37 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -128,6 +128,7 @@ google-cloud-storage = "0.20.0"
 governor = "0.4.2"
 hex = "0.4"
 http = "1.1"
+httpmock = "0.7.0"
 hyper = "1.3"
 iai = "0.1"
 insta = "1.29.0"
diff --git a/core/lib/external_price_api/Cargo.toml b/core/lib/external_price_api/Cargo.toml
index 9539aa3fdc3c..3eee675b4e65 100644
--- a/core/lib/external_price_api/Cargo.toml
+++ b/core/lib/external_price_api/Cargo.toml
@@ -24,3 +24,4 @@ rand.workspace = true
 zksync_config.workspace = true
 zksync_types.workspace = true
 tokio.workspace = true
+httpmock.workspace = true
diff --git a/core/lib/external_price_api/src/coingecko_api.rs b/core/lib/external_price_api/src/coingecko_api.rs
index a710a610497d..258065c3fd3f 100644
--- a/core/lib/external_price_api/src/coingecko_api.rs
+++ b/core/lib/external_price_api/src/coingecko_api.rs
@@ -113,5 +113,22 @@ impl CoinGeckoPriceResponse {
 
 #[cfg(test)]
 mod test {
-    fn setup() {}
+    use httpmock::MockServer;
+
+    fn auth_check_create(
+        auth_header: String,
+        api_key: Option<String>,
+    ) -> impl (FnOnce(httpmock::When) -> httpmock::When) {
+        |when: httpmock::When| -> httpmock::When {
+            if let Some(x) = api_key {
+                when.header(auth_header, x)
+            } else {
+                when
+            }
+        }
+    }
+
+    fn setup() {
+        let server = MockServer::start();
+    }
 }
diff --git a/core/lib/external_price_api/src/lib.rs b/core/lib/external_price_api/src/lib.rs
index e86279dbe850..1f0dad0f7e4e 100644
--- a/core/lib/external_price_api/src/lib.rs
+++ b/core/lib/external_price_api/src/lib.rs
@@ -1,5 +1,6 @@
 pub mod coingecko_api;
 pub mod forced_price_client;
+mod tests;
 mod utils;
 
 use std::fmt;
diff --git a/core/lib/external_price_api/src/tests.rs b/core/lib/external_price_api/src/tests.rs
new file mode 100644
index 000000000000..7b1193aa3996
--- /dev/null
+++ b/core/lib/external_price_api/src/tests.rs
@@ -0,0 +1,132 @@
+#[cfg(test)]
+pub(crate) mod tests {
+    use std::{collections::HashMap, str::FromStr};
+
+    use bigdecimal::BigDecimal;
+    use chrono::Utc;
+    use httpmock::{Mock, MockServer};
+    use url::Url;
+    use zksync_types::{base_token_ratio::BaseTokenAPIRatio, Address};
+
+    use crate::{utils::get_fraction, PriceAPIClient};
+
+    const TIME_TOLERANCE_MS: i64 = 100;
+
+    pub(crate) type SetupFn = fn(
+        server: &MockServer,
+        api_key: Option<String>,
+        address: Address,
+        base_token_price: f64,
+    ) -> Box<dyn PriceAPIClient>;
+
+    pub(crate) fn server_url(server: &MockServer) -> Url {
+        Url::from_str(server.url("").as_str()).unwrap()
+    }
+
+    pub(crate) fn add_mock(
+        server: &MockServer,
+        method: httpmock::Method,
+        path: String,
+        query_params: HashMap<String, String>,
+        response_status: u16,
+        response_body: String,
+        auth_check: fn(when: httpmock::When) -> httpmock::When,
+    ) -> Mock {
+        server.mock(|mut when, then| {
+            when = when.method(method).path(path);
+
+            for (k, v) in &query_params {
+                when = when.query_param(k, v);
+            }
+            when = auth_check(when);
+            then.status(response_status).body(response_body);
+        })
+    }
+
+    pub(crate) async fn happy_day_test(api_key: Option<String>, setup: SetupFn) {
+        let server = MockServer::start();
+        let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"; //Uniswap (UNI)
+        let address = Address::from_str(address_str).unwrap();
+        let base_token_price = 198.9;
+
+        let mut client = setup(&server, api_key, address, base_token_price);
+        let api_price = client.fetch_ratio(address).await.unwrap();
+
+        let (numerator, denominator) = get_fraction(base_token_price);
+
+        assert_eq!(
+            BaseTokenAPIRatio {
+                numerator,
+                denominator,
+                ratio_timestamp: api_price.ratio_timestamp,
+            },
+            api_price
+        );
+        assert!((Utc::now() - api_price.ratio_timestamp).num_milliseconds() <= TIME_TOLERANCE_MS);
+    }
+
+    pub(crate) async fn no_eth_price_404_test(api_key: Option<String>, setup: SetupFn) {
+        let server = MockServer::start();
+        let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984";
+        let address = Address::from_str(address_str).unwrap();
+        let mut client = setup(&server, api_key, address, 1.0);
+        let api_price = client.fetch_ratio(address).await;
+
+        assert!(api_price.is_err());
+        assert!(api_price
+            .err()
+            .unwrap()
+            .to_string()
+            .starts_with("Http error while fetching token price. Status: 404 Not Found"))
+    }
+
+    pub(crate) async fn eth_price_not_found_test(api_key: Option<String>, setup: SetupFn) {
+        let server = MockServer::start();
+        let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984";
+        let address = Address::from_str("0x1f9840a85d5af5bf1d1762f925bdaddc4201f984").unwrap();
+
+        let mut client = setup(&server, api_key, address, 1.0);
+        let api_price = client
+            .fetch_ratio(Address::from_str(address_str).unwrap())
+            .await;
+
+        assert!(api_price.is_err());
+        assert!(api_price
+            .err()
+            .unwrap()
+            .to_string()
+            .starts_with("Price not found for token"))
+    }
+
+    pub(crate) async fn no_base_token_price_404_test(api_key: Option<String>, setup: SetupFn) {
+        let server = MockServer::start();
+        let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984";
+        let address = Address::from_str(address_str).unwrap();
+
+        let mut client = setup(&server, api_key, address, 1.0);
+        let api_price = client.fetch_ratio(address).await;
+
+        assert!(api_price.is_err());
+        assert!(api_price
+            .err()
+            .unwrap()
+            .to_string()
+            .starts_with("Http error while fetching token price. Status: 404 Not Found"))
+    }
+
+    pub(crate) async fn base_token_price_not_found_test(api_key: Option<String>, setup: SetupFn) {
+        let server = MockServer::start();
+        let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984";
+        let address = Address::from_str(address_str).unwrap();
+
+        let mut client = setup(&server, api_key, address, 1.0);
+        let api_price = client.fetch_ratio(address).await;
+
+        assert!(api_price.is_err());
+        assert!(api_price
+            .err()
+            .unwrap()
+            .to_string()
+            .starts_with("Price not found for token"))
+    }
+}

From ed35b68616006a74f8a6c1e44122903806693585 Mon Sep 17 00:00:00 2001
From: Artur Puzio <contact@puzio.waw.pl>
Date: Thu, 26 Sep 2024 14:33:42 +0200
Subject: [PATCH 04/11] feat: finished coingecko_api.rs tests

---
 .../external_price_api/src/coingecko_api.rs   | 169 ++++++++++++++++--
 core/lib/external_price_api/src/lib.rs        |   1 +
 core/lib/external_price_api/src/tests.rs      | 164 +++++------------
 core/lib/external_price_api/src/utils.rs      |   1 +
 4 files changed, 202 insertions(+), 133 deletions(-)

diff --git a/core/lib/external_price_api/src/coingecko_api.rs b/core/lib/external_price_api/src/coingecko_api.rs
index 258065c3fd3f..3c4181f3ee1b 100644
--- a/core/lib/external_price_api/src/coingecko_api.rs
+++ b/core/lib/external_price_api/src/coingecko_api.rs
@@ -114,21 +114,170 @@ impl CoinGeckoPriceResponse {
 #[cfg(test)]
 mod test {
     use httpmock::MockServer;
+    use zksync_config::configs::external_price_api_client::DEFAULT_TIMEOUT_MS;
 
-    fn auth_check_create(
-        auth_header: String,
+    use super::*;
+    use crate::tests::*;
+
+    fn auth_check(when: httpmock::When, api_key: Option<String>) -> httpmock::When {
+        if let Some(key) = api_key {
+            when.header(COINGECKO_AUTH_HEADER, key)
+        } else {
+            when
+        }
+    }
+
+    fn get_mock_response(address: &str, price: f64) -> String {
+        format!("{{\"{}\":{{\"eth\":{}}}}}", address, price)
+    }
+
+    #[test]
+    fn test_mock_response() {
+        // curl "https://api.coingecko.com/api/v3/simple/token_price/ethereum?contract_addresses=0x1f9840a85d5af5bf1d1762f925bdaddc4201f984&vs_currencies=eth"
+        // {"0x1f9840a85d5af5bf1d1762f925bdaddc4201f984":{"eth":0.00269512}}
+        assert_eq!(
+            get_mock_response("0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", 0.00269512),
+            r#"{"0x1f9840a85d5af5bf1d1762f925bdaddc4201f984":{"eth":0.00269512}}"#
+        )
+    }
+
+    fn add_mock_by_address(
+        server: &MockServer,
+        // use string explicitly to verify that conversion of the address to string works as expected
+        address: String,
+        price: Option<f64>,
         api_key: Option<String>,
-    ) -> impl (FnOnce(httpmock::When) -> httpmock::When) {
-        |when: httpmock::When| -> httpmock::When {
-            if let Some(x) = api_key {
-                when.header(auth_header, x)
+    ) {
+        server.mock(|mut when, then| {
+            when = when
+                .method(httpmock::Method::GET)
+                .path("/api/v3/simple/token_price/ethereum");
+
+            when = when.query_param("contract_addresses", address.clone());
+            when = when.query_param("vs_currencies", ETH_ID);
+            auth_check(when, api_key);
+
+            if let Some(p) = price {
+                then.status(200).body(get_mock_response(&address, p));
             } else {
-                when
-            }
+                // requesting with invalid/unknown address results in empty json
+                // example:
+                // $ curl "https://api.coingecko.com/api/v3/simple/token_price/ethereum?contract_addresses=0x000000000000000000000000000000000000dead&vs_currencies=eth"
+                // {}
+                then.status(200).body("{}");
+            };
+        });
+    }
+
+    fn get_config(base_url: String, api_key: Option<String>) -> ExternalPriceApiClientConfig {
+        ExternalPriceApiClientConfig {
+            base_url: Some(base_url),
+            api_key,
+            source: "FILLER".to_string(),
+            client_timeout_ms: DEFAULT_TIMEOUT_MS,
+            forced: None,
+        }
+    }
+
+    fn happy_day_setup(
+        api_key: Option<String>,
+        server: &MockServer,
+        address: Address,
+        base_token_price: f64,
+    ) -> SetupResult {
+        add_mock_by_address(
+            server,
+            address_to_string(&address),
+            Some(base_token_price),
+            api_key.clone(),
+        );
+        SetupResult {
+            client: Box::new(CoinGeckoPriceAPIClient::new(get_config(
+                server.url(""),
+                api_key,
+            ))),
+        }
+    }
+
+    fn happy_day_setup_with_key(
+        server: &MockServer,
+        address: Address,
+        base_token_price: f64,
+    ) -> SetupResult {
+        happy_day_setup(
+            Some("test-key".to_string()),
+            server,
+            address,
+            base_token_price,
+        )
+    }
+
+    #[tokio::test]
+    async fn test_happy_day_with_api_key() {
+        happy_day_test(happy_day_setup_with_key).await
+    }
+
+    fn happy_day_setup_no_key(
+        server: &MockServer,
+        address: Address,
+        base_token_price: f64,
+    ) -> SetupResult {
+        happy_day_setup(None, server, address, base_token_price)
+    }
+
+    #[tokio::test]
+    async fn test_happy_day_with_no_api_key() {
+        happy_day_test(happy_day_setup_no_key).await
+    }
+
+    fn error_404_setup(
+        server: &MockServer,
+        _address: Address,
+        _base_token_price: f64,
+    ) -> SetupResult {
+        // just don't add mock
+        SetupResult {
+            client: Box::new(CoinGeckoPriceAPIClient::new(get_config(
+                server.url(""),
+                Some("FILLER".to_string()),
+            ))),
+        }
+    }
+
+    #[tokio::test]
+    async fn test_error_404() {
+        let error_string = error_test(error_404_setup).await.to_string();
+        assert!(
+            error_string
+                .starts_with("Http error while fetching token price. Status: 404 Not Found"),
+            "Error was: {}",
+            &error_string
+        )
+    }
+
+    fn error_missing_setup(
+        server: &MockServer,
+        address: Address,
+        _base_token_price: f64,
+    ) -> SetupResult {
+        let api_key = Some("FILLER".to_string());
+
+        add_mock_by_address(server, address_to_string(&address), None, api_key.clone());
+        SetupResult {
+            client: Box::new(CoinGeckoPriceAPIClient::new(get_config(
+                server.url(""),
+                api_key,
+            ))),
         }
     }
 
-    fn setup() {
-        let server = MockServer::start();
+    #[tokio::test]
+    async fn test_error_missing() {
+        let error_string = error_test(error_missing_setup).await.to_string();
+        assert!(
+            error_string.starts_with("Price not found for token"),
+            "Error was: {}",
+            error_string
+        )
     }
 }
diff --git a/core/lib/external_price_api/src/lib.rs b/core/lib/external_price_api/src/lib.rs
index 1f0dad0f7e4e..9bc54c771d30 100644
--- a/core/lib/external_price_api/src/lib.rs
+++ b/core/lib/external_price_api/src/lib.rs
@@ -1,5 +1,6 @@
 pub mod coingecko_api;
 pub mod forced_price_client;
+#[cfg(test)]
 mod tests;
 mod utils;
 
diff --git a/core/lib/external_price_api/src/tests.rs b/core/lib/external_price_api/src/tests.rs
index 7b1193aa3996..b06d90be9b40 100644
--- a/core/lib/external_price_api/src/tests.rs
+++ b/core/lib/external_price_api/src/tests.rs
@@ -1,132 +1,50 @@
-#[cfg(test)]
-pub(crate) mod tests {
-    use std::{collections::HashMap, str::FromStr};
+use std::str::FromStr;
 
-    use bigdecimal::BigDecimal;
-    use chrono::Utc;
-    use httpmock::{Mock, MockServer};
-    use url::Url;
-    use zksync_types::{base_token_ratio::BaseTokenAPIRatio, Address};
+use chrono::Utc;
+use httpmock::MockServer;
+use zksync_types::{base_token_ratio::BaseTokenAPIRatio, Address};
 
-    use crate::{utils::get_fraction, PriceAPIClient};
+use crate::{utils::get_fraction, PriceAPIClient};
 
-    const TIME_TOLERANCE_MS: i64 = 100;
+const TIME_TOLERANCE_MS: i64 = 100;
 
-    pub(crate) type SetupFn = fn(
-        server: &MockServer,
-        api_key: Option<String>,
-        address: Address,
-        base_token_price: f64,
-    ) -> Box<dyn PriceAPIClient>;
-
-    pub(crate) fn server_url(server: &MockServer) -> Url {
-        Url::from_str(server.url("").as_str()).unwrap()
-    }
-
-    pub(crate) fn add_mock(
-        server: &MockServer,
-        method: httpmock::Method,
-        path: String,
-        query_params: HashMap<String, String>,
-        response_status: u16,
-        response_body: String,
-        auth_check: fn(when: httpmock::When) -> httpmock::When,
-    ) -> Mock {
-        server.mock(|mut when, then| {
-            when = when.method(method).path(path);
-
-            for (k, v) in &query_params {
-                when = when.query_param(k, v);
-            }
-            when = auth_check(when);
-            then.status(response_status).body(response_body);
-        })
-    }
-
-    pub(crate) async fn happy_day_test(api_key: Option<String>, setup: SetupFn) {
-        let server = MockServer::start();
-        let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"; //Uniswap (UNI)
-        let address = Address::from_str(address_str).unwrap();
-        let base_token_price = 198.9;
-
-        let mut client = setup(&server, api_key, address, base_token_price);
-        let api_price = client.fetch_ratio(address).await.unwrap();
-
-        let (numerator, denominator) = get_fraction(base_token_price);
-
-        assert_eq!(
-            BaseTokenAPIRatio {
-                numerator,
-                denominator,
-                ratio_timestamp: api_price.ratio_timestamp,
-            },
-            api_price
-        );
-        assert!((Utc::now() - api_price.ratio_timestamp).num_milliseconds() <= TIME_TOLERANCE_MS);
-    }
-
-    pub(crate) async fn no_eth_price_404_test(api_key: Option<String>, setup: SetupFn) {
-        let server = MockServer::start();
-        let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984";
-        let address = Address::from_str(address_str).unwrap();
-        let mut client = setup(&server, api_key, address, 1.0);
-        let api_price = client.fetch_ratio(address).await;
-
-        assert!(api_price.is_err());
-        assert!(api_price
-            .err()
-            .unwrap()
-            .to_string()
-            .starts_with("Http error while fetching token price. Status: 404 Not Found"))
-    }
-
-    pub(crate) async fn eth_price_not_found_test(api_key: Option<String>, setup: SetupFn) {
-        let server = MockServer::start();
-        let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984";
-        let address = Address::from_str("0x1f9840a85d5af5bf1d1762f925bdaddc4201f984").unwrap();
-
-        let mut client = setup(&server, api_key, address, 1.0);
-        let api_price = client
-            .fetch_ratio(Address::from_str(address_str).unwrap())
-            .await;
-
-        assert!(api_price.is_err());
-        assert!(api_price
-            .err()
-            .unwrap()
-            .to_string()
-            .starts_with("Price not found for token"))
-    }
-
-    pub(crate) async fn no_base_token_price_404_test(api_key: Option<String>, setup: SetupFn) {
-        let server = MockServer::start();
-        let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984";
-        let address = Address::from_str(address_str).unwrap();
-
-        let mut client = setup(&server, api_key, address, 1.0);
-        let api_price = client.fetch_ratio(address).await;
+pub(crate) struct SetupResult {
+    pub(crate) client: Box<dyn PriceAPIClient>,
+}
 
-        assert!(api_price.is_err());
-        assert!(api_price
-            .err()
-            .unwrap()
-            .to_string()
-            .starts_with("Http error while fetching token price. Status: 404 Not Found"))
-    }
+pub(crate) type SetupFn =
+    fn(server: &MockServer, address: Address, base_token_price: f64) -> SetupResult;
+
+pub(crate) async fn happy_day_test(setup: SetupFn) {
+    let server = MockServer::start();
+    let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"; //Uniswap (UNI)
+    let address = Address::from_str(address_str).unwrap();
+    let base_token_price = 198.9;
+
+    let SetupResult { client } = setup(&server, address, base_token_price);
+    let api_price = client.fetch_ratio(address).await.unwrap();
+
+    let (numerator, denominator) = get_fraction(base_token_price);
+
+    assert_eq!(
+        BaseTokenAPIRatio {
+            numerator,
+            denominator,
+            ratio_timestamp: api_price.ratio_timestamp,
+        },
+        api_price
+    );
+    assert!((Utc::now() - api_price.ratio_timestamp).num_milliseconds() <= TIME_TOLERANCE_MS);
+}
 
-    pub(crate) async fn base_token_price_not_found_test(api_key: Option<String>, setup: SetupFn) {
-        let server = MockServer::start();
-        let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984";
-        let address = Address::from_str(address_str).unwrap();
+pub(crate) async fn error_test(setup: SetupFn) -> anyhow::Error {
+    let server = MockServer::start();
+    let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984";
+    let address = Address::from_str(address_str).unwrap();
 
-        let mut client = setup(&server, api_key, address, 1.0);
-        let api_price = client.fetch_ratio(address).await;
+    let SetupResult { client } = setup(&server, address, 1.0);
+    let api_price = client.fetch_ratio(address).await;
 
-        assert!(api_price.is_err());
-        assert!(api_price
-            .err()
-            .unwrap()
-            .to_string()
-            .starts_with("Price not found for token"))
-    }
+    assert!(api_price.is_err());
+    api_price.err().unwrap()
 }
diff --git a/core/lib/external_price_api/src/utils.rs b/core/lib/external_price_api/src/utils.rs
index f6d00b080fee..c285a9095c68 100644
--- a/core/lib/external_price_api/src/utils.rs
+++ b/core/lib/external_price_api/src/utils.rs
@@ -28,6 +28,7 @@ pub(crate) mod tests {
         );
     }
 
+    #[allow(clippy::approx_constant)]
     #[test]
     fn test_float_to_fraction_conversion_as_expected() {
         assert_get_fraction_value(1.0, 1, 1);

From c65450645b07278944b18dd367aeec4ea8b38c14 Mon Sep 17 00:00:00 2001
From: Artur Puzio <contact@puzio.waw.pl>
Date: Thu, 26 Sep 2024 16:06:19 +0200
Subject: [PATCH 05/11] fix: ignore atty advisory

---
 deny.toml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/deny.toml b/deny.toml
index c2775fc057c8..559076a575bc 100644
--- a/deny.toml
+++ b/deny.toml
@@ -8,6 +8,7 @@ feature-depth = 1
 
 [advisories]
 ignore = [
+    "RUSTSEC-2024-0375", # atty dependency being unmaintained, dependency of clap and criterion, we would need to update to newer major of dependencies
     "RUSTSEC-2024-0320", # yaml_rust dependency being unmaintained, dependency in core, we should consider moving to yaml_rust2 fork
     "RUSTSEC-2020-0168", # mach dependency being unmaintained, dependency in consensus, we should consider moving to mach2 fork
     "RUSTSEC-2024-0370", # `cs_derive` needs to be updated to not rely on `proc-macro-error`

From 3b219d08f594efd1b9eac77786808a94a301d486 Mon Sep 17 00:00:00 2001
From: Artur Puzio <contact@puzio.waw.pl>
Date: Thu, 26 Sep 2024 16:36:46 +0200
Subject: [PATCH 06/11] fix: conversion from price to ratio reciprocal error

---
 core/lib/external_price_api/src/coingecko_api.rs       |  9 ++++++---
 core/lib/external_price_api/src/forced_price_client.rs |  2 +-
 core/lib/external_price_api/src/lib.rs                 |  1 +
 core/lib/external_price_api/src/tests.rs               | 10 ++++++----
 4 files changed, 14 insertions(+), 8 deletions(-)

diff --git a/core/lib/external_price_api/src/coingecko_api.rs b/core/lib/external_price_api/src/coingecko_api.rs
index 3c4181f3ee1b..18ac9f175c51 100644
--- a/core/lib/external_price_api/src/coingecko_api.rs
+++ b/core/lib/external_price_api/src/coingecko_api.rs
@@ -49,6 +49,7 @@ impl CoinGeckoPriceAPIClient {
         }
     }
 
+    /// returns ETH/BaseToken price of a token by address
     async fn get_token_price_by_address(&self, address: Address) -> anyhow::Result<f64> {
         let address_str = address_to_string(&address);
         let price_url = self
@@ -87,11 +88,13 @@ impl CoinGeckoPriceAPIClient {
 impl PriceAPIClient for CoinGeckoPriceAPIClient {
     async fn fetch_ratio(&self, token_address: Address) -> anyhow::Result<BaseTokenAPIRatio> {
         let base_token_in_eth = self.get_token_price_by_address(token_address).await?;
-        let (numerator, denominator) = get_fraction(base_token_in_eth);
+        let (num_in_eth, denom_in_eth) = get_fraction(base_token_in_eth);
+        // take reciprocal of price as returned price is ETH/BaseToken and BaseToken/ETH is needed
+        let (num_in_base, denom_in_base) = (denom_in_eth, num_in_eth);
 
         return Ok(BaseTokenAPIRatio {
-            numerator,
-            denominator,
+            numerator: num_in_base,
+            denominator: denom_in_base,
             ratio_timestamp: Utc::now(),
         });
     }
diff --git a/core/lib/external_price_api/src/forced_price_client.rs b/core/lib/external_price_api/src/forced_price_client.rs
index fd166cdfd2da..a18c03fd8cab 100644
--- a/core/lib/external_price_api/src/forced_price_client.rs
+++ b/core/lib/external_price_api/src/forced_price_client.rs
@@ -7,7 +7,7 @@ use zksync_types::{base_token_ratio::BaseTokenAPIRatio, Address};
 
 use crate::PriceAPIClient;
 
-// Struct for a a forced price "client" (conversion ratio is always a configured "forced" ratio).
+// Struct for a forced price "client" (conversion ratio is always a configured "forced" ratio).
 #[derive(Debug, Clone)]
 pub struct ForcedPriceClient {
     ratio: BaseTokenAPIRatio,
diff --git a/core/lib/external_price_api/src/lib.rs b/core/lib/external_price_api/src/lib.rs
index 9bc54c771d30..ff92c2128fe2 100644
--- a/core/lib/external_price_api/src/lib.rs
+++ b/core/lib/external_price_api/src/lib.rs
@@ -13,6 +13,7 @@ use zksync_types::{base_token_ratio::BaseTokenAPIRatio, Address};
 #[async_trait]
 pub trait PriceAPIClient: Sync + Send + fmt::Debug + 'static {
     /// Returns the BaseToken<->ETH ratio for the input token address.
+    /// The returned unit is BaseToken/ETH. Example if 1 BaseToken = 0.002 ETH, then ratio is 500/1
     async fn fetch_ratio(&self, token_address: Address) -> anyhow::Result<BaseTokenAPIRatio>;
 }
 
diff --git a/core/lib/external_price_api/src/tests.rs b/core/lib/external_price_api/src/tests.rs
index b06d90be9b40..896d2566e840 100644
--- a/core/lib/external_price_api/src/tests.rs
+++ b/core/lib/external_price_api/src/tests.rs
@@ -19,17 +19,19 @@ pub(crate) async fn happy_day_test(setup: SetupFn) {
     let server = MockServer::start();
     let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"; //Uniswap (UNI)
     let address = Address::from_str(address_str).unwrap();
-    let base_token_price = 198.9;
+    let base_token_price = 0.00269; //ETH costs one token
 
     let SetupResult { client } = setup(&server, address, base_token_price);
     let api_price = client.fetch_ratio(address).await.unwrap();
 
-    let (numerator, denominator) = get_fraction(base_token_price);
+    let (num_in_eth, denom_in_eth) = get_fraction(base_token_price);
+    let (ratio_num, ratio_denom) = (denom_in_eth, num_in_eth);
+    assert!(((ratio_num.get() as f64) / (ratio_denom.get() as f64) - 371.74).abs() < 0.1);
 
     assert_eq!(
         BaseTokenAPIRatio {
-            numerator,
-            denominator,
+            numerator: ratio_num,
+            denominator: ratio_denom,
             ratio_timestamp: api_price.ratio_timestamp,
         },
         api_price

From c6ddc1aa50908fb8399ba26fbda8a1bf746bae59 Mon Sep 17 00:00:00 2001
From: Artur Puzio <contact@puzio.waw.pl>
Date: Fri, 27 Sep 2024 14:07:24 +0200
Subject: [PATCH 07/11] fix: review feedback

---
 .../external_price_api/src/coingecko_api.rs   | 52 +++++++------------
 core/lib/external_price_api/src/tests.rs      | 38 ++++++++------
 core/lib/external_price_api/src/utils.rs      | 31 ++++++++++-
 3 files changed, 69 insertions(+), 52 deletions(-)

diff --git a/core/lib/external_price_api/src/coingecko_api.rs b/core/lib/external_price_api/src/coingecko_api.rs
index 18ac9f175c51..41ae88209c91 100644
--- a/core/lib/external_price_api/src/coingecko_api.rs
+++ b/core/lib/external_price_api/src/coingecko_api.rs
@@ -122,14 +122,6 @@ mod test {
     use super::*;
     use crate::tests::*;
 
-    fn auth_check(when: httpmock::When, api_key: Option<String>) -> httpmock::When {
-        if let Some(key) = api_key {
-            when.header(COINGECKO_AUTH_HEADER, key)
-        } else {
-            when
-        }
-    }
-
     fn get_mock_response(address: &str, price: f64) -> String {
         format!("{{\"{}\":{{\"eth\":{}}}}}", address, price)
     }
@@ -158,7 +150,7 @@ mod test {
 
             when = when.query_param("contract_addresses", address.clone());
             when = when.query_param("vs_currencies", ETH_ID);
-            auth_check(when, api_key);
+            api_key.map(|key| when.header(COINGECKO_AUTH_HEADER, key));
 
             if let Some(p) = price {
                 then.status(200).body(get_mock_response(&address, p));
@@ -176,7 +168,7 @@ mod test {
         ExternalPriceApiClientConfig {
             base_url: Some(base_url),
             api_key,
-            source: "FILLER".to_string(),
+            source: "coingecko".to_string(),
             client_timeout_ms: DEFAULT_TIMEOUT_MS,
             forced: None,
         }
@@ -202,35 +194,29 @@ mod test {
         }
     }
 
-    fn happy_day_setup_with_key(
-        server: &MockServer,
-        address: Address,
-        base_token_price: f64,
-    ) -> SetupResult {
-        happy_day_setup(
-            Some("test-key".to_string()),
-            server,
-            address,
-            base_token_price,
-        )
-    }
-
     #[tokio::test]
     async fn test_happy_day_with_api_key() {
-        happy_day_test(happy_day_setup_with_key).await
-    }
-
-    fn happy_day_setup_no_key(
-        server: &MockServer,
-        address: Address,
-        base_token_price: f64,
-    ) -> SetupResult {
-        happy_day_setup(None, server, address, base_token_price)
+        happy_day_test(
+            |server: &MockServer, address: Address, base_token_price: f64| {
+                happy_day_setup(
+                    Some("test-key".to_string()),
+                    server,
+                    address,
+                    base_token_price,
+                )
+            },
+        )
+        .await
     }
 
     #[tokio::test]
     async fn test_happy_day_with_no_api_key() {
-        happy_day_test(happy_day_setup_no_key).await
+        happy_day_test(
+            |server: &MockServer, address: Address, base_token_price: f64| {
+                happy_day_setup(None, server, address, base_token_price)
+            },
+        )
+        .await
     }
 
     fn error_404_setup(
diff --git a/core/lib/external_price_api/src/tests.rs b/core/lib/external_price_api/src/tests.rs
index 896d2566e840..bb2af866cf5f 100644
--- a/core/lib/external_price_api/src/tests.rs
+++ b/core/lib/external_price_api/src/tests.rs
@@ -2,11 +2,19 @@ use std::str::FromStr;
 
 use chrono::Utc;
 use httpmock::MockServer;
-use zksync_types::{base_token_ratio::BaseTokenAPIRatio, Address};
+use zksync_types::Address;
 
-use crate::{utils::get_fraction, PriceAPIClient};
+use crate::PriceAPIClient;
 
 const TIME_TOLERANCE_MS: i64 = 100;
+/// Uniswap (UNI)
+const TEST_TOKEN_ADDRESS: &str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984";
+/// 1UNI = 0.00269ETH
+const TEST_TOKEN_PRICE_ETH: f64 = 0.00269;
+/// 1ETH = 371.74UNI; When converting gas price from ETH to UNI
+/// you need to multiply by this value. Thus, this should be equal to the ratio.
+const TEST_BASE_PRICE: f64 = 371.74;
+const PRICE_FLOAT_COMPARE_TOLERANCE: f64 = 0.1;
 
 pub(crate) struct SetupResult {
     pub(crate) client: Box<dyn PriceAPIClient>,
@@ -17,31 +25,27 @@ pub(crate) type SetupFn =
 
 pub(crate) async fn happy_day_test(setup: SetupFn) {
     let server = MockServer::start();
-    let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"; //Uniswap (UNI)
+    let address_str = TEST_TOKEN_ADDRESS;
     let address = Address::from_str(address_str).unwrap();
-    let base_token_price = 0.00269; //ETH costs one token
 
-    let SetupResult { client } = setup(&server, address, base_token_price);
+    // APIs return token price in ETH (ETH per 1 token)
+    let SetupResult { client } = setup(&server, address, TEST_TOKEN_PRICE_ETH);
     let api_price = client.fetch_ratio(address).await.unwrap();
 
-    let (num_in_eth, denom_in_eth) = get_fraction(base_token_price);
-    let (ratio_num, ratio_denom) = (denom_in_eth, num_in_eth);
-    assert!(((ratio_num.get() as f64) / (ratio_denom.get() as f64) - 371.74).abs() < 0.1);
-
-    assert_eq!(
-        BaseTokenAPIRatio {
-            numerator: ratio_num,
-            denominator: ratio_denom,
-            ratio_timestamp: api_price.ratio_timestamp,
-        },
-        api_price
+    // we expect the returned ratio to be such that when multiplying gas price in ETH you get gas
+    // price in base token. So we expect such ratio X that X Base = 1ETH
+    assert!(
+        ((api_price.numerator.get() as f64) / (api_price.denominator.get() as f64)
+            - TEST_BASE_PRICE)
+            .abs()
+            < PRICE_FLOAT_COMPARE_TOLERANCE
     );
     assert!((Utc::now() - api_price.ratio_timestamp).num_milliseconds() <= TIME_TOLERANCE_MS);
 }
 
 pub(crate) async fn error_test(setup: SetupFn) -> anyhow::Error {
     let server = MockServer::start();
-    let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984";
+    let address_str = TEST_TOKEN_ADDRESS;
     let address = Address::from_str(address_str).unwrap();
 
     let SetupResult { client } = setup(&server, address, 1.0);
diff --git a/core/lib/external_price_api/src/utils.rs b/core/lib/external_price_api/src/utils.rs
index c285a9095c68..9c5e8f18aa15 100644
--- a/core/lib/external_price_api/src/utils.rs
+++ b/core/lib/external_price_api/src/utils.rs
@@ -5,10 +5,13 @@ use fraction::Fraction;
 /// Using the base token price and eth price, calculate the fraction of the base token to eth.
 pub fn get_fraction(ratio_f64: f64) -> (NonZeroU64, NonZeroU64) {
     let rate_fraction = Fraction::from(ratio_f64);
+    if rate_fraction.sign() == Some(fraction::Sign::Minus) {
+        panic!("number is negative");
+    }
 
-    let numerator = NonZeroU64::new(*rate_fraction.numer().expect("numerator is empty"))
+    let numerator = NonZeroU64::new(*rate_fraction.numer().expect("number is not rational"))
         .expect("numerator is zero");
-    let denominator = NonZeroU64::new(*rate_fraction.denom().expect("denominator is empty"))
+    let denominator = NonZeroU64::new(*rate_fraction.denom().expect("number is not rational"))
         .expect("denominator is zero");
 
     (numerator, denominator)
@@ -42,4 +45,28 @@ pub(crate) mod tests {
         assert_get_fraction_value(0.5, 1, 2);
         assert_get_fraction_value(3.1415, 6283, 2000);
     }
+
+    #[should_panic(expected = "numerator is zero")]
+    #[test]
+    fn test_zero_panics() {
+        get_fraction(0.0);
+    }
+
+    #[should_panic(expected = "number is negative")]
+    #[test]
+    fn test_negative() {
+        get_fraction(-1.0);
+    }
+
+    #[should_panic(expected = "number is not rational")]
+    #[test]
+    fn test_nan() {
+        get_fraction(f64::NAN);
+    }
+
+    #[should_panic(expected = "number is not rational")]
+    #[test]
+    fn test_infinity() {
+        get_fraction(f64::INFINITY);
+    }
 }

From e005f6c1a72afe7065cd7b80af721a4ce339f1db Mon Sep 17 00:00:00 2001
From: Artur Puzio <contact@puzio.waw.pl>
Date: Fri, 27 Sep 2024 14:22:04 +0200
Subject: [PATCH 08/11] docs: better explanation of ratio value

---
 core/lib/external_price_api/src/coingecko_api.rs | 2 +-
 core/lib/external_price_api/src/lib.rs           | 3 ++-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/core/lib/external_price_api/src/coingecko_api.rs b/core/lib/external_price_api/src/coingecko_api.rs
index 41ae88209c91..f383af35766f 100644
--- a/core/lib/external_price_api/src/coingecko_api.rs
+++ b/core/lib/external_price_api/src/coingecko_api.rs
@@ -49,7 +49,7 @@ impl CoinGeckoPriceAPIClient {
         }
     }
 
-    /// returns ETH/BaseToken price of a token by address
+    /// returns token price in ETH by token address. Returned value is X such that 1 TOKEN = X ETH.
     async fn get_token_price_by_address(&self, address: Address) -> anyhow::Result<f64> {
         let address_str = address_to_string(&address);
         let price_url = self
diff --git a/core/lib/external_price_api/src/lib.rs b/core/lib/external_price_api/src/lib.rs
index ff92c2128fe2..7a068f9b1cb5 100644
--- a/core/lib/external_price_api/src/lib.rs
+++ b/core/lib/external_price_api/src/lib.rs
@@ -13,7 +13,8 @@ use zksync_types::{base_token_ratio::BaseTokenAPIRatio, Address};
 #[async_trait]
 pub trait PriceAPIClient: Sync + Send + fmt::Debug + 'static {
     /// Returns the BaseToken<->ETH ratio for the input token address.
-    /// The returned unit is BaseToken/ETH. Example if 1 BaseToken = 0.002 ETH, then ratio is 500/1
+    /// The returned value is rational number X such that X BaseToken = 1 ETH.
+    /// Example if 1 BaseToken = 0.002 ETH, then ratio is 500/1 (500 BaseToken = 1ETH)
     async fn fetch_ratio(&self, token_address: Address) -> anyhow::Result<BaseTokenAPIRatio>;
 }
 

From 408fba4f0581deaef83cd7bebc237434d830555a Mon Sep 17 00:00:00 2001
From: Artur Puzio <contact@puzio.waw.pl>
Date: Fri, 27 Sep 2024 17:23:48 +0200
Subject: [PATCH 09/11] fix: error instead of panic on bad price

---
 .../external_price_api/src/coingecko_api.rs   |  2 +-
 core/lib/external_price_api/src/utils.rs      | 52 +++++++++++++------
 2 files changed, 37 insertions(+), 17 deletions(-)

diff --git a/core/lib/external_price_api/src/coingecko_api.rs b/core/lib/external_price_api/src/coingecko_api.rs
index f383af35766f..cc882db95c36 100644
--- a/core/lib/external_price_api/src/coingecko_api.rs
+++ b/core/lib/external_price_api/src/coingecko_api.rs
@@ -88,7 +88,7 @@ impl CoinGeckoPriceAPIClient {
 impl PriceAPIClient for CoinGeckoPriceAPIClient {
     async fn fetch_ratio(&self, token_address: Address) -> anyhow::Result<BaseTokenAPIRatio> {
         let base_token_in_eth = self.get_token_price_by_address(token_address).await?;
-        let (num_in_eth, denom_in_eth) = get_fraction(base_token_in_eth);
+        let (num_in_eth, denom_in_eth) = get_fraction(base_token_in_eth)?;
         // take reciprocal of price as returned price is ETH/BaseToken and BaseToken/ETH is needed
         let (num_in_base, denom_in_base) = (denom_in_eth, num_in_eth);
 
diff --git a/core/lib/external_price_api/src/utils.rs b/core/lib/external_price_api/src/utils.rs
index 9c5e8f18aa15..90a0533ac629 100644
--- a/core/lib/external_price_api/src/utils.rs
+++ b/core/lib/external_price_api/src/utils.rs
@@ -3,18 +3,26 @@ use std::num::NonZeroU64;
 use fraction::Fraction;
 
 /// Using the base token price and eth price, calculate the fraction of the base token to eth.
-pub fn get_fraction(ratio_f64: f64) -> (NonZeroU64, NonZeroU64) {
+pub fn get_fraction(ratio_f64: f64) -> anyhow::Result<(NonZeroU64, NonZeroU64)> {
     let rate_fraction = Fraction::from(ratio_f64);
     if rate_fraction.sign() == Some(fraction::Sign::Minus) {
-        panic!("number is negative");
+        return Err(anyhow::anyhow!("number is negative"));
     }
 
-    let numerator = NonZeroU64::new(*rate_fraction.numer().expect("number is not rational"))
-        .expect("numerator is zero");
-    let denominator = NonZeroU64::new(*rate_fraction.denom().expect("number is not rational"))
-        .expect("denominator is zero");
+    let numerator = NonZeroU64::new(
+        *rate_fraction
+            .numer()
+            .ok_or(anyhow::anyhow!("number is not rational"))?,
+    )
+    .ok_or(anyhow::anyhow!("numerator is zero"))?;
+    let denominator = NonZeroU64::new(
+        *rate_fraction
+            .denom()
+            .ok_or(anyhow::anyhow!("number is not rational"))?,
+    )
+    .ok_or(anyhow::anyhow!("denominator is zero"))?;
 
-    (numerator, denominator)
+    Ok((numerator, denominator))
 }
 
 #[cfg(test)]
@@ -23,7 +31,7 @@ pub(crate) mod tests {
 
     fn assert_get_fraction_value(f: f64, num: u64, denum: u64) {
         assert_eq!(
-            get_fraction(f),
+            get_fraction(f).unwrap(),
             (
                 NonZeroU64::try_from(num).unwrap(),
                 NonZeroU64::try_from(denum).unwrap()
@@ -46,27 +54,39 @@ pub(crate) mod tests {
         assert_get_fraction_value(3.1415, 6283, 2000);
     }
 
-    #[should_panic(expected = "numerator is zero")]
     #[test]
     fn test_zero_panics() {
-        get_fraction(0.0);
+        assert_eq!(
+            get_fraction(0.0).expect_err("did not error").to_string(),
+            "numerator is zero"
+        );
     }
 
-    #[should_panic(expected = "number is negative")]
     #[test]
     fn test_negative() {
-        get_fraction(-1.0);
+        assert_eq!(
+            get_fraction(-1.0).expect_err("did not error").to_string(),
+            "number is negative"
+        );
     }
 
-    #[should_panic(expected = "number is not rational")]
     #[test]
     fn test_nan() {
-        get_fraction(f64::NAN);
+        assert_eq!(
+            get_fraction(f64::NAN)
+                .expect_err("did not error")
+                .to_string(),
+            "number is not rational"
+        );
     }
 
-    #[should_panic(expected = "number is not rational")]
     #[test]
     fn test_infinity() {
-        get_fraction(f64::INFINITY);
+        assert_eq!(
+            get_fraction(f64::INFINITY)
+                .expect_err("did not error")
+                .to_string(),
+            "number is not rational"
+        );
     }
 }

From 68e8b2ddf4996d080493624f05a2c098177fe1e6 Mon Sep 17 00:00:00 2001
From: Artur Puzio <contact@puzio.waw.pl>
Date: Fri, 27 Sep 2024 17:25:33 +0200
Subject: [PATCH 10/11] better formatting

---
 core/lib/external_price_api/src/utils.rs | 14 +-------------
 1 file changed, 1 insertion(+), 13 deletions(-)

diff --git a/core/lib/external_price_api/src/utils.rs b/core/lib/external_price_api/src/utils.rs
index 90a0533ac629..4b8abc39dff2 100644
--- a/core/lib/external_price_api/src/utils.rs
+++ b/core/lib/external_price_api/src/utils.rs
@@ -55,33 +55,21 @@ pub(crate) mod tests {
     }
 
     #[test]
-    fn test_zero_panics() {
+    fn test_to_fraction_bad_inputs() {
         assert_eq!(
             get_fraction(0.0).expect_err("did not error").to_string(),
             "numerator is zero"
         );
-    }
-
-    #[test]
-    fn test_negative() {
         assert_eq!(
             get_fraction(-1.0).expect_err("did not error").to_string(),
             "number is negative"
         );
-    }
-
-    #[test]
-    fn test_nan() {
         assert_eq!(
             get_fraction(f64::NAN)
                 .expect_err("did not error")
                 .to_string(),
             "number is not rational"
         );
-    }
-
-    #[test]
-    fn test_infinity() {
         assert_eq!(
             get_fraction(f64::INFINITY)
                 .expect_err("did not error")

From 71cd367e7ac8cf0b9f573cde52397b4c7cb01b3a Mon Sep 17 00:00:00 2001
From: Artur Puzio <contact@puzio.waw.pl>
Date: Tue, 1 Oct 2024 15:09:49 +0200
Subject: [PATCH 11/11] Update Cargo.lock

---
 Cargo.lock | 355 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 348 insertions(+), 7 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 9425ad8e238f..646732a19759 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -234,12 +234,52 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
 
+[[package]]
+name = "ascii-canvas"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6"
+dependencies = [
+ "term",
+]
+
+[[package]]
+name = "assert-json-diff"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "assert_matches"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
 
+[[package]]
+name = "async-attributes"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5"
+dependencies = [
+ "quote 1.0.37",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener 2.5.3",
+ "futures-core",
+]
+
 [[package]]
 name = "async-channel"
 version = "2.3.1"
@@ -276,6 +316,21 @@ dependencies = [
  "futures-lite",
 ]
 
+[[package]]
+name = "async-global-executor"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
+dependencies = [
+ "async-channel 2.3.1",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "blocking",
+ "futures-lite",
+ "once_cell",
+]
+
 [[package]]
 name = "async-io"
 version = "2.3.4"
@@ -317,13 +372,22 @@ dependencies = [
  "futures-lite",
 ]
 
+[[package]]
+name = "async-object-pool"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "333c456b97c3f2d50604e8b2624253b7f787208cb72eb75e64b0ad11b221652c"
+dependencies = [
+ "async-std",
+]
+
 [[package]]
 name = "async-process"
 version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
 dependencies = [
- "async-channel",
+ "async-channel 2.3.1",
  "async-io",
  "async-lock",
  "async-signal",
@@ -365,6 +429,34 @@ dependencies = [
  "windows-sys 0.59.0",
 ]
 
+[[package]]
+name = "async-std"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615"
+dependencies = [
+ "async-attributes",
+ "async-channel 1.9.0",
+ "async-global-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "crossbeam-utils",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-lite",
+ "gloo-timers 0.3.0",
+ "kv-log-macro",
+ "log",
+ "memchr",
+ "once_cell",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen-futures",
+]
+
 [[package]]
 name = "async-stream"
 version = "0.3.5"
@@ -594,6 +686,17 @@ version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
 
+[[package]]
+name = "basic-cookies"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67bd8fd42c16bdb08688243dc5f0cc117a3ca9efeeaba3a345a18a6159ad96f7"
+dependencies = [
+ "lalrpop",
+ "lalrpop-util",
+ "regex",
+]
+
 [[package]]
 name = "beef"
 version = "0.5.2"
@@ -680,6 +783,15 @@ dependencies = [
  "unicode-normalization",
 ]
 
+[[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"
@@ -853,7 +965,7 @@ version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
 dependencies = [
- "async-channel",
+ "async-channel 2.3.1",
  "async-task",
  "futures-io",
  "futures-lite",
@@ -1959,6 +2071,27 @@ dependencies = [
  "subtle",
 ]
 
+[[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-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
 [[package]]
 name = "dotenvy"
 version = "0.15.7"
@@ -2107,6 +2240,15 @@ dependencies = [
  "stable_deref_trait",
 ]
 
+[[package]]
+name = "ena"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5"
+dependencies = [
+ "log",
+]
+
 [[package]]
 name = "encode_unicode"
 version = "0.3.6"
@@ -2244,6 +2386,12 @@ dependencies = [
  "uint",
 ]
 
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
 [[package]]
 name = "event-listener"
 version = "4.0.3"
@@ -2586,7 +2734,7 @@ version = "3.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
 dependencies = [
- "gloo-timers",
+ "gloo-timers 0.2.6",
  "send_wrapper",
 ]
 
@@ -2722,6 +2870,18 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "gloo-timers"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "gloo-utils"
 version = "0.2.0"
@@ -3115,6 +3275,34 @@ version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
 
+[[package]]
+name = "httpmock"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08ec9586ee0910472dec1a1f0f8acf52f0fdde93aea74d70d4a3107b4be0fd5b"
+dependencies = [
+ "assert-json-diff",
+ "async-object-pool",
+ "async-std",
+ "async-trait",
+ "base64 0.21.7",
+ "basic-cookies",
+ "crossbeam-utils",
+ "form_urlencoded",
+ "futures-util",
+ "hyper 0.14.30",
+ "lazy_static",
+ "levenshtein",
+ "log",
+ "regex",
+ "serde",
+ "serde_json",
+ "serde_regex",
+ "similar",
+ "tokio",
+ "url",
+]
+
 [[package]]
 name = "hyper"
 version = "0.14.30"
@@ -3439,6 +3627,15 @@ 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 = "itertools"
 version = "0.12.1"
@@ -3816,6 +4013,46 @@ dependencies = [
  "cpufeatures",
 ]
 
+[[package]]
+name = "kv-log-macro"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "lalrpop"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca"
+dependencies = [
+ "ascii-canvas",
+ "bit-set",
+ "ena",
+ "itertools 0.11.0",
+ "lalrpop-util",
+ "petgraph",
+ "pico-args",
+ "regex",
+ "regex-syntax 0.8.4",
+ "string_cache",
+ "term",
+ "tiny-keccak 2.0.2",
+ "unicode-xid 0.2.6",
+ "walkdir",
+]
+
+[[package]]
+name = "lalrpop-util"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553"
+dependencies = [
+ "regex-automata 0.4.7",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.5.0"
@@ -3837,6 +4074,12 @@ version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
 
+[[package]]
+name = "levenshtein"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760"
+
 [[package]]
 name = "libc"
 version = "0.2.159"
@@ -3859,6 +4102,16 @@ version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
 
+[[package]]
+name = "libredox"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags 2.6.0",
+ "libc",
+]
+
 [[package]]
 name = "librocksdb-sys"
 version = "0.11.0+8.1.1"
@@ -4004,6 +4257,9 @@ name = "log"
 version = "0.4.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+dependencies = [
+ "value-bag",
+]
 
 [[package]]
 name = "logos"
@@ -4256,6 +4512,12 @@ dependencies = [
  "tempfile",
 ]
 
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
 [[package]]
 name = "nix"
 version = "0.29.0"
@@ -4865,6 +5127,21 @@ dependencies = [
  "indexmap 2.5.0",
 ]
 
+[[package]]
+name = "phf_shared"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+dependencies = [
+ "siphasher 0.3.11",
+]
+
+[[package]]
+name = "pico-args"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
+
 [[package]]
 name = "pin-project"
 version = "1.1.5"
@@ -5026,6 +5303,12 @@ dependencies = [
  "zerocopy",
 ]
 
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
 [[package]]
 name = "pretty_assertions"
 version = "1.4.1"
@@ -5478,6 +5761,17 @@ dependencies = [
  "bitflags 2.6.0",
 ]
 
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror",
+]
+
 [[package]]
 name = "regex"
 version = "1.10.6"
@@ -6458,6 +6752,16 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "serde_regex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf"
+dependencies = [
+ "regex",
+ "serde",
+]
+
 [[package]]
 name = "serde_spanned"
 version = "0.6.8"
@@ -6675,6 +6979,12 @@ dependencies = [
  "time",
 ]
 
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
 [[package]]
 name = "siphasher"
 version = "1.0.1"
@@ -6730,7 +7040,7 @@ version = "2.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f"
 dependencies = [
- "async-channel",
+ "async-channel 2.3.1",
  "async-executor",
  "async-fs",
  "async-io",
@@ -6786,7 +7096,7 @@ dependencies = [
  "serde_json",
  "sha2 0.10.8",
  "sha3 0.10.8",
- "siphasher",
+ "siphasher 1.0.1",
  "slab",
  "smallvec",
  "soketto 0.7.1",
@@ -6802,7 +7112,7 @@ version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5496f2d116b7019a526b1039ec2247dd172b8670633b1a64a614c9ea12c9d8c7"
 dependencies = [
- "async-channel",
+ "async-channel 2.3.1",
  "async-lock",
  "base64 0.21.7",
  "blake2-rfc",
@@ -6825,7 +7135,7 @@ dependencies = [
  "rand_chacha",
  "serde",
  "serde_json",
- "siphasher",
+ "siphasher 1.0.1",
  "slab",
  "smol",
  "smoldot",
@@ -7189,6 +7499,19 @@ 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",
+ "precomputed-hash",
+]
+
 [[package]]
 name = "stringprep"
 version = "0.1.5"
@@ -7534,6 +7857,17 @@ dependencies = [
  "windows-sys 0.59.0",
 ]
 
+[[package]]
+name = "term"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
+dependencies = [
+ "dirs-next",
+ "rustversion",
+ "winapi",
+]
+
 [[package]]
 name = "termcolor"
 version = "1.4.1"
@@ -8303,6 +8637,12 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
 
+[[package]]
+name = "value-bag"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101"
+
 [[package]]
 name = "vcpkg"
 version = "0.2.15"
@@ -9901,6 +10241,7 @@ dependencies = [
  "bigdecimal",
  "chrono",
  "fraction",
+ "httpmock",
  "rand 0.8.5",
  "reqwest 0.12.7",
  "serde",