@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.
@@ -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
  }
@@ -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
  };
@@ -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>;
@@ -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
- const response = method === 'post'
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: (_a = options.retries) !== null && _a !== void 0 ? _a : Math.max(3, endpoints.length),
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) {
@@ -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;
@@ -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). Unknown
46
- * hosts are assumed to speak the chain's default provider dialect.
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
- endpoints: ['https://api.blockchair.com/zcash'],
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
- return typeof chain === 'string' && Object.prototype.hasOwnProperty.call(exports.CHAINS, chain);
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
- function getConfig(chain) {
118
- const config = exports.CHAINS[chain];
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
- /** Provider dialect for an endpoint; unknown hosts use the chain's first default endpoint's dialect. */
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 fallback = PROVIDERS.find(p => p.match.test(getConfig(chain).endpoints[0]));
291
- return fallback !== null && fallback !== void 0 ? fallback : PROVIDERS[PROVIDERS.length - 1];
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 (TATUM_API_KEY)`);
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((_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e).slice(0, 200);
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
  }