From 5ae474eecce6e0dff5ee4b1ae32d1e91dc43e3df Mon Sep 17 00:00:00 2001 From: Jakub Jerabek Date: Thu, 7 May 2026 13:25:15 +0200 Subject: [PATCH 1/3] feat(scripts): add script for fetching historical truth data for ERC-20 tokens --- contrib/scripts/refresh_eth_fiat_truth.py | 226 ++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 contrib/scripts/refresh_eth_fiat_truth.py diff --git a/contrib/scripts/refresh_eth_fiat_truth.py b/contrib/scripts/refresh_eth_fiat_truth.py new file mode 100644 index 0000000000..0a16b0e091 --- /dev/null +++ b/contrib/scripts/refresh_eth_fiat_truth.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +"""Refresh Ethereum historical fiat truth fixtures from CoinGecko.""" + +import argparse +import datetime as dt +import json +import os +import sys +import time +import urllib.error +import urllib.parse +import urllib.request + + +TOKENS = [ + { + "coinId": "usd-coin", + "symbol": "USDC", + "contract": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "relativeTolerance": 0.001, + }, + { + "coinId": "tether", + "symbol": "USDT", + "contract": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "relativeTolerance": 0.001, + }, + { + "coinId": "wrapped-bitcoin", + "symbol": "WBTC", + "contract": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "relativeTolerance": 0.05, + }, + { + "coinId": "aave", + "symbol": "AAVE", + "contract": "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", + "relativeTolerance": 0.05, + }, + { + "coinId": "uniswap", + "symbol": "UNI", + "contract": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "relativeTolerance": 0.05, + }, +] + +DEFAULT_OUTPUT = "tests/api/testdata/ethereum_fiat_truth.json" +DEFAULT_POINTS = 20 +FREE_BASE_URL = "https://api.coingecko.com/api/v3" +PRO_BASE_URL = "https://pro-api.coingecko.com/api/v3" + + +def parse_args(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--date", + action="append", + dest="dates", + help="UTC date to fetch in YYYY-MM-DD format. Can be repeated. Overrides --points.", + ) + parser.add_argument("--points", type=int, default=DEFAULT_POINTS, help="Number of spread daily points per token.") + parser.add_argument("--output", default=DEFAULT_OUTPUT, help="Fixture path to write.") + parser.add_argument( + "--days", + default="365", + help="CoinGecko market_chart days parameter. Use max with a paid plan for older dates.", + ) + parser.add_argument( + "--plan", + choices=("free", "pro"), + default=os.getenv("COINGECKO_PLAN", "free"), + help="CoinGecko API plan. Defaults to COINGECKO_PLAN or free.", + ) + return parser.parse_args() + + +def utc_timestamp(date_text): + try: + parsed = dt.datetime.strptime(date_text, "%Y-%m-%d") + except ValueError as exc: + raise SystemExit(f"invalid --date {date_text!r}: expected YYYY-MM-DD") from exc + return int(parsed.replace(tzinfo=dt.timezone.utc).timestamp()) + + +def coingecko_base_url(plan): + if plan == "pro": + return PRO_BASE_URL + return FREE_BASE_URL + + +def api_headers(plan): + headers = {"User-Agent": "blockbook-fiat-truth-refresh/1.0"} + api_key = os.getenv("COINGECKO_API_KEY", "").strip() + if api_key: + if plan == "pro": + headers["x-cg-pro-api-key"] = api_key + else: + headers["x-cg-demo-api-key"] = api_key + return headers + + +def fetch_json(url, headers): + req = urllib.request.Request(url, headers=headers) + last_error = None + for attempt in range(4): + try: + with urllib.request.urlopen(req, timeout=30) as response: + return json.load(response) + except urllib.error.URLError as exc: + last_error = exc + if attempt == 3: + break + time.sleep(1.0 + attempt) + raise last_error + + +def market_chart_url(base_url, coin_id, currency, days): + query = urllib.parse.urlencode( + { + "vs_currency": currency, + "days": days, + "interval": "daily", + } + ) + return f"{base_url}/coins/{coin_id}/market_chart?{query}" + + +def price_by_timestamp(prices): + result = {} + for ts_ms, price in prices: + if ts_ms % 1000 != 0: + continue + result[int(ts_ms) // 1000] = price + return result + + +def select_spread_points(prices, count): + points = [] + today_midnight = dt.datetime.now(dt.timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + today_midnight_ts = int(today_midnight.timestamp()) + for ts_ms, price in prices: + if ts_ms % 1000 != 0 or price <= 0: + continue + ts = int(ts_ms) // 1000 + if ts % 86400 != 0 or ts >= today_midnight_ts: + continue + points.append([ts, price]) + if count <= 0: + raise SystemExit("--points must be positive") + if len(points) < count: + raise SystemExit(f"CoinGecko returned only {len(points)} daily points, cannot select {count}") + if count == 1: + return [points[-1]] + + selected = [] + last_index = len(points) - 1 + for i in range(count): + index = round(i * last_index / (count - 1)) + selected.append(points[index]) + return selected + + +def build_fixture(args): + timestamps = [] + if args.dates: + timestamps = [(date, utc_timestamp(date)) for date in args.dates] + fetched_at = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") + base_url = coingecko_base_url(args.plan) + headers = api_headers(args.plan) + cases = [] + + for token in TOKENS: + url = market_chart_url(base_url, token["coinId"], "usd", args.days) + data = fetch_json(url, headers) + prices = data.get("prices", []) + expected_rates = [] + if timestamps: + prices_by_timestamp = price_by_timestamp(prices) + for date_text, timestamp in timestamps: + price = prices_by_timestamp.get(timestamp) + if price is None: + raise SystemExit( + f"{token['symbol']} has no daily price for {date_text} in {url}; " + "use a wider --days value, a paid plan, or choose a newer fixture date" + ) + expected_rates.append([timestamp, price]) + else: + expected_rates = select_spread_points(prices, args.points) + + cases.append( + { + "name": token["symbol"], + "coinId": token["coinId"], + "symbol": token["symbol"], + "contract": token["contract"], + "currency": "usd", + "expectedRates": expected_rates, + "relativeTolerance": token["relativeTolerance"], + "source": url, + "fetchedAt": fetched_at, + } + ) + time.sleep(2.0) + + return { + "source": "coingecko", + "currency": "usd", + "fetchedAt": fetched_at, + "cases": cases, + } + + +def main(): + args = parse_args() + fixture = build_fixture(args) + with open(args.output, "w", encoding="utf-8") as f: + json.dump(fixture, f, indent=2) + f.write("\n") + + +if __name__ == "__main__": + try: + main() + except urllib.error.HTTPError as exc: + sys.exit(f"CoinGecko request failed: HTTP {exc.code}: {exc.read().decode('utf-8', 'replace')}") From 720c69104333323fac92acc992b43ca1bcb10445 Mon Sep 17 00:00:00 2001 From: Jakub Jerabek Date: Thu, 7 May 2026 13:25:39 +0200 Subject: [PATCH 2/3] tests(rates): eth integration test for historical ERC-20 fiat rates --- docs/testing.md | 5 ++ tests/api/api.go | 1 + tests/api/http_tests.go | 105 ++++++++++++++++++++++++++++++++++++++++ tests/api/testdata.go | 42 ++++++++++++++++ tests/tests.json | 2 +- 5 files changed, 154 insertions(+), 1 deletion(-) diff --git a/docs/testing.md b/docs/testing.md index 66a1e292f4..edbc527319 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -104,6 +104,11 @@ Public Blockbook API checks are implemented in package `blockbook/tests/api` and in *blockbook/tests/tests.json*. Use `make test-e2e` to run this suite only. +Ethereum also has a historical fiat truth check for selected ERC-20 token USD rates. The test reads deterministic +fixtures from *tests/api/testdata/ethereum_fiat_truth.json* and does not call CoinGecko during the test run. Refresh the +fixture intentionally with `python3 contrib/scripts/refresh_eth_fiat_truth.py`, which stores 20 spread daily points per +token by default; for dates outside CoinGecko's public history window, use a paid API key and `--plan=pro --days=max`. + Phase 1 covers smoke checks for: * HTTP: `Status`, `GetBlockIndex`, `GetBlockByHeight`, `GetBlock`, `GetTransaction`, `GetTransactionSpecific`, `GetAddress`, `GetAddressTxids`, `GetAddressTxs`, `GetUtxo`, `GetUtxoConfirmedFilter` diff --git a/tests/api/api.go b/tests/api/api.go index bd735ddead..f1ce3dca65 100644 --- a/tests/api/api.go +++ b/tests/api/api.go @@ -66,6 +66,7 @@ var evmOnlyTests = map[string]func(t *testing.T, th *TestHandler){ "GetAddressTokenBalances": testGetAddressTokenBalances, "GetAddressProtocolsEVM": testGetAddressProtocolsEVM, "GetContractInfoEVM": testGetContractInfoEVM, + "GetHistoricalFiatRatesTruth": testGetHistoricalFiatRatesTruth, "GetAddressTxidsPaginationEVM": testGetAddressTxidsPaginationEVM, "GetAddressTxsPaginationEVM": testGetAddressTxsPaginationEVM, "GetAddressContractFilterEVM": testGetAddressContractFilterEVM, diff --git a/tests/api/http_tests.go b/tests/api/http_tests.go index e3a54fd4b6..ec6f0ac97b 100644 --- a/tests/api/http_tests.go +++ b/tests/api/http_tests.go @@ -197,6 +197,111 @@ func testGetMultiTickers(t *testing.T, h *TestHandler) { } } +func testGetHistoricalFiatRatesTruth(t *testing.T, h *TestHandler) { + status := h.getStatus(t) + if !status.HasFiatRates { + t.Skipf("Skipping test, endpoint reports hasFiatRates=false") + } + + fixtures, err := loadAPIFiatTruthTestData(h.Coin) + if err != nil { + t.Fatalf("load fiat truth fixtures for %s: %v", h.Coin, err) + } + if len(fixtures.Cases) == 0 { + t.Fatalf("fiat truth fixtures for %s have no cases", h.Coin) + } + + for _, fixture := range fixtures.Cases { + t.Run(fixture.Name, func(t *testing.T) { + validateFiatTruthFixture(t, fixture) + + for _, expected := range fixture.ExpectedRates { + timestamp := expected.Timestamp() + expectedRate := expected.Rate() + t.Run(fmt.Sprintf("%d", timestamp), func(t *testing.T) { + path := fmt.Sprintf( + "/api/v2/tickers?timestamp=%d¤cy=%s&token=%s", + timestamp, + url.QueryEscape(fixture.Currency), + url.QueryEscape(fixture.Contract), + ) + httpStatus, body := h.getHTTP(t, path) + if httpStatus != http.StatusOK { + t.Fatalf("GET %s returned HTTP %d: %s", path, httpStatus, preview(body)) + } + + var got fiatTickerResponse + if err := json.Unmarshal(body, &got); err != nil { + t.Fatalf("decode %s: %v", path, err) + } + if got.Timestamp != timestamp { + t.Fatalf("%s timestamp mismatch: got %d, want %d", fixture.Name, got.Timestamp, timestamp) + } + rate, found := got.Rates[strings.ToLower(fixture.Currency)] + if !found { + t.Fatalf("%s missing %s rate in %v", fixture.Name, fixture.Currency, got.Rates) + } + if rate <= 0 { + t.Fatalf("%s returned non-positive %s rate: %v", fixture.Name, fixture.Currency, rate) + } + if diff := relativeDifference(float64(rate), expectedRate); diff > fixture.RelativeTolerance { + t.Fatalf( + "%s %s rate mismatch: got %v, want %v within %.4f relative tolerance, diff %.4f; source %s", + fixture.Name, + fixture.Currency, + rate, + expectedRate, + fixture.RelativeTolerance, + diff, + fixture.Source, + ) + } + }) + } + }) + } +} + +func validateFiatTruthFixture(t *testing.T, fixture fiatTruthFixture) { + t.Helper() + + assertNonEmptyString(t, fixture.Name, "fiatTruth.name") + assertNonEmptyString(t, fixture.CoinID, "fiatTruth.coinId") + assertNonEmptyString(t, fixture.Symbol, "fiatTruth.symbol") + assertNonEmptyString(t, fixture.Contract, "fiatTruth.contract") + assertNonEmptyString(t, fixture.Currency, "fiatTruth.currency") + assertNonEmptyString(t, fixture.Source, "fiatTruth.source") + assertNonEmptyString(t, fixture.FetchedAt, "fiatTruth.fetchedAt") + if len(fixture.ExpectedRates) == 0 { + t.Fatalf("%s has no expectedRates", fixture.Name) + } + previousTimestamp := int64(0) + for _, expected := range fixture.ExpectedRates { + timestamp := expected.Timestamp() + if timestamp <= 0 { + t.Fatalf("%s has invalid timestamp %d", fixture.Name, timestamp) + } + if timestamp <= previousTimestamp { + t.Fatalf("%s has non-increasing timestamp %d after %d", fixture.Name, timestamp, previousTimestamp) + } + if expected.Rate() <= 0 { + t.Fatalf("%s has invalid expected rate %v", fixture.Name, expected.Rate()) + } + previousTimestamp = timestamp + } + if fixture.RelativeTolerance <= 0 { + t.Fatalf("%s has invalid relativeTolerance %v", fixture.Name, fixture.RelativeTolerance) + } +} + +func relativeDifference(got, want float64) float64 { + diff := got - want + if diff < 0 { + diff = -diff + } + return diff / want +} + func testGetAddressTxids(t *testing.T, h *TestHandler) { address := h.sampleAddressOrSkip(t) txid := h.sampleTxIDOrSkip(t) diff --git a/tests/api/testdata.go b/tests/api/testdata.go index 296ec6a696..813d670c3b 100644 --- a/tests/api/testdata.go +++ b/tests/api/testdata.go @@ -14,6 +14,35 @@ type erc4626Fixture struct { Contract string `json:"contract"` } +type fiatTruthFixtureSet struct { + Source string `json:"source"` + Currency string `json:"currency"` + FetchedAt string `json:"fetchedAt"` + Cases []fiatTruthFixture `json:"cases"` +} + +type fiatTruthFixture struct { + Name string `json:"name"` + CoinID string `json:"coinId"` + Symbol string `json:"symbol"` + Contract string `json:"contract"` + Currency string `json:"currency"` + ExpectedRates []fiatTruthRatePoint `json:"expectedRates"` + RelativeTolerance float64 `json:"relativeTolerance"` + Source string `json:"source"` + FetchedAt string `json:"fetchedAt"` +} + +type fiatTruthRatePoint [2]float64 + +func (p fiatTruthRatePoint) Timestamp() int64 { + return int64(p[0]) +} + +func (p fiatTruthRatePoint) Rate() float64 { + return p[1] +} + type testData struct { ERC4626Fixtures []erc4626Fixture `json:"erc4626Fixtures,omitempty"` } @@ -30,3 +59,16 @@ func loadAPITestData(coin string) (*testData, error) { } return &v, nil } + +func loadAPIFiatTruthTestData(coin string) (*fiatTruthFixtureSet, error) { + path := filepath.Join("api/testdata", coin+"_fiat_truth.json") + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var v fiatTruthFixtureSet + if err := json.Unmarshal(b, &v); err != nil { + return nil, err + } + return &v, nil +} diff --git a/tests/tests.json b/tests/tests.json index b8ce4c0f24..eacba1e51b 100644 --- a/tests/tests.json +++ b/tests/tests.json @@ -302,7 +302,7 @@ "ethereum": { "connectivity": ["http", "ws"], "api": ["Status", "GetBlockIndex", "GetBlockByHeight", "GetBlock", "GetTransaction", "GetTransactionSpecific", - "GetAddress", "GetCurrentFiatRates", "GetTickersList", "GetMultiTickers", "GetAddressBasicEVM", "GetAddressTokensEVM", "GetAddressTokenBalances", "GetAddressProtocolsEVM", "GetContractInfoEVM", "GetAddressTxidsPaginationEVM", "GetAddressTxsPaginationEVM", "GetAddressContractFilterEVM", "GetTransactionEVMShape", + "GetAddress", "GetCurrentFiatRates", "GetTickersList", "GetMultiTickers", "GetAddressBasicEVM", "GetAddressTokensEVM", "GetAddressTokenBalances", "GetAddressProtocolsEVM", "GetContractInfoEVM", "GetHistoricalFiatRatesTruth", "GetAddressTxidsPaginationEVM", "GetAddressTxsPaginationEVM", "GetAddressContractFilterEVM", "GetTransactionEVMShape", "WsGetInfo", "WsGetBlockHash", "WsGetTransaction", "WsGetAccountInfoBasicEVM", "WsGetAccountInfoEVM", "WsGetAccountInfoTxidsConsistencyEVM", "WsGetAccountInfoTxsConsistencyEVM", "WsGetAccountInfoContractFilterEVM", "WsGetAccountInfoProtocolsEVM", "WsGetContractInfoEVM", "WsPing"], "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader"] }, From d634a1db585e33fd64fc6f17b6efb7bb768133e9 Mon Sep 17 00:00:00 2001 From: Jakub Jerabek Date: Thu, 7 May 2026 13:25:50 +0200 Subject: [PATCH 3/3] tests(rates): add ethereum fiat truth rates --- tests/api/testdata/ethereum_fiat_truth.json | 467 ++++++++++++++++++++ 1 file changed, 467 insertions(+) create mode 100644 tests/api/testdata/ethereum_fiat_truth.json diff --git a/tests/api/testdata/ethereum_fiat_truth.json b/tests/api/testdata/ethereum_fiat_truth.json new file mode 100644 index 0000000000..962d7828c7 --- /dev/null +++ b/tests/api/testdata/ethereum_fiat_truth.json @@ -0,0 +1,467 @@ +{ + "source": "coingecko", + "currency": "usd", + "fetchedAt": "2026-05-07T10:57:36Z", + "cases": [ + { + "name": "USDC", + "coinId": "usd-coin", + "symbol": "USDC", + "contract": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "currency": "usd", + "expectedRates": [ + [ + 1746662400, + 0.9999593641301138 + ], + [ + 1748304000, + 0.9997886714957062 + ], + [ + 1749945600, + 0.9997031713415484 + ], + [ + 1751673600, + 0.9998050337453583 + ], + [ + 1753315200, + 0.9998059403333254 + ], + [ + 1754956800, + 0.9998334987065358 + ], + [ + 1756598400, + 0.9998012887207978 + ], + [ + 1758240000, + 0.9997286082156372 + ], + [ + 1759968000, + 0.9996975205135418 + ], + [ + 1761609600, + 0.999842184903193 + ], + [ + 1763251200, + 0.9997995696297937 + ], + [ + 1764892800, + 0.9998048293821209 + ], + [ + 1766620800, + 1.0000761307482435 + ], + [ + 1767225600, + 0.9998079595892425 + ], + [ + 1768262400, + 0.9997849605034322 + ], + [ + 1769904000, + 0.999551829097527 + ], + [ + 1771545600, + 0.999999140168384 + ], + [ + 1773187200, + 0.9999533776305615 + ], + [ + 1774915200, + 0.9997288055493505 + ], + [ + 1776556800, + 0.9997934711287205 + ] + ], + "relativeTolerance": 0.001, + "source": "https://api.coingecko.com/api/v3/coins/usd-coin/market_chart?vs_currency=usd&days=365&interval=daily", + "fetchedAt": "2026-05-07T10:57:36Z" + }, + { + "name": "USDT", + "coinId": "tether", + "symbol": "USDT", + "contract": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "currency": "usd", + "expectedRates": [ + [ + 1746662400, + 1.0001813000419701 + ], + [ + 1748304000, + 1.0002640914987955 + ], + [ + 1749945600, + 1.0003010126168363 + ], + [ + 1751673600, + 1.0002100728429393 + ], + [ + 1753315200, + 1.0003665884315096 + ], + [ + 1754956800, + 0.9999829869476258 + ], + [ + 1756598400, + 1.0002107070247845 + ], + [ + 1758240000, + 1.000345821394593 + ], + [ + 1759968000, + 1.000223743804291 + ], + [ + 1761609600, + 1.000152551518408 + ], + [ + 1763251200, + 0.9996246101770074 + ], + [ + 1764892800, + 1.0001821287427555 + ], + [ + 1766620800, + 0.9995187050751727 + ], + [ + 1767225600, + 0.9986627246946019 + ], + [ + 1768262400, + 0.9989061546969549 + ], + [ + 1769904000, + 0.9989497615329074 + ], + [ + 1771545600, + 0.9996645344960943 + ], + [ + 1773187200, + 1.0000168490151076 + ], + [ + 1774915200, + 0.9992106628948578 + ], + [ + 1776556800, + 1.0002172992279033 + ] + ], + "relativeTolerance": 0.001, + "source": "https://api.coingecko.com/api/v3/coins/tether/market_chart?vs_currency=usd&days=365&interval=daily", + "fetchedAt": "2026-05-07T10:57:36Z" + }, + { + "name": "WBTC", + "coinId": "wrapped-bitcoin", + "symbol": "WBTC", + "contract": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "currency": "usd", + "expectedRates": [ + [ + 1746662400, + 96841.63406761974 + ], + [ + 1748304000, + 109004.3023338958 + ], + [ + 1749945600, + 105418.57093229842 + ], + [ + 1751673600, + 107892.80744817255 + ], + [ + 1753315200, + 118466.71277139678 + ], + [ + 1754956800, + 118474.8218344662 + ], + [ + 1756598400, + 108759.47252761183 + ], + [ + 1758240000, + 117039.12094202286 + ], + [ + 1759968000, + 123275.31066891988 + ], + [ + 1761609600, + 114128.92804854536 + ], + [ + 1763251200, + 95472.33216968716 + ], + [ + 1764892800, + 91904.417902608 + ], + [ + 1766620800, + 87457.5607856024 + ], + [ + 1767225600, + 87080.01511170523 + ], + [ + 1768262400, + 90697.62682122778 + ], + [ + 1769904000, + 78658.95483108563 + ], + [ + 1771545600, + 66658.65813249983 + ], + [ + 1773187200, + 69778.98508430267 + ], + [ + 1774915200, + 66568.59248822147 + ], + [ + 1776556800, + 75522.44079236686 + ] + ], + "relativeTolerance": 0.05, + "source": "https://api.coingecko.com/api/v3/coins/wrapped-bitcoin/market_chart?vs_currency=usd&days=365&interval=daily", + "fetchedAt": "2026-05-07T10:57:36Z" + }, + { + "name": "AAVE", + "coinId": "aave", + "symbol": "AAVE", + "contract": "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", + "currency": "usd", + "expectedRates": [ + [ + 1746662400, + 172.01520862794462 + ], + [ + 1748304000, + 267.368984442674 + ], + [ + 1749945600, + 276.82051587938275 + ], + [ + 1751673600, + 265.8420385962192 + ], + [ + 1753315200, + 290.7872272674331 + ], + [ + 1754956800, + 294.0515816301711 + ], + [ + 1756598400, + 318.9856145257542 + ], + [ + 1758240000, + 309.3074470853695 + ], + [ + 1759968000, + 285.61277047441894 + ], + [ + 1761609600, + 235.232340518776 + ], + [ + 1763251200, + 178.6240429948591 + ], + [ + 1764892800, + 190.46993040756232 + ], + [ + 1766620800, + 148.38000963882584 + ], + [ + 1767225600, + 146.29374797589736 + ], + [ + 1768262400, + 164.9656717076608 + ], + [ + 1769904000, + 128.75815639111372 + ], + [ + 1771545600, + 124.20641020449105 + ], + [ + 1773187200, + 111.56468557891041 + ], + [ + 1774915200, + 96.52693858831283 + ], + [ + 1776556800, + 101.87831886757445 + ] + ], + "relativeTolerance": 0.05, + "source": "https://api.coingecko.com/api/v3/coins/aave/market_chart?vs_currency=usd&days=365&interval=daily", + "fetchedAt": "2026-05-07T10:57:36Z" + }, + { + "name": "UNI", + "coinId": "uniswap", + "symbol": "UNI", + "contract": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "currency": "usd", + "expectedRates": [ + [ + 1746662400, + 4.875910458848198 + ], + [ + 1748304000, + 6.497412241643052 + ], + [ + 1749945600, + 7.314411686589805 + ], + [ + 1751673600, + 6.930882101122414 + ], + [ + 1753315200, + 10.173388495661785 + ], + [ + 1754956800, + 11.127086698028993 + ], + [ + 1756598400, + 9.720283039010738 + ], + [ + 1758240000, + 9.619616765658952 + ], + [ + 1759968000, + 8.076713226656048 + ], + [ + 1761609600, + 6.5170449251411595 + ], + [ + 1763251200, + 7.352866190185604 + ], + [ + 1764892800, + 5.959379122750932 + ], + [ + 1766620800, + 5.774714946966649 + ], + [ + 1767225600, + 5.622236698856501 + ], + [ + 1768262400, + 5.358587596051589 + ], + [ + 1769904000, + 3.94569212067087 + ], + [ + 1771545600, + 3.387198065444971 + ], + [ + 1773187200, + 3.842539966304297 + ], + [ + 1774915200, + 3.47453707121789 + ], + [ + 1776556800, + 3.3567463362321934 + ] + ], + "relativeTolerance": 0.05, + "source": "https://api.coingecko.com/api/v3/coins/uniswap/market_chart?vs_currency=usd&days=365&interval=daily", + "fetchedAt": "2026-05-07T10:57:36Z" + } + ] +}