@defillama/sdk 5.0.238 → 5.0.242
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.
- package/build/abi/multicall.js +1 -0
- package/build/abi/multicall3.js +6 -0
- package/build/chains/aptos.d.ts +39 -0
- package/build/chains/aptos.js +97 -9
- package/build/chains/utxo.d.ts +8 -0
- package/build/chains/utxo.js +105 -19
- package/build/providers.json +1 -1
- package/build/util/blocks.js +1 -1
- package/build/util/chainUtils/data.json +13 -1
- package/build/util/env.js +1 -0
- package/build/util/updateProviderList.js +1 -1
- package/package.json +3 -2
package/build/abi/multicall.js
CHANGED
|
@@ -376,6 +376,7 @@ function multicallAddress(chainId) {
|
|
|
376
376
|
case 7119: return '0xFd4b34b5763f54a580a0d9f7997A2A993ef9ceE9'; // sentrix
|
|
377
377
|
case 36900: return '0x73df6E8F0D112D22bD672952323b43d6893AB6D2'; // adi
|
|
378
378
|
case 589: return '0x36b580266BD2B9581B805BF99D0Db92FbC9CAa56'; // lady
|
|
379
|
+
case 3344: return '0x1D9589CF11D92f46addd491e9698D7719c45a18f'; // pentagonchain (v1: aggregate only, deployed block 401584)
|
|
379
380
|
default:
|
|
380
381
|
return null;
|
|
381
382
|
}
|
package/build/abi/multicall3.js
CHANGED
|
@@ -240,6 +240,11 @@ const DEPLOYMENT_BLOCK = {
|
|
|
240
240
|
prom: 1238475,
|
|
241
241
|
nibiru: 19587573,
|
|
242
242
|
arc: 1,
|
|
243
|
+
btcwchain: 178635,
|
|
244
|
+
horizen: 1,
|
|
245
|
+
metall2: 1,
|
|
246
|
+
rbn: 2714761,
|
|
247
|
+
xpla: 19189761,
|
|
243
248
|
};
|
|
244
249
|
async function makeMultiCall(functionABI, calls, chain, block) {
|
|
245
250
|
if (!functionABI)
|
|
@@ -394,4 +399,5 @@ const CUSTOM_MULTICALL_ADDRESSES = {
|
|
|
394
399
|
'eni': '0xbf9d276932DF9c0AA1644b2569482f7a754433be',
|
|
395
400
|
'shido': '0xe3da1EC9e9BfEa1fE8C5905133c36A86351e4561',
|
|
396
401
|
'prom': '0xfF785aF3De8C2cb5727A8665984E741c16679131',
|
|
402
|
+
'btcwchain': '0xA0093EA76633A983364cb2582959870eaffDdea0',
|
|
397
403
|
};
|
package/build/chains/aptos.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ export declare const DEFAULT_ENDPOINTS: Record<string, string>;
|
|
|
4
4
|
* (the public aptos fullnode keeps only a few weeks of blocks). Override with `<CHAIN>_ARCHIVAL_RPC`.
|
|
5
5
|
*/
|
|
6
6
|
export declare const ARCHIVAL_ENDPOINTS: Record<string, string>;
|
|
7
|
+
/**
|
|
8
|
+
* Indexer (Hasura GraphQL) endpoints. Override / add fallbacks with `<CHAIN>_INDEXER` (comma separated).
|
|
9
|
+
* The `<CHAIN>_API_KEY` bearer key is sent here too (aptoslabs keys cover both the full node and the indexer).
|
|
10
|
+
*/
|
|
11
|
+
export declare const INDEXER_ENDPOINTS: Record<string, string>;
|
|
7
12
|
export declare const aptosChains: string[];
|
|
8
13
|
export declare const APT_COIN_TYPE = "0x1::aptos_coin::AptosCoin";
|
|
9
14
|
export declare const APT_DECIMALS = 8;
|
|
@@ -68,6 +73,8 @@ export declare function getEndpointList({ chain }?: ChainOptions): string[];
|
|
|
68
73
|
export declare function getEndpoint({ chain }?: ChainOptions): string;
|
|
69
74
|
/** Archival endpoints for the chain (`<CHAIN>_ARCHIVAL_RPC` env, then built-in); empty when none. */
|
|
70
75
|
export declare function getArchivalEndpointList({ chain }?: ChainOptions): string[];
|
|
76
|
+
/** Indexer GraphQL endpoints for the chain (`<CHAIN>_INDEXER` env first, then built-in); empty when none. */
|
|
77
|
+
export declare function getIndexerEndpointList({ chain }?: ChainOptions): string[];
|
|
71
78
|
/** GET `path` (relative to the node root, e.g. `/v1/accounts/0x1`). 404 -> `null` when `allowNotFound`. */
|
|
72
79
|
export declare function get({ chain, path, params, allowNotFound, timeout, retries, archival }: RequestOptions & {
|
|
73
80
|
path: string;
|
|
@@ -81,6 +88,38 @@ export declare function post({ chain, path, body, params, allowNotFound, timeout
|
|
|
81
88
|
params?: Record<string, any>;
|
|
82
89
|
allowNotFound?: boolean;
|
|
83
90
|
}): Promise<any>;
|
|
91
|
+
/** GraphQL `errors` in a 200 response (bad query, or a rate limit the indexer reports in-band). */
|
|
92
|
+
export declare class IndexerError extends Error {
|
|
93
|
+
errors: any[];
|
|
94
|
+
constructor(errors: any[]);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Run a GraphQL query against the chain's indexer, returning `data`. Same transport as the node calls:
|
|
98
|
+
* rotates over `<CHAIN>_INDEXER` endpoints (then the default), retries transport/5xx/429 failures with
|
|
99
|
+
* backoff and sends the `<CHAIN>_API_KEY` bearer key. GraphQL `errors` throw `IndexerError` and are
|
|
100
|
+
* only retried when they report a rate limit (a bad query fails fast).
|
|
101
|
+
*/
|
|
102
|
+
export declare function indexerQuery<T = any>({ chain, query, variables, timeout, retries }: RequestOptions & {
|
|
103
|
+
query: string;
|
|
104
|
+
variables?: Record<string, any>;
|
|
105
|
+
}): Promise<T>;
|
|
106
|
+
export interface FungibleAssetBalance {
|
|
107
|
+
owner: string;
|
|
108
|
+
/** coin type (`0x1::aptos_coin::AptosCoin`) or FA metadata address */
|
|
109
|
+
assetType: string;
|
|
110
|
+
/** raw amount (coin + fungible store balance combined by the indexer) */
|
|
111
|
+
amount: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Coin + fungible asset balances of many owners from the indexer (`current_fungible_asset_balances`),
|
|
115
|
+
* `chunkSize` owners per query and paginated, so a few requests replace one node call per owner per token.
|
|
116
|
+
* Owners are matched by the indexer's 64-hex form, so short addresses are padded first.
|
|
117
|
+
*/
|
|
118
|
+
export declare function getFungibleAssetBalances({ chain, owners, chunkSize, pageSize, timeout, retries }: RequestOptions & {
|
|
119
|
+
owners: string[];
|
|
120
|
+
chunkSize?: number;
|
|
121
|
+
pageSize?: number;
|
|
122
|
+
}): Promise<FungibleAssetBalance[]>;
|
|
84
123
|
export declare function getLedgerInfo({ chain, archival }?: ChainOptions & {
|
|
85
124
|
archival?: boolean;
|
|
86
125
|
}): Promise<LedgerInfo>;
|
package/build/chains/aptos.js
CHANGED
|
@@ -3,13 +3,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.APT_DECIMALS = exports.APT_COIN_TYPE = exports.aptosChains = exports.ARCHIVAL_ENDPOINTS = exports.DEFAULT_ENDPOINTS = void 0;
|
|
6
|
+
exports.IndexerError = exports.APT_DECIMALS = exports.APT_COIN_TYPE = exports.aptosChains = exports.INDEXER_ENDPOINTS = exports.ARCHIVAL_ENDPOINTS = exports.DEFAULT_ENDPOINTS = void 0;
|
|
7
7
|
exports.isAptosChain = isAptosChain;
|
|
8
8
|
exports.getEndpointList = getEndpointList;
|
|
9
9
|
exports.getEndpoint = getEndpoint;
|
|
10
10
|
exports.getArchivalEndpointList = getArchivalEndpointList;
|
|
11
|
+
exports.getIndexerEndpointList = getIndexerEndpointList;
|
|
11
12
|
exports.get = get;
|
|
12
13
|
exports.post = post;
|
|
14
|
+
exports.indexerQuery = indexerQuery;
|
|
15
|
+
exports.getFungibleAssetBalances = getFungibleAssetBalances;
|
|
13
16
|
exports.getLedgerInfo = getLedgerInfo;
|
|
14
17
|
exports.getAccount = getAccount;
|
|
15
18
|
exports.getResources = getResources;
|
|
@@ -52,6 +55,9 @@ exports.parseTypeTag = parseTypeTag;
|
|
|
52
55
|
* retried on the archival endpoint (`<CHAIN>_ARCHIVAL_RPC`, default `archive.mainnet.aptoslabs.com`).
|
|
53
56
|
* Timestamp -> version lookups use a REST binary search over block heights instead of the
|
|
54
57
|
* aptoslabs indexer GraphQL API, so they work on any full node and on Movement.
|
|
58
|
+
* Indexer GraphQL queries (`indexerQuery`, `getFungibleAssetBalances`) go through the same transport:
|
|
59
|
+
* `<CHAIN>_INDEXER` (comma separated) overrides / adds to the default endpoint, with rotation, retries
|
|
60
|
+
* and the bearer key.
|
|
55
61
|
*
|
|
56
62
|
* Usage: `sdk.chains.aptos.getResource({ account: '0x1', type: '0x1::coin::CoinInfo<0x1::aptos_coin::AptosCoin>' })`
|
|
57
63
|
*/
|
|
@@ -73,6 +79,15 @@ exports.DEFAULT_ENDPOINTS = {
|
|
|
73
79
|
exports.ARCHIVAL_ENDPOINTS = {
|
|
74
80
|
aptos: 'https://archive.mainnet.aptoslabs.com',
|
|
75
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
* Indexer (Hasura GraphQL) endpoints. Override / add fallbacks with `<CHAIN>_INDEXER` (comma separated).
|
|
84
|
+
* The `<CHAIN>_API_KEY` bearer key is sent here too (aptoslabs keys cover both the full node and the indexer).
|
|
85
|
+
*/
|
|
86
|
+
exports.INDEXER_ENDPOINTS = {
|
|
87
|
+
// keyless public endpoints (third-party indexers like Nodit need a key: add them via <CHAIN>_INDEXER)
|
|
88
|
+
aptos: 'https://api.mainnet.aptoslabs.com/v1/graphql,https://indexer.mainnet.aptoslabs.com/v1/graphql',
|
|
89
|
+
move: 'https://indexer.mainnet.movementnetwork.xyz/v1/graphql',
|
|
90
|
+
};
|
|
76
91
|
exports.aptosChains = Object.keys(exports.DEFAULT_ENDPOINTS);
|
|
77
92
|
exports.APT_COIN_TYPE = '0x1::aptos_coin::AptosCoin';
|
|
78
93
|
exports.APT_DECIMALS = 8;
|
|
@@ -106,6 +121,16 @@ function getArchivalEndpointList({ chain } = {}) {
|
|
|
106
121
|
return [];
|
|
107
122
|
}
|
|
108
123
|
}
|
|
124
|
+
/** Indexer GraphQL endpoints for the chain (`<CHAIN>_INDEXER` env first, then built-in); empty when none. */
|
|
125
|
+
function getIndexerEndpointList({ chain } = {}) {
|
|
126
|
+
chain = resolveChain(chain);
|
|
127
|
+
try {
|
|
128
|
+
return (0, rpc_1.getEndpoints)(chain, exports.INDEXER_ENDPOINTS[chain], { envKey: `${chain.toUpperCase()}_INDEXER` });
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
109
134
|
function getHeaders(chain) {
|
|
110
135
|
const headers = { 'Content-Type': 'application/json' };
|
|
111
136
|
const apiKey = (0, env_1.getEnvValue)(`${chain.toUpperCase()}_API_KEY`);
|
|
@@ -154,10 +179,10 @@ async function request(options) {
|
|
|
154
179
|
}
|
|
155
180
|
}
|
|
156
181
|
async function requestOn(endpoints, options) {
|
|
157
|
-
var _a;
|
|
182
|
+
var _a, _b, _c;
|
|
158
183
|
const chain = resolveChain(options.chain);
|
|
159
|
-
const limiter = (0, rpc_1.getLimiter)(chain.toUpperCase(), 10);
|
|
160
|
-
const { method, path, params, body, allowNotFound, withMetadata, timeout = DEFAULT_TIMEOUT } = options;
|
|
184
|
+
const limiter = (0, rpc_1.getLimiter)((_a = options.limiterKey) !== null && _a !== void 0 ? _a : chain.toUpperCase(), 10);
|
|
185
|
+
const { method, path, params, body, allowNotFound, withMetadata, validate, timeout = DEFAULT_TIMEOUT } = options;
|
|
161
186
|
const headers = getHeaders(chain);
|
|
162
187
|
const config = { timeout, headers, params };
|
|
163
188
|
const res = await (0, rpc_1.withRetry)(async (attempt) => {
|
|
@@ -165,11 +190,11 @@ async function requestOn(endpoints, options) {
|
|
|
165
190
|
const url = (0, rpc_1.joinUrl)(base, path);
|
|
166
191
|
return limiter(async () => {
|
|
167
192
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
193
|
+
let response;
|
|
168
194
|
try {
|
|
169
|
-
|
|
195
|
+
response = method === 'post'
|
|
170
196
|
? await axios_1.default.post(url, body, config)
|
|
171
197
|
: await axios_1.default.get(url, config);
|
|
172
|
-
return withMetadata ? response : response.data;
|
|
173
198
|
}
|
|
174
199
|
catch (e) {
|
|
175
200
|
if (allowNotFound && isNotFound(e)) {
|
|
@@ -186,11 +211,14 @@ async function requestOn(endpoints, options) {
|
|
|
186
211
|
err._isCustomError = true; // already formatted: keeps status/errorCode intact through withRetry/formError
|
|
187
212
|
throw err;
|
|
188
213
|
}
|
|
214
|
+
if (validate)
|
|
215
|
+
validate(response.data);
|
|
216
|
+
return withMetadata ? response : response.data;
|
|
189
217
|
});
|
|
190
218
|
}, {
|
|
191
|
-
label: `${chain} ${method.toUpperCase()} ${path.split('?')[0].slice(0, 80)}`,
|
|
192
|
-
retries: (
|
|
193
|
-
shouldRetry: rpc_1.isRetryableError,
|
|
219
|
+
label: `${chain} ${method.toUpperCase()} ${(path || (0, rpc_1.shortUrl)(endpoints[0])).split('?')[0].slice(0, 80)}`,
|
|
220
|
+
retries: (_b = options.retries) !== null && _b !== void 0 ? _b : Math.max(3, endpoints.length),
|
|
221
|
+
shouldRetry: (_c = options.shouldRetry) !== null && _c !== void 0 ? _c : rpc_1.isRetryableError,
|
|
194
222
|
});
|
|
195
223
|
return res === NOT_FOUND ? null : res;
|
|
196
224
|
}
|
|
@@ -211,6 +239,66 @@ async function post({ chain, path, body, params, allowNotFound, timeout, retries
|
|
|
211
239
|
return request({ chain, method: 'post', path, body, params, allowNotFound, timeout, retries, archival });
|
|
212
240
|
}
|
|
213
241
|
// ---------------------------------------------------------------------------
|
|
242
|
+
// indexer (GraphQL)
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
/** GraphQL `errors` in a 200 response (bad query, or a rate limit the indexer reports in-band). */
|
|
245
|
+
class IndexerError extends Error {
|
|
246
|
+
constructor(errors) {
|
|
247
|
+
super(`aptos indexer: ${errors.map((e) => { var _a; return (_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : JSON.stringify(e); }).join('; ').slice(0, 300)}`);
|
|
248
|
+
this.name = 'IndexerError';
|
|
249
|
+
this.errors = errors;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
exports.IndexerError = IndexerError;
|
|
253
|
+
/**
|
|
254
|
+
* Run a GraphQL query against the chain's indexer, returning `data`. Same transport as the node calls:
|
|
255
|
+
* rotates over `<CHAIN>_INDEXER` endpoints (then the default), retries transport/5xx/429 failures with
|
|
256
|
+
* backoff and sends the `<CHAIN>_API_KEY` bearer key. GraphQL `errors` throw `IndexerError` and are
|
|
257
|
+
* only retried when they report a rate limit (a bad query fails fast).
|
|
258
|
+
*/
|
|
259
|
+
async function indexerQuery({ chain, query, variables = {}, timeout, retries }) {
|
|
260
|
+
chain = resolveChain(chain);
|
|
261
|
+
const endpoints = getIndexerEndpointList({ chain });
|
|
262
|
+
if (!endpoints.length)
|
|
263
|
+
throw new Error(`[${chain}] no indexer endpoint configured (env ${chain.toUpperCase()}_INDEXER)`);
|
|
264
|
+
const res = await requestOn(endpoints, {
|
|
265
|
+
chain, method: 'post', path: '', body: { query, variables }, timeout, retries,
|
|
266
|
+
limiterKey: `${chain.toUpperCase()}_INDEXER`,
|
|
267
|
+
validate: (data) => { var _a; if ((_a = data === null || data === void 0 ? void 0 : data.errors) === null || _a === void 0 ? void 0 : _a.length)
|
|
268
|
+
throw new IndexerError(data.errors); },
|
|
269
|
+
shouldRetry: (e) => e instanceof IndexerError ? (0, rpc_1.isRateLimitError)(e) : (0, rpc_1.isRetryableError)(e),
|
|
270
|
+
});
|
|
271
|
+
return res === null || res === void 0 ? void 0 : res.data;
|
|
272
|
+
}
|
|
273
|
+
const FA_BALANCES_QUERY = `query ($owners: [String!]!, $offset: Int!, $limit: Int!) {
|
|
274
|
+
current_fungible_asset_balances(where: { owner_address: { _in: $owners } }, offset: $offset, limit: $limit, order_by: [{ owner_address: asc }, { asset_type: asc }]) {
|
|
275
|
+
owner_address
|
|
276
|
+
asset_type
|
|
277
|
+
amount
|
|
278
|
+
}
|
|
279
|
+
}`;
|
|
280
|
+
/**
|
|
281
|
+
* Coin + fungible asset balances of many owners from the indexer (`current_fungible_asset_balances`),
|
|
282
|
+
* `chunkSize` owners per query and paginated, so a few requests replace one node call per owner per token.
|
|
283
|
+
* Owners are matched by the indexer's 64-hex form, so short addresses are padded first.
|
|
284
|
+
*/
|
|
285
|
+
async function getFungibleAssetBalances({ chain, owners, chunkSize = 50, pageSize = 1000, timeout, retries }) {
|
|
286
|
+
var _a;
|
|
287
|
+
const unique = [...new Set(owners.map(normalizeAddress))];
|
|
288
|
+
const out = [];
|
|
289
|
+
for (const chunk of (0, rpc_1.sliceIntoChunks)(unique, chunkSize)) {
|
|
290
|
+
for (let offset = 0;; offset += pageSize) {
|
|
291
|
+
const data = await indexerQuery({ chain, query: FA_BALANCES_QUERY, variables: { owners: chunk, offset, limit: pageSize }, timeout, retries });
|
|
292
|
+
const rows = (_a = data === null || data === void 0 ? void 0 : data.current_fungible_asset_balances) !== null && _a !== void 0 ? _a : [];
|
|
293
|
+
for (const { owner_address, asset_type, amount } of rows)
|
|
294
|
+
out.push({ owner: owner_address, assetType: asset_type, amount: String(amount) });
|
|
295
|
+
if (rows.length < pageSize)
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
214
302
|
// ledger / accounts / resources
|
|
215
303
|
// ---------------------------------------------------------------------------
|
|
216
304
|
function microsToSeconds(value) {
|
package/build/chains/utxo.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type ProviderKind = 'esplora' | 'blockbook' | 'blockcypher' | 'tatum' | 'insight' | 'whatsonchain' | 'blockchair' | 'haskoin' | 'kaspa' | 'mvc';
|
|
1
2
|
export interface UtxoChainConfig {
|
|
2
3
|
decimals: number;
|
|
3
4
|
/** explorer base urls in fallback order, overridable with `<CHAIN>_EXPLORER_API` */
|
|
@@ -6,9 +7,13 @@ export interface UtxoChainConfig {
|
|
|
6
7
|
concurrency: number;
|
|
7
8
|
/** pause before every request (free-tier rate limits), applied inside the limiter */
|
|
8
9
|
sleepMs?: number;
|
|
10
|
+
/** dialect for endpoints whose host is not recognised, overridable with `<CHAIN>_EXPLORER_PROVIDER` */
|
|
11
|
+
provider?: ProviderKind;
|
|
9
12
|
}
|
|
10
13
|
export declare const CHAINS: Record<string, UtxoChainConfig>;
|
|
14
|
+
/** Built-in chains; env-defined chains (see `getChainConfig`) are not listed here. */
|
|
11
15
|
export declare const utxoChains: string[];
|
|
16
|
+
export declare const PROVIDER_KINDS: ProviderKind[];
|
|
12
17
|
/** mempool.space base url, used for the timestamp -> block lookup (`BITCOIN_MEMPOOL_API`). */
|
|
13
18
|
export declare const DEFAULT_MEMPOOL_API = "https://mempool.space/api";
|
|
14
19
|
/** blockchain.info multiaddr endpoint for bulk bitcoin balances (`BITCOIN_MULTIADDR_API`). */
|
|
@@ -19,7 +24,10 @@ export declare const MULTIADDR_CHUNK_SIZE = 100;
|
|
|
19
24
|
export declare const MULTIADDR_SLEEP_MS = 10000;
|
|
20
25
|
/** bitcoin cache api accepts up to 700 addresses per call */
|
|
21
26
|
export declare const CACHE_API_CHUNK_SIZE = 700;
|
|
27
|
+
/** True for built-in chains and for chains enabled through env (`<CHAIN>_EXPLORER_API` / `<CHAIN>_WHITELISTED_RPC`). */
|
|
22
28
|
export declare function isUtxoChain(chain: any): boolean;
|
|
29
|
+
/** Resolved config for `chain`: `CHAINS[chain]` or the env-defined config. Throws for unknown chains. */
|
|
30
|
+
export declare function getChainConfig(chain: string): UtxoChainConfig;
|
|
23
31
|
/** Explorer base urls for `chain`: env `<CHAIN>_EXPLORER_API` wins, then `CHAINS[chain].endpoints`. */
|
|
24
32
|
export declare function getEndpoints({ chain }?: {
|
|
25
33
|
chain?: string;
|
package/build/chains/utxo.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CACHE_API_CHUNK_SIZE = exports.MULTIADDR_SLEEP_MS = exports.MULTIADDR_CHUNK_SIZE = exports.DEFAULT_MULTIADDR_API = exports.DEFAULT_MEMPOOL_API = exports.utxoChains = exports.CHAINS = void 0;
|
|
3
|
+
exports.CACHE_API_CHUNK_SIZE = exports.MULTIADDR_SLEEP_MS = exports.MULTIADDR_CHUNK_SIZE = exports.DEFAULT_MULTIADDR_API = exports.DEFAULT_MEMPOOL_API = exports.PROVIDER_KINDS = exports.utxoChains = exports.CHAINS = void 0;
|
|
4
4
|
exports.isUtxoChain = isUtxoChain;
|
|
5
|
+
exports.getChainConfig = getChainConfig;
|
|
5
6
|
exports.getEndpoints = getEndpoints;
|
|
6
7
|
exports.isBitcoinAddress = isBitcoinAddress;
|
|
7
8
|
exports.toBigInt = toBigInt;
|
|
@@ -26,14 +27,14 @@ exports.getBlockAtTimestamp = getBlockAtTimestamp;
|
|
|
26
27
|
/**
|
|
27
28
|
* Balance client for bitcoin-like UTXO chains via public explorer APIs
|
|
28
29
|
* (esplora/blockstream, mempool.space, blockbook, blockcypher, tatum, insight,
|
|
29
|
-
* whatsonchain, blockchair, kaspa REST, mvcapi).
|
|
30
|
+
* whatsonchain, blockchair, haskoin, kaspa REST, mvcapi).
|
|
30
31
|
*
|
|
31
32
|
* Replaces the chain access parts of:
|
|
32
33
|
* - DefiLlama-Adapters projects/helper/chain/bitcoin.js (getBalanceNow,
|
|
33
34
|
* getBalance(addr, timestamp), getCachedBitcoinBalances / cachedBTCBalCall,
|
|
34
35
|
* blockchain.info multiaddr batching)
|
|
35
36
|
* - DefiLlama-Adapters projects/helper/chain/litecoin.js, doge.js, dash.js,
|
|
36
|
-
* bsv.js, zcash.js, kaspa.js, mvc.js (per-chain getBalance)
|
|
37
|
+
* bsv.js, zcash.js, kaspa.js, mvc.js, bitcoincash.js (per-chain getBalance)
|
|
37
38
|
* - defillama-server coins/src/adapters/other/others2.ts getBtcBalanceSats
|
|
38
39
|
* (blockstream -> mempool.space failover)
|
|
39
40
|
*
|
|
@@ -42,8 +43,14 @@ exports.getBlockAtTimestamp = getBlockAtTimestamp;
|
|
|
42
43
|
* sompi, ...) as a decimal string.
|
|
43
44
|
*
|
|
44
45
|
* Explorer base urls are overridable per chain with `<CHAIN>_EXPLORER_API`
|
|
45
|
-
* (comma separated fallbacks, also `SDK_` / `LLAMA_SDK_` prefixed).
|
|
46
|
-
*
|
|
46
|
+
* (comma separated fallbacks, also `SDK_` / `LLAMA_SDK_` prefixed). Known hosts
|
|
47
|
+
* pick their dialect automatically; unknown hosts use `<CHAIN>_EXPLORER_PROVIDER`
|
|
48
|
+
* (one of the provider kinds below) and otherwise the chain's default dialect.
|
|
49
|
+
*
|
|
50
|
+
* Chains not listed in `CHAINS` can be enabled purely through env: setting
|
|
51
|
+
* `<CHAIN>_EXPLORER_API` (or `<CHAIN>_WHITELISTED_RPC`) makes `isUtxoChain`
|
|
52
|
+
* accept the chain, with `<CHAIN>_DECIMALS` (default 8), `<CHAIN>_EXPLORER_PROVIDER`
|
|
53
|
+
* (default esplora) and `<CHAIN>_RPC_CONCURRENCY` (default 5) completing the config.
|
|
47
54
|
*
|
|
48
55
|
* Usage: `sdk.chains.utxo.getBalance({ chain: 'bitcoin', address: 'bc1q...' })`
|
|
49
56
|
*/
|
|
@@ -56,6 +63,13 @@ exports.CHAINS = {
|
|
|
56
63
|
endpoints: ['https://blockstream.info/api', 'https://mempool.space/api', 'https://rpc.ankr.com/http/btc_blockbook/api/v2'],
|
|
57
64
|
concurrency: 5,
|
|
58
65
|
},
|
|
66
|
+
bitcoincash: {
|
|
67
|
+
decimals: 8,
|
|
68
|
+
// haskoin (also mirrored by blockchain.com) takes cashaddr with or without the
|
|
69
|
+
// `bitcoincash:` prefix as well as legacy base58 addresses
|
|
70
|
+
endpoints: ['https://api.haskoin.com/bch', 'https://api.blockchain.info/haskoin-store/bch', 'https://api.tatum.io/v3/bcash', 'https://api.blockchair.com/bitcoin-cash'],
|
|
71
|
+
concurrency: 5,
|
|
72
|
+
},
|
|
59
73
|
litecoin: {
|
|
60
74
|
decimals: 8,
|
|
61
75
|
endpoints: ['https://litecoinspace.org/api', 'https://api.blockchair.com/litecoin'],
|
|
@@ -81,8 +95,10 @@ exports.CHAINS = {
|
|
|
81
95
|
},
|
|
82
96
|
zcash: {
|
|
83
97
|
decimals: 8,
|
|
84
|
-
// blockchair free tier: ~30 req/min per IP
|
|
85
|
-
|
|
98
|
+
// blockchair free tier: ~30 req/min per IP; zelcore blockbook is unthrottled; nownodes needs NOWNODES_API_KEY.
|
|
99
|
+
// Dead or gated as of 2026-10: zcha.in, zcashblockexplorer.com, zcashnetwork.info, zcashexplorer.app (403),
|
|
100
|
+
// trezor blockbooks (cloudflare), ankr zec_blockbook (plan-gated), cryptoid (no zec), tatum (no zec), 3xpl (token)
|
|
101
|
+
endpoints: ['https://api.blockchair.com/zcash', 'https://blockbook.zec.zelcore.io/api/v2', 'https://zecbook.nownodes.io/api/v2'],
|
|
86
102
|
concurrency: 1,
|
|
87
103
|
sleepMs: 5000,
|
|
88
104
|
},
|
|
@@ -97,7 +113,11 @@ exports.CHAINS = {
|
|
|
97
113
|
concurrency: 5,
|
|
98
114
|
},
|
|
99
115
|
};
|
|
116
|
+
/** Built-in chains; env-defined chains (see `getChainConfig`) are not listed here. */
|
|
100
117
|
exports.utxoChains = Object.keys(exports.CHAINS);
|
|
118
|
+
exports.PROVIDER_KINDS = ['esplora', 'blockbook', 'blockcypher', 'tatum', 'insight', 'whatsonchain', 'blockchair', 'haskoin', 'kaspa', 'mvc'];
|
|
119
|
+
const ENV_CHAIN_DEFAULT_DECIMALS = 8;
|
|
120
|
+
const ENV_CHAIN_DEFAULT_CONCURRENCY = 5;
|
|
101
121
|
/** mempool.space base url, used for the timestamp -> block lookup (`BITCOIN_MEMPOOL_API`). */
|
|
102
122
|
exports.DEFAULT_MEMPOOL_API = 'https://mempool.space/api';
|
|
103
123
|
/** blockchain.info multiaddr endpoint for bulk bitcoin balances (`BITCOIN_MULTIADDR_API`). */
|
|
@@ -111,15 +131,55 @@ exports.CACHE_API_CHUNK_SIZE = 700;
|
|
|
111
131
|
const DEFAULT_CHAIN = 'bitcoin';
|
|
112
132
|
/** UTXO snapshots newer than this are read from `/utxo`, older ones by replaying `/txs` */
|
|
113
133
|
const RECENT_WINDOW_SECONDS = 30 * 60;
|
|
134
|
+
/** True for built-in chains and for chains enabled through env (`<CHAIN>_EXPLORER_API` / `<CHAIN>_WHITELISTED_RPC`). */
|
|
114
135
|
function isUtxoChain(chain) {
|
|
115
|
-
|
|
136
|
+
if (typeof chain !== 'string')
|
|
137
|
+
return false;
|
|
138
|
+
if (Object.prototype.hasOwnProperty.call(exports.CHAINS, chain))
|
|
139
|
+
return true;
|
|
140
|
+
return getEnvChainConfig(chain) !== undefined;
|
|
141
|
+
}
|
|
142
|
+
function parseProviderKind(value, chain) {
|
|
143
|
+
if (!value)
|
|
144
|
+
return undefined;
|
|
145
|
+
const kind = value.trim().toLowerCase();
|
|
146
|
+
if (!exports.PROVIDER_KINDS.includes(kind))
|
|
147
|
+
throw new Error(`utxo: unknown provider "${value}" for ${chain} (${chain.toUpperCase()}_EXPLORER_PROVIDER), expected one of ${exports.PROVIDER_KINDS.join(', ')}`);
|
|
148
|
+
return kind;
|
|
116
149
|
}
|
|
117
|
-
|
|
118
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Config for a chain that is not in `CHAINS`, built from env: `<CHAIN>_EXPLORER_API`
|
|
152
|
+
* (or `<CHAIN>_WHITELISTED_RPC`) enables it, `<CHAIN>_DECIMALS` defaults to 8 and
|
|
153
|
+
* `<CHAIN>_EXPLORER_PROVIDER` picks the dialect for unrecognised hosts (default esplora).
|
|
154
|
+
*/
|
|
155
|
+
function getEnvChainConfig(chain) {
|
|
156
|
+
var _a;
|
|
157
|
+
if (!/^[a-z0-9_-]+$/i.test(chain))
|
|
158
|
+
return undefined;
|
|
159
|
+
const key = chain.toUpperCase();
|
|
160
|
+
const endpoints = (0, rpc_1.toEndpointList)((0, env_1.getEnvValue)(`${key}_EXPLORER_API`));
|
|
161
|
+
if (!endpoints.length && !(0, rpc_1.toEndpointList)((0, env_1.getWhitelistedRPCs)(key)).length)
|
|
162
|
+
return undefined;
|
|
163
|
+
const decimals = (0, env_1.getEnvValue)(`${key}_DECIMALS`);
|
|
164
|
+
if (decimals !== undefined && !/^\d+$/.test(decimals.trim()))
|
|
165
|
+
throw new Error(`utxo: invalid ${key}_DECIMALS "${decimals}"`);
|
|
166
|
+
return {
|
|
167
|
+
decimals: decimals === undefined ? ENV_CHAIN_DEFAULT_DECIMALS : +decimals,
|
|
168
|
+
// env endpoints are picked up by resolveEndpoints, keep defaults empty so they are not listed twice
|
|
169
|
+
endpoints: [],
|
|
170
|
+
concurrency: ENV_CHAIN_DEFAULT_CONCURRENCY,
|
|
171
|
+
provider: (_a = parseProviderKind((0, env_1.getEnvValue)(`${key}_EXPLORER_PROVIDER`), chain)) !== null && _a !== void 0 ? _a : 'esplora',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** Resolved config for `chain`: `CHAINS[chain]` or the env-defined config. Throws for unknown chains. */
|
|
175
|
+
function getChainConfig(chain) {
|
|
176
|
+
var _a;
|
|
177
|
+
const config = (_a = exports.CHAINS[chain]) !== null && _a !== void 0 ? _a : getEnvChainConfig(chain);
|
|
119
178
|
if (!config)
|
|
120
|
-
throw new Error(`utxo: unsupported chain "${chain}" (known: ${exports.utxoChains.join(', ')})`);
|
|
179
|
+
throw new Error(`utxo: unsupported chain "${chain}" (known: ${exports.utxoChains.join(', ')}; set ${String(chain).toUpperCase()}_EXPLORER_API to add it)`);
|
|
121
180
|
return config;
|
|
122
181
|
}
|
|
182
|
+
const getConfig = getChainConfig;
|
|
123
183
|
/** Explorer base urls for `chain`: env `<CHAIN>_EXPLORER_API` wins, then `CHAINS[chain].endpoints`. */
|
|
124
184
|
function getEndpoints({ chain = DEFAULT_CHAIN } = {}) {
|
|
125
185
|
const config = getConfig(chain);
|
|
@@ -244,7 +304,7 @@ function parseBalanceResponse(json, { address, decimals }) {
|
|
|
244
304
|
// insight (dash): `balance` is in coins, `balanceSat` in base units
|
|
245
305
|
if (json.balanceSat !== undefined)
|
|
246
306
|
return toBigInt(json.balanceSat).toString();
|
|
247
|
-
// whatsonchain (bsv) / mvcapi: `{ confirmed, unconfirmed }` in base units
|
|
307
|
+
// whatsonchain (bsv) / mvcapi / haskoin (bitcoincash): `{ confirmed, unconfirmed }` in base units
|
|
248
308
|
if (json.confirmed !== undefined) {
|
|
249
309
|
if (typeof json.confirmed !== 'number' && typeof json.confirmed !== 'string')
|
|
250
310
|
throw new Error(`utxo: unexpected balance response for ${address}`);
|
|
@@ -266,10 +326,14 @@ function parseBalanceResponse(json, { address, decimals }) {
|
|
|
266
326
|
}
|
|
267
327
|
const PROVIDERS = [
|
|
268
328
|
{ kind: 'blockchair', match: /blockchair\.com/i, balancePath: (a) => `/dashboards/address/${a}?limit=0` },
|
|
269
|
-
{ kind: 'tatum', match: /tatum\.io/i, balancePath: (a) => `/address/balance/${a}`, headers: tatumHeaders },
|
|
329
|
+
{ kind: 'tatum', match: /tatum\.io/i, balancePath: (a) => `/address/balance/${a}`, headers: tatumHeaders, keyEnv: 'TATUM_API_KEY' },
|
|
330
|
+
// nownodes `<coin>book.nownodes.io` hosts are blockbooks behind an `api-key` header
|
|
331
|
+
{ kind: 'blockbook', match: /nownodes\.io/i, balancePath: (a) => `/address/${a}`, headers: nownodesHeaders, keyEnv: 'NOWNODES_API_KEY' },
|
|
270
332
|
{ kind: 'blockcypher', match: /blockcypher\.com/i, balancePath: (a) => `/addrs/${a}/balance` },
|
|
271
333
|
{ kind: 'insight', match: /insight/i, balancePath: (a) => `/addr/${a}` },
|
|
272
334
|
{ kind: 'whatsonchain', match: /whatsonchain\.com/i, balancePath: (a) => `/address/${a}/balance` },
|
|
335
|
+
// haskoin store (api.haskoin.com, api.blockchain.info/haskoin-store): `{ confirmed, unconfirmed }` in base units
|
|
336
|
+
{ kind: 'haskoin', match: /haskoin/i, balancePath: (a) => `/address/${a}/balance` },
|
|
273
337
|
{ kind: 'kaspa', match: /kaspa/i, balancePath: (a) => `/addresses/${encodeURIComponent(a)}/balance` },
|
|
274
338
|
{ kind: 'mvc', match: /mvcapi\.com/i, balancePath: (a) => `/address/${a}/balance` },
|
|
275
339
|
{ kind: 'blockbook', match: /blockbook/i, balancePath: (a) => `/address/${a}` },
|
|
@@ -282,13 +346,35 @@ function tatumHeaders() {
|
|
|
282
346
|
return undefined;
|
|
283
347
|
return { 'x-api-key': key, 'User-Agent': 'Thunder Client (https://www.thunderclient.com)' };
|
|
284
348
|
}
|
|
285
|
-
|
|
349
|
+
function nownodesHeaders() {
|
|
350
|
+
const key = (0, env_1.getEnvValue)('NOWNODES_API_KEY');
|
|
351
|
+
if (!key)
|
|
352
|
+
return undefined;
|
|
353
|
+
return { 'api-key': key };
|
|
354
|
+
}
|
|
355
|
+
const ESPLORA_PROVIDER = PROVIDERS[PROVIDERS.length - 1];
|
|
356
|
+
/** Provider for an explicitly requested dialect; keyless entries win over keyed host-specific ones. */
|
|
357
|
+
function providerByKind(kind) {
|
|
358
|
+
var _a;
|
|
359
|
+
return (_a = PROVIDERS.find(p => p.kind === kind && !p.headers)) !== null && _a !== void 0 ? _a : PROVIDERS.find(p => p.kind === kind);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Provider dialect for an endpoint. Known hosts match directly; otherwise
|
|
363
|
+
* `<CHAIN>_EXPLORER_PROVIDER` / `config.provider` wins, then the dialect of the
|
|
364
|
+
* chain's first default endpoint, then esplora.
|
|
365
|
+
*/
|
|
286
366
|
function detectProvider(endpoint, chain) {
|
|
367
|
+
var _a;
|
|
287
368
|
const found = PROVIDERS.find(p => p.match.test(endpoint));
|
|
288
369
|
if (found)
|
|
289
370
|
return found;
|
|
290
|
-
const
|
|
291
|
-
|
|
371
|
+
const config = getConfig(chain);
|
|
372
|
+
const kind = (_a = parseProviderKind((0, env_1.getEnvValue)(`${chain.toUpperCase()}_EXPLORER_PROVIDER`), chain)) !== null && _a !== void 0 ? _a : config.provider;
|
|
373
|
+
if (kind)
|
|
374
|
+
return providerByKind(kind);
|
|
375
|
+
const firstDefault = config.endpoints[0];
|
|
376
|
+
const fallback = firstDefault ? PROVIDERS.find(p => p.match.test(firstDefault)) : undefined;
|
|
377
|
+
return fallback !== null && fallback !== void 0 ? fallback : ESPLORA_PROVIDER;
|
|
292
378
|
}
|
|
293
379
|
/** Endpoints of `chain` that speak esplora (needed for utxo / tx / block calls). */
|
|
294
380
|
function esploraEndpoints(chain) {
|
|
@@ -306,7 +392,7 @@ async function esploraGet(chain, path, { retries, timeout } = {}) {
|
|
|
306
392
|
* when all of them fail.
|
|
307
393
|
*/
|
|
308
394
|
async function getBalance({ chain = DEFAULT_CHAIN, address, retries = 2, timeout }) {
|
|
309
|
-
var _a;
|
|
395
|
+
var _a, _b;
|
|
310
396
|
const { decimals } = getConfig(chain);
|
|
311
397
|
const endpoints = getEndpoints({ chain });
|
|
312
398
|
const errors = [];
|
|
@@ -314,7 +400,7 @@ async function getBalance({ chain = DEFAULT_CHAIN, address, retries = 2, timeout
|
|
|
314
400
|
const provider = detectProvider(endpoint, chain);
|
|
315
401
|
const headers = provider.headers ? provider.headers() : undefined;
|
|
316
402
|
if (provider.headers && !headers) {
|
|
317
|
-
(0, debugLog_1.debugLog)(`[chains.utxo] ${chain}: skipping ${endpoint}, no api key configured (
|
|
403
|
+
(0, debugLog_1.debugLog)(`[chains.utxo] ${chain}: skipping ${endpoint}, no api key configured (${(_a = provider.keyEnv) !== null && _a !== void 0 ? _a : 'api key env'})`);
|
|
318
404
|
continue;
|
|
319
405
|
}
|
|
320
406
|
try {
|
|
@@ -322,7 +408,7 @@ async function getBalance({ chain = DEFAULT_CHAIN, address, retries = 2, timeout
|
|
|
322
408
|
return parseBalanceResponse(data, { address, decimals });
|
|
323
409
|
}
|
|
324
410
|
catch (e) {
|
|
325
|
-
const message = String((
|
|
411
|
+
const message = String((_b = e === null || e === void 0 ? void 0 : e.message) !== null && _b !== void 0 ? _b : e).slice(0, 200);
|
|
326
412
|
errors.push(`${endpoint}: ${message}`);
|
|
327
413
|
(0, debugLog_1.debugLog)(`[chains.utxo] ${chain} balance error for ${address} via ${endpoint}: ${message}`);
|
|
328
414
|
}
|