@blockrun/franklin 3.15.53 → 3.15.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -7,7 +7,26 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { ProviderError } from '../standard-models.js';
|
|
9
9
|
export declare const TICKER_TO_ID: Record<string, string>;
|
|
10
|
+
/** For tests + cache invalidation. */
|
|
11
|
+
export declare function clearIdResolutionCache(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a ticker to its CoinGecko id. Synchronous — checks the static
|
|
14
|
+
* map and the dynamic cache. Falls through to lowercase as a final guess.
|
|
15
|
+
*
|
|
16
|
+
* Use this from `transformData` (which is sync). Use `resolveProviderIdAsync`
|
|
17
|
+
* from `fetchData` to populate the cache before the sync read happens.
|
|
18
|
+
*/
|
|
10
19
|
export declare function resolveProviderId(ticker: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Like `resolveProviderId`, but on a static-map miss, hits CoinGecko's
|
|
22
|
+
* `/search?query=` to find the canonical id. Caches the result for 7 days
|
|
23
|
+
* so `resolveProviderId` (sync) can read it back during `transformData`.
|
|
24
|
+
*
|
|
25
|
+
* Why not always async: `transformData` is part of the Fetcher contract
|
|
26
|
+
* and is intentionally sync. `fetchData` is async, runs first, and is the
|
|
27
|
+
* right place to do network resolution. The two share state via the cache.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveProviderIdAsync(ticker: string): Promise<string>;
|
|
11
30
|
export declare function cached<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T>;
|
|
12
31
|
/** For tests: wipe every cached entry. */
|
|
13
32
|
export declare function clearCache(): void;
|
|
@@ -11,7 +11,15 @@ const BASE = 'https://api.coingecko.com/api/v3';
|
|
|
11
11
|
const UA = `franklin/${VERSION} (trading)`;
|
|
12
12
|
const TIMEOUT_MS = 10_000;
|
|
13
13
|
// Ticker → CoinGecko slug. Not exhaustive; unknown tickers fall through to
|
|
14
|
-
//
|
|
14
|
+
// the dynamic /search resolver below, which caches results.
|
|
15
|
+
//
|
|
16
|
+
// Verified 2026-05-04 in a live session: user asked Franklin for TON price,
|
|
17
|
+
// TradingMarket returned "No CoinGecko data for TON" because TON wasn't in
|
|
18
|
+
// this map and the lowercase fallback ("ton") doesn't match CoinGecko's
|
|
19
|
+
// actual id ("the-open-network"). Same hole exists for any token whose
|
|
20
|
+
// symbol differs from its id slug. Expanded the static map to cover the
|
|
21
|
+
// top ~30 currently-missing tokens, and added a /search-based resolver
|
|
22
|
+
// for everything else.
|
|
15
23
|
export const TICKER_TO_ID = {
|
|
16
24
|
BTC: 'bitcoin', ETH: 'ethereum', SOL: 'solana', BNB: 'binancecoin', XRP: 'ripple',
|
|
17
25
|
ADA: 'cardano', DOGE: 'dogecoin', AVAX: 'avalanche-2', DOT: 'polkadot', MATIC: 'matic-network',
|
|
@@ -20,12 +28,87 @@ export const TICKER_TO_ID = {
|
|
|
20
28
|
FIL: 'filecoin', AAVE: 'aave', MKR: 'maker', SNX: 'synthetix-network-token',
|
|
21
29
|
COMP: 'compound-governance-token', INJ: 'injective-protocol', TIA: 'celestia',
|
|
22
30
|
PEPE: 'pepe', WIF: 'dogwifcoin', RENDER: 'render-token',
|
|
31
|
+
// ── Added 2026-05-04 after live "No CoinGecko data for TON" report ──
|
|
32
|
+
TON: 'the-open-network', HYPE: 'hyperliquid', TRX: 'tron', TAO: 'bittensor',
|
|
33
|
+
WLD: 'worldcoin-wld', ENA: 'ethena', BERA: 'berachain-bera', JUP: 'jupiter-exchange-solana',
|
|
34
|
+
FET: 'fetch-ai', ONDO: 'ondo-finance', RNDR: 'render-token',
|
|
35
|
+
USDT: 'tether', USDC: 'usd-coin', DAI: 'dai', BCH: 'bitcoin-cash', ETC: 'ethereum-classic',
|
|
36
|
+
XLM: 'stellar', XMR: 'monero', IMX: 'immutable-x', GRT: 'the-graph', SAND: 'the-sandbox',
|
|
37
|
+
MANA: 'decentraland', AXS: 'axie-infinity', KAS: 'kaspa', ICP: 'internet-computer',
|
|
38
|
+
HBAR: 'hedera-hashgraph', VET: 'vechain', ALGO: 'algorand', FTM: 'fantom',
|
|
39
|
+
EGLD: 'elrond-erd-2', CRV: 'curve-dao-token', LDO: 'lido-dao', SHIB: 'shiba-inu',
|
|
40
|
+
BONK: 'bonk', POPCAT: 'popcat', FLOKI: 'floki', PNUT: 'peanut-the-squirrel',
|
|
23
41
|
};
|
|
42
|
+
const ID_RESOLUTION_CACHE = new Map();
|
|
43
|
+
const ID_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
44
|
+
function normalizeTicker(ticker) {
|
|
45
|
+
return ticker.toUpperCase().replace(/-USD$/, '').replace(/USDT?$/, '');
|
|
46
|
+
}
|
|
47
|
+
/** For tests + cache invalidation. */
|
|
48
|
+
export function clearIdResolutionCache() {
|
|
49
|
+
ID_RESOLUTION_CACHE.clear();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a ticker to its CoinGecko id. Synchronous — checks the static
|
|
53
|
+
* map and the dynamic cache. Falls through to lowercase as a final guess.
|
|
54
|
+
*
|
|
55
|
+
* Use this from `transformData` (which is sync). Use `resolveProviderIdAsync`
|
|
56
|
+
* from `fetchData` to populate the cache before the sync read happens.
|
|
57
|
+
*/
|
|
24
58
|
export function resolveProviderId(ticker) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
59
|
+
const normalized = normalizeTicker(ticker);
|
|
60
|
+
if (TICKER_TO_ID[normalized])
|
|
61
|
+
return TICKER_TO_ID[normalized];
|
|
62
|
+
if (TICKER_TO_ID[ticker.toUpperCase()])
|
|
63
|
+
return TICKER_TO_ID[ticker.toUpperCase()];
|
|
64
|
+
const cached = ID_RESOLUTION_CACHE.get(normalized);
|
|
65
|
+
if (cached && cached.expiresAt > Date.now())
|
|
66
|
+
return cached.id;
|
|
67
|
+
return normalized.toLowerCase();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Like `resolveProviderId`, but on a static-map miss, hits CoinGecko's
|
|
71
|
+
* `/search?query=` to find the canonical id. Caches the result for 7 days
|
|
72
|
+
* so `resolveProviderId` (sync) can read it back during `transformData`.
|
|
73
|
+
*
|
|
74
|
+
* Why not always async: `transformData` is part of the Fetcher contract
|
|
75
|
+
* and is intentionally sync. `fetchData` is async, runs first, and is the
|
|
76
|
+
* right place to do network resolution. The two share state via the cache.
|
|
77
|
+
*/
|
|
78
|
+
export async function resolveProviderIdAsync(ticker) {
|
|
79
|
+
const normalized = normalizeTicker(ticker);
|
|
80
|
+
// Static map and dynamic cache — fast path.
|
|
81
|
+
if (TICKER_TO_ID[normalized])
|
|
82
|
+
return TICKER_TO_ID[normalized];
|
|
83
|
+
if (TICKER_TO_ID[ticker.toUpperCase()])
|
|
84
|
+
return TICKER_TO_ID[ticker.toUpperCase()];
|
|
85
|
+
const cached = ID_RESOLUTION_CACHE.get(normalized);
|
|
86
|
+
if (cached && cached.expiresAt > Date.now())
|
|
87
|
+
return cached.id;
|
|
88
|
+
// Network: ask CoinGecko's search endpoint.
|
|
89
|
+
try {
|
|
90
|
+
const result = await coingeckoGet(`/search?query=${encodeURIComponent(normalized)}`);
|
|
91
|
+
if (result && typeof result === 'object' && !('kind' in result) && 'coins' in result) {
|
|
92
|
+
const coins = result.coins;
|
|
93
|
+
if (Array.isArray(coins) && coins.length > 0) {
|
|
94
|
+
// Prefer an exact symbol match; fall back to the highest-ranked
|
|
95
|
+
// coin (lowest market_cap_rank value, ignoring null/undefined).
|
|
96
|
+
const exact = coins.find(c => c.symbol?.toUpperCase() === normalized && typeof c.id === 'string');
|
|
97
|
+
const fallback = [...coins]
|
|
98
|
+
.filter((c) => typeof c.id === 'string')
|
|
99
|
+
.sort((a, b) => (a.market_cap_rank ?? Infinity) - (b.market_cap_rank ?? Infinity))[0];
|
|
100
|
+
const resolved = exact?.id ?? fallback?.id;
|
|
101
|
+
if (resolved) {
|
|
102
|
+
ID_RESOLUTION_CACHE.set(normalized, { id: resolved, expiresAt: Date.now() + ID_TTL_MS });
|
|
103
|
+
return resolved;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// /search itself failed — fall through to the lowercase guess.
|
|
110
|
+
}
|
|
111
|
+
return normalized.toLowerCase();
|
|
29
112
|
}
|
|
30
113
|
const cache = new Map();
|
|
31
114
|
export async function cached(key, ttlMs, fn) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cached, coingeckoGet,
|
|
1
|
+
import { cached, coingeckoGet, resolveProviderIdAsync, TTL } from './client.js';
|
|
2
2
|
export const coingeckoOHLCVFetcher = {
|
|
3
3
|
providerName: 'coingecko',
|
|
4
4
|
transformQuery(input) {
|
|
@@ -9,7 +9,7 @@ export const coingeckoOHLCVFetcher = {
|
|
|
9
9
|
return { ticker, days };
|
|
10
10
|
},
|
|
11
11
|
async fetchData(query) {
|
|
12
|
-
const id =
|
|
12
|
+
const id = await resolveProviderIdAsync(query.ticker);
|
|
13
13
|
return cached(`ohlcv:${id}:${query.days}`, TTL.ohlcv, async () => {
|
|
14
14
|
return coingeckoGet(`/coins/${id}/market_chart?vs_currency=usd&days=${query.days}&interval=daily`);
|
|
15
15
|
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Coerces: the nested `{ [id]: { usd: ..., usd_24h_change: ... } }`
|
|
7
7
|
* response into the standard PriceData shape.
|
|
8
8
|
*/
|
|
9
|
-
import { cached, coingeckoGet, resolveProviderId, TTL } from './client.js';
|
|
9
|
+
import { cached, coingeckoGet, resolveProviderId, resolveProviderIdAsync, TTL } from './client.js';
|
|
10
10
|
export const coingeckoPriceFetcher = {
|
|
11
11
|
providerName: 'coingecko',
|
|
12
12
|
transformQuery(input) {
|
|
@@ -17,7 +17,10 @@ export const coingeckoPriceFetcher = {
|
|
|
17
17
|
return { ticker };
|
|
18
18
|
},
|
|
19
19
|
async fetchData(query) {
|
|
20
|
-
|
|
20
|
+
// resolveProviderIdAsync warms the dynamic id cache via /search when the
|
|
21
|
+
// ticker isn't in the static map (e.g. TON → the-open-network).
|
|
22
|
+
// transformData below reads back from the same cache synchronously.
|
|
23
|
+
const id = await resolveProviderIdAsync(query.ticker);
|
|
21
24
|
return cached(`price:${id}`, TTL.price, async () => {
|
|
22
25
|
return coingeckoGet(`/simple/price?ids=${id}` +
|
|
23
26
|
`&vs_currencies=usd&include_24hr_change=true` +
|
package/package.json
CHANGED