feat(chain138-snap): add snap-ready pricing methods and integrator docs

This commit is contained in:
defiQUG
2026-04-25 23:45:07 -07:00
parent f9c7929a41
commit b15b13c57e
5 changed files with 572 additions and 2 deletions

View File

@@ -50,4 +50,4 @@ For the companion site (this repos `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`.

View File

@@ -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&timestamp=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&timestamp={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

View File

@@ -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`). |

View File

@@ -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",

View File

@@ -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<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return res.json() as Promise<T>;
}
/**
* 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 {