From b15b13c57ef5714263edb40a922ef9b11968ac64 Mon Sep 17 00:00:00 2001 From: defiQUG Date: Sat, 25 Apr 2026 23:45:07 -0700 Subject: [PATCH] feat(chain138-snap): add snap-ready pricing methods and integrator docs --- chain138-snap/INTEGRATORS.md | 2 +- .../docs/SNAP_PRICING_REQUIREMENTS.md | 444 ++++++++++++++++++ chain138-snap/packages/snap/README.md | 3 + .../packages/snap/snap.manifest.json | 2 +- chain138-snap/packages/snap/src/index.tsx | 123 +++++ 5 files changed, 572 insertions(+), 2 deletions(-) create mode 100644 chain138-snap/docs/SNAP_PRICING_REQUIREMENTS.md diff --git a/chain138-snap/INTEGRATORS.md b/chain138-snap/INTEGRATORS.md index 1a55942..f43de41 100644 --- a/chain138-snap/INTEGRATORS.md +++ b/chain138-snap/INTEGRATORS.md @@ -50,4 +50,4 @@ For the companion site (this repo’s `packages/site`): ## RPC methods -See [TESTING_INSTRUCTIONS.md](TESTING_INSTRUCTIONS.md) for the full list: `hello`, `get_networks`, `get_chain138_config`, `get_chain138_market_chains`, `get_token_list`, `get_token_list_url`, `get_oracles`, `show_dynamic_info`, `get_market_summary`, `show_market_data`, `get_bridge_routes`, `show_bridge_routes`, `get_swap_quote`, `show_swap_quote`. +See [TESTING_INSTRUCTIONS.md](TESTING_INSTRUCTIONS.md) for the full list: `hello`, `get_networks`, `get_chain138_config`, `get_chain138_market_chains`, `get_token_list`, `get_token_list_url`, `get_oracles`, `show_dynamic_info`, `get_market_summary`, `get_current_price`, `get_historical_price`, `get_pricing_context`, `show_market_data`, `get_bridge_routes`, `show_bridge_routes`, `get_swap_quote`, `show_swap_quote`. diff --git a/chain138-snap/docs/SNAP_PRICING_REQUIREMENTS.md b/chain138-snap/docs/SNAP_PRICING_REQUIREMENTS.md new file mode 100644 index 0000000..586c81b --- /dev/null +++ b/chain138-snap/docs/SNAP_PRICING_REQUIREMENTS.md @@ -0,0 +1,444 @@ +# Chain 138 Snap Pricing Requirements + +This note traces what is required to make Chain 138 pricing "MetaMask-Snap-grade" end to end for both: + +- current valuation on EOA and token detail surfaces +- transfer-time locked valuation on transaction detail surfaces + +It is based on the current live API contract and the current Snap implementation in this repo. + +## Current state + +The Snap already supports: + +- network config via `get_networks` and `get_chain138_config` +- token list via `get_token_list` +- current market summary via `get_market_summary` +- swap and bridge helper RPCs + +The token-aggregation API already supports: + +- current token summaries via `GET /api/v1/tokens` +- token detail via `GET /api/v1/tokens/:address` +- OHLCV candles via `GET /api/v1/tokens/:address/ohlcv` +- historical point lookup via `GET /api/v1/tokens/:address/price-at` + +The gap is that the Snap only consumes current market summary, while transfer-time pricing still depends on OHLCV coverage that is not yet reliably backfilled. + +## What the Snap calls today + +Current Snap RPC implementation: + +- `get_market_summary` calls `GET {apiBaseUrl}/api/v1/tokens?chainId={chainId}&limit=50` +- `get_oracles` calls `GET {apiBaseUrl}/api/v1/config?chainId={chainId}` +- `get_networks` calls `GET {apiBaseUrl}/api/v1/networks` + +Relevant implementation: + +- `metamask-integration/chain138-snap/packages/snap/src/index.tsx` +- `smom-dbis-138/services/token-aggregation/src/api/routes/tokens.ts` + +## Verified live payloads + +### 1. Networks + +Request: + +```http +GET /token-aggregation/api/v1/networks +``` + +Live shape: + +```json +{ + "source": "built-in", + "version": "1.0.0", + "networks": [ + { + "chainId": "0x8a", + "chainIdDecimal": 138, + "chainName": "DeFi Oracle Meta Mainnet", + "rpcUrls": ["..."], + "blockExplorerUrls": ["https://explorer.d-bis.org"], + "nativeCurrency": { + "name": "Ether", + "symbol": "ETH", + "decimals": 18 + }, + "iconUrls": ["..."], + "oracles": [ + { + "name": "ETH/USD", + "address": "0x3304b747e565a97ec8ac220b0b6a1f6ffdb837e6", + "decimals": 8 + } + ] + } + ] +} +``` + +### 2. Oracles config + +Request: + +```http +GET /token-aggregation/api/v1/config?chainId=138 +``` + +Live shape: + +```json +{ + "source": "built-in", + "version": "1.0.0", + "chainId": 138, + "oracles": [ + { + "name": "ETH/USD", + "address": "0x3304b747e565a97ec8ac220b0b6a1f6ffdb837e6", + "decimals": 8 + } + ] +} +``` + +### 3. Current market summary + +Request: + +```http +GET /token-aggregation/api/v1/tokens?chainId=138&limit=2 +``` + +Live shape: + +```json +{ + "source": "db", + "pagination": { + "limit": 2, + "offset": 0, + "count": 2 + }, + "tokens": [ + { + "address": "0x...", + "symbol": "USDT", + "name": "Tether USD (Chain 138)", + "market": { + "chainId": 138, + "tokenAddress": "0x...", + "priceUsd": 1, + "volume24h": 0, + "volume7d": 0, + "volume30d": 0, + "liquidityUsd": 12104786.72586392, + "holdersCount": 0, + "transfers24h": 0, + "lastUpdated": "2026-04-26T03:31:01.926Z" + }, + "pricing": { + "priceUsd": 1, + "sourceLayer": "indexer_market", + "precedenceRank": 1, + "stale": false, + "maxAgeSeconds": 900, + "asOf": "2026-04-26T03:31:01.926Z", + "ageSeconds": 3 + }, + "explorer": { + "chainId": 138, + "explorerBaseUrl": "https://explorer.d-bis.org", + "addressUrl": "https://explorer.d-bis.org/address/0x...", + "tokenUrl": "https://explorer.d-bis.org/address/0x..." + } + } + ] +} +``` + +### 4. Historical point lookup + +Request: + +```http +GET /token-aggregation/api/v1/tokens/0xc02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/price-at?chainId=138×tamp=2026-04-26T01:33:02.000Z +``` + +Live shape today: + +```json +{ + "chainId": 138, + "tokenAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "requestedTimestamp": "2026-04-26T01:33:02.000Z", + "effectiveTimestamp": "2026-04-26T03:31:01.988Z", + "priceUsd": 2490, + "source": "current_market_fallback" +} +``` + +This proves the endpoint contract exists, but also proves that the current result is not yet a locked historical candle for this timestamp. + +## Why transfer-time pricing is not fully locked yet + +### 1. The Snap does not call historical pricing yet + +Current Snap market RPCs only call: + +- `GET /api/v1/tokens` + +They do not call: + +- `GET /api/v1/tokens/:address/price-at` +- `GET /api/v1/tokens/:address/ohlcv` + +So the Snap is current-price compatible, but not transfer-time-price aware. + +### 2. The indexer only rolls OHLCV for the last 7 days + +Current indexing flow: + +- discovers pools +- indexes tokens +- updates current market data +- generates OHLCV for `5m`, `1h`, `24h` +- only for `now - 7 days` through `now` + +That means a historical valuation request can miss if: + +- `swap_events` were never backfilled for the requested token or time +- the requested timestamp is older than the rolling backfill window +- the token had sparse or no swap coverage in the indexed pool set + +### 3. Historical lookup deliberately falls back + +The current `price-at` route tries: + +1. `5m` OHLCV near the timestamp +2. broader `15m`, `1h`, `4h`, `24h` windows +3. current market data fallback +4. canonical fallback + +This is correct defensive behavior for explorer UX, but it is not sufficient for wallet-grade "locked at transfer time" semantics unless the caller can distinguish a true historical hit from a fallback. The `source` field already exposes that distinction. + +## What is required + +## A. Backfill OHLCV history + +Minimum requirement: + +- backfill `swap_events` and `token_ohlcv` for the token universe the Snap will price +- include native-wrapped asset pairs for WETH9/WETH10 and key stables +- preserve enough history to cover transaction lookback expectations + +Required data sources: + +- on-chain swap event replay into `swap_events` +- optional external pair OHLCV seeding where on-chain coverage is missing, using the existing CMC pair OHLCV adapter + +Implementation requirements: + +- add an explicit backfill job, not just rolling indexer generation +- support `fromBlock` / `toBlock` or `fromTimestamp` / `toTimestamp` +- generate `5m`, `15m`, `1h`, `4h`, `24h` candles, not only `5m`, `1h`, `24h` +- seed historical candles before enabling wallet-grade transfer-time valuation + +Operational acceptance criteria: + +- `price-at` for known transaction timestamps returns `source: ohlcv_*`, not `current_market_fallback` +- the requested timestamp and effective timestamp are within the expected candle tolerance +- historical coverage exists across the curated Chain 138 token set + +## B. Expose a Snap-ready pricing method + +The Snap needs a wallet-facing RPC for pricing, instead of overloading `get_market_summary`. + +Recommended new Snap RPC methods: + +- `get_current_price` +- `get_historical_price` +- `get_pricing_context` + +Recommended behavior: + +### `get_current_price` + +Request params: + +```json +{ + "apiBaseUrl": "https://explorer.d-bis.org/token-aggregation", + "chainId": 138, + "address": "0x..." +} +``` + +Recommended response: + +```json +{ + "chainId": 138, + "address": "0x...", + "priceUsd": 1, + "asOf": "2026-04-26T03:31:01.926Z", + "sourceLayer": "indexer_market", + "stale": false +} +``` + +This can be implemented by calling `GET /api/v1/tokens/:address?chainId=138` and projecting `token.pricing` plus selected `token.market` fields. + +### `get_historical_price` + +Request params: + +```json +{ + "apiBaseUrl": "https://explorer.d-bis.org/token-aggregation", + "chainId": 138, + "address": "0x...", + "timestamp": "2026-04-26T01:33:02.000Z" +} +``` + +Recommended response: + +```json +{ + "chainId": 138, + "address": "0x...", + "requestedTimestamp": "2026-04-26T01:33:02.000Z", + "effectiveTimestamp": "2026-04-26T01:30:00.000Z", + "priceUsd": 2490, + "source": "ohlcv_5m", + "historical": true +} +``` + +Important rule: + +- if `source` is `current_market_fallback` or `canonical_fallback`, the response should include `historical: false` +- callers must not treat fallback data as transfer-locked valuation + +### `get_pricing_context` + +This is the most Snap-friendly single call. + +Request params: + +```json +{ + "apiBaseUrl": "https://explorer.d-bis.org/token-aggregation", + "chainId": 138, + "address": "0x...", + "timestamp": "2026-04-26T01:33:02.000Z" +} +``` + +Recommended response: + +```json +{ + "chainId": 138, + "address": "0x...", + "current": { + "priceUsd": 2490, + "asOf": "2026-04-26T03:31:01.988Z", + "sourceLayer": "indexer_market", + "stale": false + }, + "historical": { + "requestedTimestamp": "2026-04-26T01:33:02.000Z", + "effectiveTimestamp": "2026-04-26T01:30:00.000Z", + "priceUsd": 2488.42, + "source": "ohlcv_5m", + "locked": true + } +} +``` + +This gives the Snap everything it needs for: + +- current wallet balance valuation +- transaction review valuation +- explicit distinction between live and transfer-time price + +## C. Exact payload shape the Snap/provider should call + +If we do not add a new backend endpoint immediately, the Snap/provider should call these existing routes: + +### Current valuation + +Use: + +```http +GET /api/v1/tokens/:address?chainId=138 +``` + +Read: + +- `token.market.priceUsd` +- `token.market.lastUpdated` +- `token.pricing.priceUsd` +- `token.pricing.asOf` +- `token.pricing.sourceLayer` +- `token.pricing.stale` + +### Historical valuation + +Use: + +```http +GET /api/v1/tokens/:address/price-at?chainId=138×tamp={ISO_8601} +``` + +Read: + +- `chainId` +- `tokenAddress` +- `requestedTimestamp` +- `effectiveTimestamp` +- `priceUsd` +- `source` + +Caller rule: + +- only treat `source` values starting with `ohlcv_` as locked historical valuation +- treat `current_market_fallback` and `canonical_fallback` as non-historical fallback data + +### Optional charting + +Use: + +```http +GET /api/v1/tokens/:address/ohlcv?chainId=138&interval=1h&from={ISO_8601}&to={ISO_8601} +``` + +Read: + +- `chainId` +- `tokenAddress` +- `interval` +- `data[]` with `timestamp`, `open`, `high`, `low`, `close`, `volume`, `volumeUsd` + +## Recommended implementation order + +1. Add a backfill job for `swap_events` and `token_ohlcv`. +2. Expand candle generation to include `15m` and `4h` in the indexer, not only the API fallback reader. +3. Add a single Snap-oriented API response, preferably `GET /api/v1/tokens/:address/pricing-context`. +4. Add new Snap RPC methods for current and historical pricing. +5. Update the companion site and provider examples to call the new pricing RPCs. +6. Add release checks that fail if curated assets return fallback instead of `ohlcv_*` for known test timestamps. + +## Practical definition of "MetaMask-Snap-grade" + +The pricing path is Snap-grade only when all of the following are true: + +- current price is available from the API for curated assets +- historical price is available for transaction timestamps from OHLCV, not fallback +- the Snap exposes current and historical valuation as distinct methods or a clearly typed combined method +- the caller can programmatically distinguish `locked historical` from `best-effort fallback` +- docs and examples show the exact request and response contract diff --git a/chain138-snap/packages/snap/README.md b/chain138-snap/packages/snap/README.md index c35e84c..aade4d7 100644 --- a/chain138-snap/packages/snap/README.md +++ b/chain138-snap/packages/snap/README.md @@ -51,6 +51,9 @@ For **market data**, **swap quotes**, and **bridge routes**, the dApp must pass | `get_oracles` | Oracles config. | | `show_dynamic_info` | In-Snap dialog with networks and token list URL. | | `get_market_summary` / `show_market_data` | Tokens and USD prices. | +| `get_current_price` | Current USD price snapshot for one token. | +| `get_historical_price` | Point-in-time USD valuation for one token at a timestamp. | +| `get_pricing_context` | Combined current and historical pricing response. | | `get_bridge_routes` / `show_bridge_routes` | CCIP and Trustless bridge routes. | | `get_swap_quote` / `show_swap_quote` | Swap quote (requires `tokenIn`, `tokenOut`, `amountIn`). | diff --git a/chain138-snap/packages/snap/snap.manifest.json b/chain138-snap/packages/snap/snap.manifest.json index 979aa8e..4bacae3 100644 --- a/chain138-snap/packages/snap/snap.manifest.json +++ b/chain138-snap/packages/snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/bis-innovations/chain138-snap.git" }, "source": { - "shasum": "8GYAFlgbiR/jXAwnprqqE4jTIvQv/Uhkn3MiH23g/tQ=", + "shasum": "n8O3BEDN45Q8+QgiyLpEhli82ZoNnyZy5r2zuu7jmqM=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/chain138-snap/packages/snap/src/index.tsx b/chain138-snap/packages/snap/src/index.tsx index 31cb1a0..c69b66e 100644 --- a/chain138-snap/packages/snap/src/index.tsx +++ b/chain138-snap/packages/snap/src/index.tsx @@ -22,6 +22,8 @@ export type SnapRpcParams = { toChain?: number; /** For get_token_mapping (resolve): token address on source chain to resolve to destination */ address?: string; + /** For historical pricing requests */ + timestamp?: string; /** * */ @@ -123,6 +125,14 @@ async function fetchNetworks(apiBase: string) { }>; } +async function fetchJson(url: string): Promise { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`HTTP ${res.status}`); + } + return res.json() as Promise; +} + /** * Handle incoming JSON-RPC requests, sent through `wallet_invokeSnap`. * @@ -578,6 +588,119 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ } } + case 'get_current_price': { + if (!base) { + return { + error: + 'Pass apiBaseUrl (token-aggregation service URL) to fetch current price', + }; + } + const chainIdParam = params?.chainId ?? 138; + const address = typeof params?.address === 'string' ? params.address.trim() : ''; + if (!address) { + return { error: 'Missing params: address' }; + } + try { + const data = await fetchJson<{ + token?: { + pricing?: { + priceUsd?: number; + asOf?: string; + sourceLayer?: string; + stale?: boolean; + }; + market?: { + lastUpdated?: string; + }; + }; + }>(`${base}/api/v1/tokens/${address}?chainId=${String(chainIdParam)}`); + return { + chainId: chainIdParam, + address, + priceUsd: data.token?.pricing?.priceUsd, + asOf: data.token?.pricing?.asOf ?? data.token?.market?.lastUpdated, + sourceLayer: data.token?.pricing?.sourceLayer, + stale: data.token?.pricing?.stale, + }; + } catch (error) { + return { + error: + error instanceof Error + ? error.message + : 'Failed to fetch current price', + }; + } + } + + case 'get_historical_price': { + if (!base) { + return { + error: + 'Pass apiBaseUrl (token-aggregation service URL) to fetch historical price', + }; + } + const chainIdParam = params?.chainId ?? 138; + const address = typeof params?.address === 'string' ? params.address.trim() : ''; + const timestamp = typeof params?.timestamp === 'string' ? params.timestamp.trim() : ''; + if (!address || !timestamp) { + return { error: 'Missing params: address, timestamp' }; + } + try { + const url = new URL(`${base}/api/v1/tokens/${address}/price-at`); + url.searchParams.set('chainId', String(chainIdParam)); + url.searchParams.set('timestamp', timestamp); + const data = await fetchJson<{ + chainId: number; + tokenAddress: string; + requestedTimestamp: string; + effectiveTimestamp?: string; + priceUsd?: number; + source: string; + }>(url.toString()); + return { + ...data, + address, + historical: data.source.startsWith('ohlcv_') || data.source === 'swap_event', + }; + } catch (error) { + return { + error: + error instanceof Error + ? error.message + : 'Failed to fetch historical price', + }; + } + } + + case 'get_pricing_context': { + if (!base) { + return { + error: + 'Pass apiBaseUrl (token-aggregation service URL) to fetch pricing context', + }; + } + const chainIdParam = params?.chainId ?? 138; + const address = typeof params?.address === 'string' ? params.address.trim() : ''; + if (!address) { + return { error: 'Missing params: address' }; + } + try { + const url = new URL(`${base}/api/v1/tokens/${address}/pricing-context`); + url.searchParams.set('chainId', String(chainIdParam)); + if (typeof params?.timestamp === 'string' && params.timestamp.trim()) { + url.searchParams.set('timestamp', params.timestamp.trim()); + } + return await fetchJson(url.toString()); + } catch (error) { + return { + error: + error instanceof Error + ? error.message + : 'Failed to fetch pricing context', + }; + } + } + case 'get_swap_quote': { if (!base) { return {