@defillama/sdk 5.0.240 → 5.0.244
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/Balances.d.ts +139 -0
- package/build/Balances.js +363 -0
- package/build/ChainApi.d.ts +142 -0
- package/build/ChainApi.js +310 -0
- package/build/abi/abi2.d.ts +5 -0
- package/build/abi/abi2.js +215 -0
- package/build/abi/cachedABIs.d.ts +4 -0
- package/build/abi/cachedABIs.js +8 -0
- package/build/abi/convertResults.d.ts +5 -0
- package/build/abi/convertResults.js +60 -0
- package/build/abi/index.d.ts +31 -0
- package/build/abi/index.js +327 -0
- package/build/abi/multicall.d.ts +23 -0
- package/build/abi/multicall.js +383 -0
- package/build/abi/multicall3.d.ts +7 -0
- package/build/abi/multicall3.js +405 -0
- package/build/abi/rpcCall.d.ts +4 -0
- package/build/abi/rpcCall.js +62 -0
- package/build/abi/tron.d.ts +33 -0
- package/build/abi/tron.js +118 -0
- package/build/api.d.ts +10 -0
- package/build/api.js +45 -0
- package/build/api2.d.ts +5 -0
- package/build/api2.js +41 -0
- package/build/chains/algorand.d.ts +277 -0
- package/build/chains/algorand.js +618 -0
- package/build/chains/aptos.d.ts +269 -0
- package/build/chains/aptos.js +700 -0
- package/build/chains/cardano.d.ts +242 -0
- package/build/chains/cardano.js +407 -0
- package/build/chains/cosmos.d.ts +252 -0
- package/build/chains/cosmos.js +623 -0
- package/build/chains/icp.d.ts +235 -0
- package/build/chains/icp.js +1040 -0
- package/build/chains/index.d.ts +23 -0
- package/build/chains/index.js +59 -0
- package/build/chains/near.d.ts +192 -0
- package/build/chains/near.js +446 -0
- package/build/chains/rpc.d.ts +125 -0
- package/build/chains/rpc.js +332 -0
- package/build/chains/starknet.d.ts +205 -0
- package/build/chains/starknet.js +886 -0
- package/build/chains/stellar.d.ts +410 -0
- package/build/chains/stellar.js +1082 -0
- package/build/chains/substrate.d.ts +333 -0
- package/build/chains/substrate.js +1042 -0
- package/build/chains/sui.d.ts +392 -0
- package/build/chains/sui.js +1213 -0
- package/build/chains/svm.d.ts +340 -0
- package/build/chains/svm.js +764 -0
- package/build/chains/tezos.d.ts +225 -0
- package/build/chains/tezos.js +412 -0
- package/build/chains/ton.d.ts +353 -0
- package/build/chains/ton.js +788 -0
- package/build/chains/tron.d.ts +45 -0
- package/build/chains/tron.js +110 -0
- package/build/chains/utxo.d.ts +210 -0
- package/build/chains/utxo.js +678 -0
- package/build/chains/xrpl.d.ts +179 -0
- package/build/chains/xrpl.js +467 -0
- package/build/computeTVL/blocks.d.ts +1 -0
- package/build/computeTVL/blocks.js +12 -0
- package/build/computeTVL/humanizeNumber.d.ts +1 -0
- package/build/computeTVL/humanizeNumber.js +19 -0
- package/build/erc20/index.d.ts +31 -0
- package/build/erc20/index.js +56 -0
- package/build/eth/index.d.ts +22 -0
- package/build/eth/index.js +118 -0
- package/build/general.d.ts +5 -0
- package/build/general.js +20 -0
- package/build/generalUtil.d.ts +8 -0
- package/build/generalUtil.js +73 -0
- package/build/index.d.ts +33 -0
- package/build/index.js +75 -0
- package/build/providers.json +1 -0
- package/build/types.d.ts +76 -0
- package/build/types.js +2 -0
- package/build/util/LlamaProvider.d.ts +46 -0
- package/build/util/LlamaProvider.js +343 -0
- package/build/util/blocks.d.ts +36 -0
- package/build/util/blocks.js +500 -0
- package/build/util/cache.d.ts +45 -0
- package/build/util/cache.js +309 -0
- package/build/util/callStats.d.ts +74 -0
- package/build/util/callStats.js +385 -0
- package/build/util/chainUtils/data.json +1459 -0
- package/build/util/chainUtils/index.d.ts +11 -0
- package/build/util/chainUtils/index.js +136 -0
- package/build/util/coins.d.ts +19 -0
- package/build/util/coins.js +124 -0
- package/build/util/common.d.ts +34 -0
- package/build/util/common.js +376 -0
- package/build/util/computeTVL.d.ts +19 -0
- package/build/util/computeTVL.js +102 -0
- package/build/util/cosmos.d.ts +1 -0
- package/build/util/cosmos.js +8 -0
- package/build/util/debugLog.d.ts +4 -0
- package/build/util/debugLog.js +18 -0
- package/build/util/discord.d.ts +1 -0
- package/build/util/discord.js +33 -0
- package/build/util/elastic.d.ts +43 -0
- package/build/util/elastic.js +113 -0
- package/build/util/env.d.ts +20 -0
- package/build/util/env.js +176 -0
- package/build/util/graph.d.ts +8 -0
- package/build/util/graph.js +50 -0
- package/build/util/index.d.ts +32 -0
- package/build/util/index.js +227 -0
- package/build/util/indexer.compatibility.d.ts +6 -0
- package/build/util/indexer.compatibility.js +64 -0
- package/build/util/indexer.d.ts +92 -0
- package/build/util/indexer.js +854 -0
- package/build/util/indexerSql.d.ts +9 -0
- package/build/util/indexerSql.js +102 -0
- package/build/util/internal-cache.d.ts +11 -0
- package/build/util/internal-cache.js +77 -0
- package/build/util/logs.d.ts +58 -0
- package/build/util/logs.decode.shared.d.ts +8 -0
- package/build/util/logs.decode.shared.js +289 -0
- package/build/util/logs.js +432 -0
- package/build/util/promisePool.d.ts +8 -0
- package/build/util/promisePool.js +31 -0
- package/build/util/r2.d.ts +4 -0
- package/build/util/r2.js +52 -0
- package/build/util/removeRedundantRPCs.d.ts +1 -0
- package/build/util/removeRedundantRPCs.js +49 -0
- package/build/util/transactions.d.ts +35 -0
- package/build/util/transactions.js +22 -0
- package/build/util/updateChainLabels.d.ts +1 -0
- package/build/util/updateChainLabels.js +5 -0
- package/build/util/updateProviderList.d.ts +1 -0
- package/build/util/updateProviderList.js +275 -0
- package/package.json +3 -2
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
"use strict";
|
|
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.PROVIDER_KINDS = exports.utxoChains = exports.CHAINS = void 0;
|
|
4
|
+
exports.isUtxoChain = isUtxoChain;
|
|
5
|
+
exports.getChainConfig = getChainConfig;
|
|
6
|
+
exports.getEndpoints = getEndpoints;
|
|
7
|
+
exports.isBitcoinAddress = isBitcoinAddress;
|
|
8
|
+
exports.toBigInt = toBigInt;
|
|
9
|
+
exports.toBaseUnits = toBaseUnits;
|
|
10
|
+
exports.fromBaseUnits = fromBaseUnits;
|
|
11
|
+
exports.parseBlockstreamAddressStats = parseBlockstreamAddressStats;
|
|
12
|
+
exports.parseBalanceResponse = parseBalanceResponse;
|
|
13
|
+
exports.getBalance = getBalance;
|
|
14
|
+
exports.getBalances = getBalances;
|
|
15
|
+
exports.getMultiaddrEndpoints = getMultiaddrEndpoints;
|
|
16
|
+
exports.getBitcoinBalancesMultiaddr = getBitcoinBalancesMultiaddr;
|
|
17
|
+
exports.getBitcoinCacheApi = getBitcoinCacheApi;
|
|
18
|
+
exports.getCachedBitcoinTotal = getCachedBitcoinTotal;
|
|
19
|
+
exports.getTotalBalance = getTotalBalance;
|
|
20
|
+
exports.getUtxos = getUtxos;
|
|
21
|
+
exports.getAddressTxs = getAddressTxs;
|
|
22
|
+
exports.getBitcoinBalanceAt = getBitcoinBalanceAt;
|
|
23
|
+
exports.getLatestBlock = getLatestBlock;
|
|
24
|
+
exports.getBlock = getBlock;
|
|
25
|
+
exports.getMempoolEndpoints = getMempoolEndpoints;
|
|
26
|
+
exports.getBlockAtTimestamp = getBlockAtTimestamp;
|
|
27
|
+
/**
|
|
28
|
+
* Balance client for bitcoin-like UTXO chains via public explorer APIs
|
|
29
|
+
* (esplora/blockstream, mempool.space, blockbook, blockcypher, tatum, insight,
|
|
30
|
+
* whatsonchain, blockchair, haskoin, kaspa REST, mvcapi).
|
|
31
|
+
*
|
|
32
|
+
* Replaces the chain access parts of:
|
|
33
|
+
* - DefiLlama-Adapters projects/helper/chain/bitcoin.js (getBalanceNow,
|
|
34
|
+
* getBalance(addr, timestamp), getCachedBitcoinBalances / cachedBTCBalCall,
|
|
35
|
+
* blockchain.info multiaddr batching)
|
|
36
|
+
* - DefiLlama-Adapters projects/helper/chain/litecoin.js, doge.js, dash.js,
|
|
37
|
+
* bsv.js, zcash.js, kaspa.js, mvc.js, bitcoincash.js (per-chain getBalance)
|
|
38
|
+
* - defillama-server coins/src/adapters/other/others2.ts getBtcBalanceSats
|
|
39
|
+
* (blockstream -> mempool.space failover)
|
|
40
|
+
*
|
|
41
|
+
* TVL coupling (sumTokens, Balances, getUniqueAddresses) is intentionally left
|
|
42
|
+
* out; every balance is returned in base units (satoshis, litoshis, zatoshi,
|
|
43
|
+
* sompi, ...) as a decimal string.
|
|
44
|
+
*
|
|
45
|
+
* Explorer base urls are overridable per chain with `<CHAIN>_EXPLORER_API`
|
|
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.
|
|
54
|
+
*
|
|
55
|
+
* Usage: `sdk.chains.utxo.getBalance({ chain: 'bitcoin', address: 'bc1q...' })`
|
|
56
|
+
*/
|
|
57
|
+
const debugLog_1 = require("../util/debugLog");
|
|
58
|
+
const env_1 = require("../util/env");
|
|
59
|
+
const rpc_1 = require("./rpc");
|
|
60
|
+
exports.CHAINS = {
|
|
61
|
+
bitcoin: {
|
|
62
|
+
decimals: 8,
|
|
63
|
+
endpoints: ['https://blockstream.info/api', 'https://mempool.space/api', 'https://rpc.ankr.com/http/btc_blockbook/api/v2'],
|
|
64
|
+
concurrency: 5,
|
|
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
|
+
},
|
|
73
|
+
litecoin: {
|
|
74
|
+
decimals: 8,
|
|
75
|
+
endpoints: ['https://litecoinspace.org/api', 'https://api.blockchair.com/litecoin'],
|
|
76
|
+
concurrency: 5,
|
|
77
|
+
},
|
|
78
|
+
doge: {
|
|
79
|
+
decimals: 8,
|
|
80
|
+
// blockcypher free tier: ~3 req/s, 100 req/h per IP
|
|
81
|
+
endpoints: ['https://api.blockcypher.com/v1/doge/main', 'https://api.tatum.io/v3/dogecoin', 'https://api.blockchair.com/dogecoin'],
|
|
82
|
+
concurrency: 1,
|
|
83
|
+
sleepMs: 2000,
|
|
84
|
+
},
|
|
85
|
+
dash: {
|
|
86
|
+
decimals: 8,
|
|
87
|
+
endpoints: ['https://insight.dash.org/insight-api', 'https://api.blockchair.com/dash'],
|
|
88
|
+
concurrency: 5,
|
|
89
|
+
},
|
|
90
|
+
bsv: {
|
|
91
|
+
decimals: 8,
|
|
92
|
+
// whatsonchain throttles aggressively and answers with a body missing `confirmed`
|
|
93
|
+
endpoints: ['https://api.whatsonchain.com/v1/bsv/main'],
|
|
94
|
+
concurrency: 1,
|
|
95
|
+
},
|
|
96
|
+
zcash: {
|
|
97
|
+
decimals: 8,
|
|
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'],
|
|
102
|
+
concurrency: 1,
|
|
103
|
+
sleepMs: 5000,
|
|
104
|
+
},
|
|
105
|
+
kaspa: {
|
|
106
|
+
decimals: 8,
|
|
107
|
+
endpoints: ['https://api.kaspa.org'],
|
|
108
|
+
concurrency: 5,
|
|
109
|
+
},
|
|
110
|
+
mvc: {
|
|
111
|
+
decimals: 8,
|
|
112
|
+
endpoints: ['https://mainnet.mvcapi.com'],
|
|
113
|
+
concurrency: 5,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
/** Built-in chains; env-defined chains (see `getChainConfig`) are not listed here. */
|
|
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;
|
|
121
|
+
/** mempool.space base url, used for the timestamp -> block lookup (`BITCOIN_MEMPOOL_API`). */
|
|
122
|
+
exports.DEFAULT_MEMPOOL_API = 'https://mempool.space/api';
|
|
123
|
+
/** blockchain.info multiaddr endpoint for bulk bitcoin balances (`BITCOIN_MULTIADDR_API`). */
|
|
124
|
+
exports.DEFAULT_MULTIADDR_API = 'https://blockchain.info/multiaddr';
|
|
125
|
+
/** blockchain.info multiaddr accepts ~100 addresses per call */
|
|
126
|
+
exports.MULTIADDR_CHUNK_SIZE = 100;
|
|
127
|
+
/** pause between multiaddr chunks (blockchain.info rate limit), as in the adapters helper */
|
|
128
|
+
exports.MULTIADDR_SLEEP_MS = 10000;
|
|
129
|
+
/** bitcoin cache api accepts up to 700 addresses per call */
|
|
130
|
+
exports.CACHE_API_CHUNK_SIZE = 700;
|
|
131
|
+
const DEFAULT_CHAIN = 'bitcoin';
|
|
132
|
+
/** UTXO snapshots newer than this are read from `/utxo`, older ones by replaying `/txs` */
|
|
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`). */
|
|
135
|
+
function isUtxoChain(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;
|
|
149
|
+
}
|
|
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);
|
|
178
|
+
if (!config)
|
|
179
|
+
throw new Error(`utxo: unsupported chain "${chain}" (known: ${exports.utxoChains.join(', ')}; set ${String(chain).toUpperCase()}_EXPLORER_API to add it)`);
|
|
180
|
+
return config;
|
|
181
|
+
}
|
|
182
|
+
const getConfig = getChainConfig;
|
|
183
|
+
/** Explorer base urls for `chain`: env `<CHAIN>_EXPLORER_API` wins, then `CHAINS[chain].endpoints`. */
|
|
184
|
+
function getEndpoints({ chain = DEFAULT_CHAIN } = {}) {
|
|
185
|
+
const config = getConfig(chain);
|
|
186
|
+
return (0, rpc_1.getEndpoints)(chain, config.endpoints, { envKey: `${chain.toUpperCase()}_EXPLORER_API` });
|
|
187
|
+
}
|
|
188
|
+
function limiter(chain) {
|
|
189
|
+
return (0, rpc_1.getLimiter)(chain.toUpperCase(), getConfig(chain).concurrency);
|
|
190
|
+
}
|
|
191
|
+
/** Run `fn` under the chain limiter, honouring the chain's `sleepMs` pause. */
|
|
192
|
+
async function throttled(chain, fn) {
|
|
193
|
+
const { sleepMs } = getConfig(chain);
|
|
194
|
+
return limiter(chain)(async () => {
|
|
195
|
+
if (sleepMs)
|
|
196
|
+
await (0, rpc_1.sleep)(sleepMs);
|
|
197
|
+
return fn();
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// pure helpers
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
const BECH32_RE = /^(bc1[ac-hj-np-z02-9]{25,87}|BC1[AC-HJ-NP-Z02-9]{25,87})$/;
|
|
204
|
+
const BASE58_RE = /^[13][1-9A-HJ-NP-Za-km-z]{25,34}$/;
|
|
205
|
+
/**
|
|
206
|
+
* Basic shape check for a bitcoin mainnet address: bech32 / bech32m (`bc1...`,
|
|
207
|
+
* single case) or base58 P2PKH / P2SH (`1...` / `3...`). No checksum validation.
|
|
208
|
+
*/
|
|
209
|
+
function isBitcoinAddress(str) {
|
|
210
|
+
if (typeof str !== 'string')
|
|
211
|
+
return false;
|
|
212
|
+
return BECH32_RE.test(str) || BASE58_RE.test(str);
|
|
213
|
+
}
|
|
214
|
+
/** Coerce integer-like values (decimal / bigint / integer number / integer string) to BigInt. */
|
|
215
|
+
function toBigInt(value) {
|
|
216
|
+
if (value === null || value === undefined || value === '')
|
|
217
|
+
return BigInt(0);
|
|
218
|
+
if (typeof value === 'bigint')
|
|
219
|
+
return value;
|
|
220
|
+
if (typeof value === 'number') {
|
|
221
|
+
if (!Number.isFinite(value))
|
|
222
|
+
throw new Error(`utxo.toBigInt: not a finite number: ${value}`);
|
|
223
|
+
return BigInt(Math.trunc(value));
|
|
224
|
+
}
|
|
225
|
+
const s = String(value).trim();
|
|
226
|
+
if (/^-?\d+$/.test(s))
|
|
227
|
+
return BigInt(s);
|
|
228
|
+
if (/^-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(s)) {
|
|
229
|
+
const n = Number(s);
|
|
230
|
+
if (!Number.isFinite(n))
|
|
231
|
+
throw new Error(`utxo.toBigInt: cannot parse ${s}`);
|
|
232
|
+
return BigInt(Math.trunc(n));
|
|
233
|
+
}
|
|
234
|
+
throw new Error(`utxo.toBigInt: cannot parse ${s}`);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Whole-coin amount (`'1.5'`, `1.5`, `'2e-3'`) -> base units as a decimal string,
|
|
238
|
+
* using string arithmetic so 8-decimal values never go through float rounding.
|
|
239
|
+
* Extra fractional digits are truncated.
|
|
240
|
+
*/
|
|
241
|
+
function toBaseUnits(value, decimals) {
|
|
242
|
+
if (typeof value === 'bigint')
|
|
243
|
+
return (value * BigInt(10) ** BigInt(decimals)).toString();
|
|
244
|
+
let s = String(value).trim();
|
|
245
|
+
if (s === '')
|
|
246
|
+
return '0';
|
|
247
|
+
if (/e/i.test(s)) {
|
|
248
|
+
const n = Number(s);
|
|
249
|
+
if (!Number.isFinite(n))
|
|
250
|
+
throw new Error(`utxo.toBaseUnits: cannot parse ${value}`);
|
|
251
|
+
s = n.toFixed(Math.min(decimals, 100));
|
|
252
|
+
}
|
|
253
|
+
const match = /^(-)?(\d*)(?:\.(\d*))?$/.exec(s);
|
|
254
|
+
if (!match || (match[2] === '' && (match[3] === undefined || match[3] === '')))
|
|
255
|
+
throw new Error(`utxo.toBaseUnits: cannot parse ${value}`);
|
|
256
|
+
const sign = match[1] ? '-' : '';
|
|
257
|
+
const whole = match[2] || '0';
|
|
258
|
+
const fraction = (match[3] || '').slice(0, decimals).padEnd(decimals, '0');
|
|
259
|
+
const raw = BigInt(whole + fraction);
|
|
260
|
+
if (raw === BigInt(0))
|
|
261
|
+
return '0';
|
|
262
|
+
return sign + raw.toString();
|
|
263
|
+
}
|
|
264
|
+
/** Base units -> whole coins as a JS number (`fromBaseUnits('150000000', 8) === 1.5`). */
|
|
265
|
+
function fromBaseUnits(raw, decimals) {
|
|
266
|
+
return Number(toBigInt(raw)) / 10 ** decimals;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Parse an esplora `/address/{addr}` response (blockstream, mempool.space,
|
|
270
|
+
* litecoinspace) into base-unit strings. `confirmed` is `chain_stats.funded - spent`,
|
|
271
|
+
* matching the adapters' getBalanceNow and the coins server getBtcBalanceSats.
|
|
272
|
+
*/
|
|
273
|
+
function parseBlockstreamAddressStats(json) {
|
|
274
|
+
var _a, _b, _c, _d;
|
|
275
|
+
const chainStats = json === null || json === void 0 ? void 0 : json.chain_stats;
|
|
276
|
+
if (!chainStats || chainStats.funded_txo_sum === undefined || chainStats.spent_txo_sum === undefined)
|
|
277
|
+
throw new Error('utxo: malformed esplora address response (missing chain_stats)');
|
|
278
|
+
const mempoolStats = (_a = json.mempool_stats) !== null && _a !== void 0 ? _a : {};
|
|
279
|
+
const confirmed = toBigInt(chainStats.funded_txo_sum) - toBigInt(chainStats.spent_txo_sum);
|
|
280
|
+
const unconfirmed = toBigInt((_b = mempoolStats.funded_txo_sum) !== null && _b !== void 0 ? _b : 0) - toBigInt((_c = mempoolStats.spent_txo_sum) !== null && _c !== void 0 ? _c : 0);
|
|
281
|
+
return {
|
|
282
|
+
address: json.address,
|
|
283
|
+
confirmed: confirmed.toString(),
|
|
284
|
+
unconfirmed: unconfirmed.toString(),
|
|
285
|
+
total: (confirmed + unconfirmed).toString(),
|
|
286
|
+
txCount: Number((_d = chainStats.tx_count) !== null && _d !== void 0 ? _d : 0),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Confirmed balance in base units from any of the supported explorer dialects.
|
|
291
|
+
* Shape detection is ordered so ambiguous `balance` fields (coins vs base units)
|
|
292
|
+
* are only reached for providers that report base units (blockbook v2, kaspa).
|
|
293
|
+
*/
|
|
294
|
+
function parseBalanceResponse(json, { address, decimals }) {
|
|
295
|
+
var _a, _b, _c;
|
|
296
|
+
if (json === null || json === undefined || typeof json !== 'object')
|
|
297
|
+
throw new Error('utxo: empty balance response');
|
|
298
|
+
// esplora: blockstream / mempool.space / litecoinspace
|
|
299
|
+
if (json.chain_stats)
|
|
300
|
+
return parseBlockstreamAddressStats(json).confirmed;
|
|
301
|
+
// blockcypher (`final_balance` includes unconfirmed, `balance` is confirmed only)
|
|
302
|
+
if (json.final_balance !== undefined)
|
|
303
|
+
return toBigInt((_a = json.balance) !== null && _a !== void 0 ? _a : json.final_balance).toString();
|
|
304
|
+
// insight (dash): `balance` is in coins, `balanceSat` in base units
|
|
305
|
+
if (json.balanceSat !== undefined)
|
|
306
|
+
return toBigInt(json.balanceSat).toString();
|
|
307
|
+
// whatsonchain (bsv) / mvcapi / haskoin (bitcoincash): `{ confirmed, unconfirmed }` in base units
|
|
308
|
+
if (json.confirmed !== undefined) {
|
|
309
|
+
if (typeof json.confirmed !== 'number' && typeof json.confirmed !== 'string')
|
|
310
|
+
throw new Error(`utxo: unexpected balance response for ${address}`);
|
|
311
|
+
return toBigInt(json.confirmed).toString();
|
|
312
|
+
}
|
|
313
|
+
// blockchair dashboards
|
|
314
|
+
const blockchair = (_c = (_b = json.data) === null || _b === void 0 ? void 0 : _b[address]) === null || _c === void 0 ? void 0 : _c.address;
|
|
315
|
+
if (blockchair && blockchair.balance !== undefined)
|
|
316
|
+
return toBigInt(blockchair.balance).toString();
|
|
317
|
+
if (json.data && typeof json.data === 'object' && !Array.isArray(json.data) && Object.keys(json.data).length && json.data[address] === null)
|
|
318
|
+
throw new Error(`utxo: blockchair has no data for ${address}`);
|
|
319
|
+
// tatum: `{ incoming, outgoing }` in whole coins
|
|
320
|
+
if (json.incoming !== undefined && json.outgoing !== undefined)
|
|
321
|
+
return (BigInt(toBaseUnits(json.incoming, decimals)) - BigInt(toBaseUnits(json.outgoing, decimals))).toString();
|
|
322
|
+
// blockbook v2 (string sats) / kaspa (`balance` in sompi)
|
|
323
|
+
if (json.balance !== undefined)
|
|
324
|
+
return toBigInt(json.balance).toString();
|
|
325
|
+
throw new Error(`utxo: unrecognised balance response for ${address}: ${JSON.stringify(json).slice(0, 200)}`);
|
|
326
|
+
}
|
|
327
|
+
const PROVIDERS = [
|
|
328
|
+
{ kind: 'blockchair', match: /blockchair\.com/i, balancePath: (a) => `/dashboards/address/${a}?limit=0` },
|
|
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' },
|
|
332
|
+
{ kind: 'blockcypher', match: /blockcypher\.com/i, balancePath: (a) => `/addrs/${a}/balance` },
|
|
333
|
+
{ kind: 'insight', match: /insight/i, balancePath: (a) => `/addr/${a}` },
|
|
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` },
|
|
337
|
+
{ kind: 'kaspa', match: /kaspa/i, balancePath: (a) => `/addresses/${encodeURIComponent(a)}/balance` },
|
|
338
|
+
{ kind: 'mvc', match: /mvcapi\.com/i, balancePath: (a) => `/address/${a}/balance` },
|
|
339
|
+
{ kind: 'blockbook', match: /blockbook/i, balancePath: (a) => `/address/${a}` },
|
|
340
|
+
{ kind: 'esplora', match: /blockstream\.info|mempool\.space|litecoinspace\.org/i, balancePath: (a) => `/address/${a}` },
|
|
341
|
+
];
|
|
342
|
+
function tatumHeaders() {
|
|
343
|
+
var _a;
|
|
344
|
+
const key = (_a = (0, env_1.getEnvValue)('TATUM_API_KEY')) !== null && _a !== void 0 ? _a : (0, env_1.getEnvValue)('TATUM_PUBLIC_API_KEY');
|
|
345
|
+
if (!key)
|
|
346
|
+
return undefined;
|
|
347
|
+
return { 'x-api-key': key, 'User-Agent': 'Thunder Client (https://www.thunderclient.com)' };
|
|
348
|
+
}
|
|
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
|
+
*/
|
|
366
|
+
function detectProvider(endpoint, chain) {
|
|
367
|
+
var _a;
|
|
368
|
+
const found = PROVIDERS.find(p => p.match.test(endpoint));
|
|
369
|
+
if (found)
|
|
370
|
+
return found;
|
|
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;
|
|
378
|
+
}
|
|
379
|
+
/** Endpoints of `chain` that speak esplora (needed for utxo / tx / block calls). */
|
|
380
|
+
function esploraEndpoints(chain) {
|
|
381
|
+
const list = getEndpoints({ chain }).filter(e => detectProvider(e, chain).kind === 'esplora');
|
|
382
|
+
if (!list.length)
|
|
383
|
+
throw new Error(`utxo: no esplora endpoint configured for ${chain} (set ${chain.toUpperCase()}_EXPLORER_API)`);
|
|
384
|
+
return list;
|
|
385
|
+
}
|
|
386
|
+
async function esploraGet(chain, path, { retries, timeout } = {}) {
|
|
387
|
+
return throttled(chain, () => (0, rpc_1.httpGet)(esploraEndpoints(chain), { path, retries, timeout }));
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Confirmed balance of `address` in base units. Tries every configured endpoint in
|
|
391
|
+
* order (blockstream -> mempool.space -> blockbook for bitcoin) and throws only
|
|
392
|
+
* when all of them fail.
|
|
393
|
+
*/
|
|
394
|
+
async function getBalance({ chain = DEFAULT_CHAIN, address, retries = 2, timeout }) {
|
|
395
|
+
var _a, _b;
|
|
396
|
+
const { decimals } = getConfig(chain);
|
|
397
|
+
const endpoints = getEndpoints({ chain });
|
|
398
|
+
const errors = [];
|
|
399
|
+
for (const endpoint of endpoints) {
|
|
400
|
+
const provider = detectProvider(endpoint, chain);
|
|
401
|
+
const headers = provider.headers ? provider.headers() : undefined;
|
|
402
|
+
if (provider.headers && !headers) {
|
|
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'})`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const data = await throttled(chain, () => (0, rpc_1.httpGet)(endpoint, { path: provider.balancePath(address), headers, retries, timeout }));
|
|
408
|
+
return parseBalanceResponse(data, { address, decimals });
|
|
409
|
+
}
|
|
410
|
+
catch (e) {
|
|
411
|
+
const message = String((_b = e === null || e === void 0 ? void 0 : e.message) !== null && _b !== void 0 ? _b : e).slice(0, 200);
|
|
412
|
+
errors.push(`${endpoint}: ${message}`);
|
|
413
|
+
(0, debugLog_1.debugLog)(`[chains.utxo] ${chain} balance error for ${address} via ${endpoint}: ${message}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
throw new Error(`utxo: all explorers failed for ${chain} ${address}: ${errors.join(' | ') || 'no usable endpoint'}`);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Confirmed balances of many addresses as `{ [address]: baseUnits }`. Bitcoin
|
|
420
|
+
* batches through blockchain.info `multiaddr` (100 per call, 10s between chunks)
|
|
421
|
+
* and falls back to per-address explorer lookups; other chains go per address
|
|
422
|
+
* under the chain limiter.
|
|
423
|
+
*/
|
|
424
|
+
async function getBalances({ chain = DEFAULT_CHAIN, addresses, skipMultiaddr = false, retries, timeout }) {
|
|
425
|
+
var _a;
|
|
426
|
+
getConfig(chain);
|
|
427
|
+
const unique = uniqueAddresses(addresses);
|
|
428
|
+
if (!unique.length)
|
|
429
|
+
return {};
|
|
430
|
+
if (chain === 'bitcoin' && !skipMultiaddr && unique.length > 1) {
|
|
431
|
+
try {
|
|
432
|
+
return await getBitcoinBalancesMultiaddr({ addresses: unique });
|
|
433
|
+
}
|
|
434
|
+
catch (e) {
|
|
435
|
+
(0, debugLog_1.debugLog)(`[chains.utxo] bitcoin multiaddr failed, falling back to per-address lookups: ${String((_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e).slice(0, 200)}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const res = {};
|
|
439
|
+
await Promise.all(unique.map(async (address) => {
|
|
440
|
+
res[address] = await getBalance({ chain, address, retries, timeout });
|
|
441
|
+
}));
|
|
442
|
+
return res;
|
|
443
|
+
}
|
|
444
|
+
/** blockchain.info `multiaddr` base url (`BITCOIN_MULTIADDR_API` overrides). */
|
|
445
|
+
function getMultiaddrEndpoints() {
|
|
446
|
+
return (0, rpc_1.getEndpoints)('bitcoin', exports.DEFAULT_MULTIADDR_API, { envKey: 'BITCOIN_MULTIADDR_API' });
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Bitcoin balances via blockchain.info `multiaddr?active=a|b|c` in chunks of 100.
|
|
450
|
+
* `final_balance` includes unconfirmed outputs, as in the adapters helper.
|
|
451
|
+
*/
|
|
452
|
+
async function getBitcoinBalancesMultiaddr({ addresses, chunkSize = exports.MULTIADDR_CHUNK_SIZE, sleepMs = exports.MULTIADDR_SLEEP_MS }) {
|
|
453
|
+
const unique = uniqueAddresses(addresses);
|
|
454
|
+
const res = {};
|
|
455
|
+
const endpoints = getMultiaddrEndpoints();
|
|
456
|
+
await (0, rpc_1.runInChunks)(unique, async (chunk) => {
|
|
457
|
+
const data = await (0, rpc_1.httpGet)(endpoints, { params: { active: chunk.join('|') } });
|
|
458
|
+
if (!Array.isArray(data === null || data === void 0 ? void 0 : data.addresses))
|
|
459
|
+
throw new Error('utxo: malformed multiaddr response');
|
|
460
|
+
for (const entry of data.addresses)
|
|
461
|
+
res[entry.address] = toBigInt(entry.final_balance).toString();
|
|
462
|
+
const missing = chunk.filter(a => res[a] === undefined);
|
|
463
|
+
if (missing.length)
|
|
464
|
+
throw new Error(`utxo: multiaddr returned no balance for ${missing.length} address(es): ${missing.slice(0, 3).join(', ')}`);
|
|
465
|
+
return [];
|
|
466
|
+
}, { chunkSize, concurrency: 1, sleepTime: sleepMs });
|
|
467
|
+
return res;
|
|
468
|
+
}
|
|
469
|
+
/** `BITCOIN_CACHE_API` url when configured (bulk bitcoin balance cache used by the adapters). */
|
|
470
|
+
function getBitcoinCacheApi() {
|
|
471
|
+
return (0, env_1.getEnvValue)('BITCOIN_CACHE_API');
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Sum of bitcoin balances via the `BITCOIN_CACHE_API` bulk endpoint
|
|
475
|
+
* (`POST { addresses, network: 'BTC' }` -> total sats, 700 addresses per call),
|
|
476
|
+
* mirroring getCachedBitcoinBalances in the adapters. Throws when the env is not set.
|
|
477
|
+
*/
|
|
478
|
+
async function getCachedBitcoinTotal({ addresses, chunkSize = exports.CACHE_API_CHUNK_SIZE }) {
|
|
479
|
+
var _a, _b;
|
|
480
|
+
const cacheApi = getBitcoinCacheApi();
|
|
481
|
+
if (!cacheApi)
|
|
482
|
+
throw new Error('utxo: BITCOIN_CACHE_API is not set');
|
|
483
|
+
const unique = uniqueAddresses(addresses);
|
|
484
|
+
const chunks = (0, rpc_1.sliceIntoChunks)(unique, chunkSize);
|
|
485
|
+
(0, debugLog_1.debugLog)(`[chains.utxo] bitcoin cache api call: ${unique.length} addresses, ${chunks.length} chunk(s)`);
|
|
486
|
+
let sum = BigInt(0);
|
|
487
|
+
for (const chunk of chunks) {
|
|
488
|
+
const res = await (0, rpc_1.httpPost)(cacheApi, { addresses: chunk, network: 'BTC' }, { retries: 3 });
|
|
489
|
+
const value = typeof res === 'object' && res !== null ? ((_b = (_a = res.balance) !== null && _a !== void 0 ? _a : res.total) !== null && _b !== void 0 ? _b : res.result) : res;
|
|
490
|
+
sum += toBigInt(value);
|
|
491
|
+
}
|
|
492
|
+
return sum.toString();
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Sum of confirmed balances in base units. For bitcoin with `BITCOIN_CACHE_API`
|
|
496
|
+
* set and more than `cacheThreshold` addresses the bulk cache is used first
|
|
497
|
+
* (the adapters' `_sumTokensBlockchain` flow), then `getBalances` is summed.
|
|
498
|
+
*/
|
|
499
|
+
async function getTotalBalance({ chain = DEFAULT_CHAIN, addresses, cacheThreshold = 51, forceCacheUse = false, ...rest }) {
|
|
500
|
+
var _a;
|
|
501
|
+
const unique = uniqueAddresses(addresses);
|
|
502
|
+
if (chain === 'bitcoin' && getBitcoinCacheApi() && unique.length > cacheThreshold) {
|
|
503
|
+
if (unique.length > 1000)
|
|
504
|
+
forceCacheUse = true;
|
|
505
|
+
try {
|
|
506
|
+
return await getCachedBitcoinTotal({ addresses: unique });
|
|
507
|
+
}
|
|
508
|
+
catch (e) {
|
|
509
|
+
if (forceCacheUse)
|
|
510
|
+
throw e;
|
|
511
|
+
(0, debugLog_1.debugLog)(`[chains.utxo] bitcoin cache error, falling back to explorers: ${String((_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e).slice(0, 200)}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const balances = await getBalances({ chain, addresses: unique, ...rest });
|
|
515
|
+
let sum = BigInt(0);
|
|
516
|
+
for (const value of Object.values(balances))
|
|
517
|
+
sum += toBigInt(value);
|
|
518
|
+
return sum.toString();
|
|
519
|
+
}
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
// esplora: utxos, txs, historical balance
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
/**
|
|
524
|
+
* Unspent outputs of `address` (`/address/{addr}/utxo`), values as base-unit strings.
|
|
525
|
+
* Blockstream answers 400 "Too many unspent outputs" for very large utxo sets
|
|
526
|
+
* (e.g. the genesis address); use `getBalance` for those.
|
|
527
|
+
*/
|
|
528
|
+
async function getUtxos({ chain = DEFAULT_CHAIN, address }) {
|
|
529
|
+
const utxos = await esploraGet(chain, `/address/${address}/utxo`);
|
|
530
|
+
if (!Array.isArray(utxos))
|
|
531
|
+
throw new Error(`utxo: malformed utxo response for ${address}`);
|
|
532
|
+
return utxos.map((u) => { var _a; return ({ txid: u.txid, vout: u.vout, value: toBigInt(u.value).toString(), status: (_a = u.status) !== null && _a !== void 0 ? _a : { confirmed: false } }); });
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* One page of transactions for `address`: `/address/{addr}/txs` (up to 25 mempool
|
|
536
|
+
* + 25 confirmed) or, with `lastSeenTxid`, `/address/{addr}/txs/chain/{txid}`
|
|
537
|
+
* (next 25 confirmed). An empty array means no more pages.
|
|
538
|
+
*/
|
|
539
|
+
async function getAddressTxs({ chain = DEFAULT_CHAIN, address, lastSeenTxid }) {
|
|
540
|
+
const path = lastSeenTxid ? `/address/${address}/txs/chain/${lastSeenTxid}` : `/address/${address}/txs`;
|
|
541
|
+
const txs = await esploraGet(chain, path);
|
|
542
|
+
if (!Array.isArray(txs))
|
|
543
|
+
throw new Error(`utxo: malformed txs response for ${address}`);
|
|
544
|
+
return txs;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Bitcoin balance of `address` at `timestamp` (unix seconds) in base units, ported
|
|
548
|
+
* from the adapters' archive getBalance: for timestamps within the last 30 minutes
|
|
549
|
+
* the current utxo set is filtered by `block_time`; otherwise every confirmed tx is
|
|
550
|
+
* replayed (vout adds, vin.prevout subtracts) until the page list is exhausted.
|
|
551
|
+
* Unconfirmed txs (no `block_time`) are ignored.
|
|
552
|
+
*/
|
|
553
|
+
async function getBitcoinBalanceAt({ chain = DEFAULT_CHAIN, address, timestamp, maxPages }) {
|
|
554
|
+
var _a, _b, _c, _d, _e;
|
|
555
|
+
const now = Date.now() / 1e3;
|
|
556
|
+
let balance = BigInt(0);
|
|
557
|
+
if (timestamp > now - RECENT_WINDOW_SECONDS) {
|
|
558
|
+
const utxos = await getUtxos({ chain, address });
|
|
559
|
+
for (const utxo of utxos) {
|
|
560
|
+
const blockTime = (_a = utxo.status) === null || _a === void 0 ? void 0 : _a.block_time;
|
|
561
|
+
if (blockTime !== undefined && blockTime <= timestamp)
|
|
562
|
+
balance += BigInt(utxo.value);
|
|
563
|
+
}
|
|
564
|
+
return balance.toString();
|
|
565
|
+
}
|
|
566
|
+
let txs = await getAddressTxs({ chain, address });
|
|
567
|
+
let page = 0;
|
|
568
|
+
while (txs.length) {
|
|
569
|
+
for (const tx of txs) {
|
|
570
|
+
const blockTime = (_b = tx.status) === null || _b === void 0 ? void 0 : _b.block_time;
|
|
571
|
+
if (blockTime === undefined || blockTime > timestamp)
|
|
572
|
+
continue;
|
|
573
|
+
for (const vin of (_c = tx.vin) !== null && _c !== void 0 ? _c : [])
|
|
574
|
+
if (((_d = vin.prevout) === null || _d === void 0 ? void 0 : _d.scriptpubkey_address) === address)
|
|
575
|
+
balance -= toBigInt(vin.prevout.value);
|
|
576
|
+
for (const vout of (_e = tx.vout) !== null && _e !== void 0 ? _e : [])
|
|
577
|
+
if (vout.scriptpubkey_address === address)
|
|
578
|
+
balance += toBigInt(vout.value);
|
|
579
|
+
}
|
|
580
|
+
page++;
|
|
581
|
+
if (maxPages !== undefined && page >= maxPages) {
|
|
582
|
+
(0, debugLog_1.debugLog)(`[chains.utxo] getBitcoinBalanceAt ${address}: stopped after ${maxPages} pages`);
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
txs = await getAddressTxs({ chain, address, lastSeenTxid: txs[txs.length - 1].txid });
|
|
586
|
+
}
|
|
587
|
+
return balance.toString();
|
|
588
|
+
}
|
|
589
|
+
// ---------------------------------------------------------------------------
|
|
590
|
+
// esplora: blocks
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
async function getBlockByHeight(chain, height) {
|
|
593
|
+
var _a;
|
|
594
|
+
const hash = String(await esploraGet(chain, `/block-height/${height}`)).trim();
|
|
595
|
+
if (!/^[0-9a-f]{64}$/i.test(hash))
|
|
596
|
+
throw new Error(`utxo: no block hash for ${chain} height ${height}`);
|
|
597
|
+
const block = await esploraGet(chain, `/block/${hash}`);
|
|
598
|
+
return { number: Number((_a = block.height) !== null && _a !== void 0 ? _a : height), timestamp: Number(block.timestamp), hash };
|
|
599
|
+
}
|
|
600
|
+
/** Chain tip via `/blocks/tip/height` + `/block-height/{h}` + `/block/{hash}`. */
|
|
601
|
+
async function getLatestBlock({ chain = DEFAULT_CHAIN } = {}) {
|
|
602
|
+
const height = Number(await esploraGet(chain, '/blocks/tip/height'));
|
|
603
|
+
if (!Number.isFinite(height))
|
|
604
|
+
throw new Error(`utxo: malformed tip height for ${chain}`);
|
|
605
|
+
return getBlockByHeight(chain, height);
|
|
606
|
+
}
|
|
607
|
+
/** Block by height (`/block-height/{h}` then `/block/{hash}`). */
|
|
608
|
+
async function getBlock({ chain = DEFAULT_CHAIN, height }) {
|
|
609
|
+
return getBlockByHeight(chain, height);
|
|
610
|
+
}
|
|
611
|
+
/** mempool.space base urls (`BITCOIN_MEMPOOL_API` overrides; falls back to any configured mempool endpoint). */
|
|
612
|
+
function getMempoolEndpoints() {
|
|
613
|
+
const configured = getEndpoints({ chain: 'bitcoin' }).filter(e => /mempool/i.test(e));
|
|
614
|
+
return (0, rpc_1.getEndpoints)('bitcoin', configured.length ? configured : exports.DEFAULT_MEMPOOL_API, { envKey: 'BITCOIN_MEMPOOL_API' });
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Latest block mined at or before `timestamp` (unix seconds). Bitcoin asks
|
|
618
|
+
* mempool.space `/v1/mining/blocks/timestamp/{ts}` first (then walks back if the
|
|
619
|
+
* returned block is newer than `timestamp`); otherwise / on failure a binary
|
|
620
|
+
* search over `/block-height/{h}` is used.
|
|
621
|
+
*/
|
|
622
|
+
async function getBlockAtTimestamp({ chain = DEFAULT_CHAIN, timestamp }) {
|
|
623
|
+
var _a;
|
|
624
|
+
timestamp = Math.floor(timestamp > 1e12 ? timestamp / 1000 : timestamp);
|
|
625
|
+
if (chain === 'bitcoin') {
|
|
626
|
+
try {
|
|
627
|
+
const res = await throttled(chain, () => (0, rpc_1.httpGet)(getMempoolEndpoints(), { path: `/v1/mining/blocks/timestamp/${timestamp}` }));
|
|
628
|
+
const height = Number(res === null || res === void 0 ? void 0 : res.height);
|
|
629
|
+
if (!Number.isFinite(height))
|
|
630
|
+
throw new Error('malformed mempool response');
|
|
631
|
+
let block = await getBlockByHeight(chain, height);
|
|
632
|
+
// mempool returns the closest block, which may be just after the timestamp
|
|
633
|
+
while (block.timestamp > timestamp && block.number > 0)
|
|
634
|
+
block = await getBlockByHeight(chain, block.number - 1);
|
|
635
|
+
return block;
|
|
636
|
+
}
|
|
637
|
+
catch (e) {
|
|
638
|
+
(0, debugLog_1.debugLog)(`[chains.utxo] mempool timestamp lookup failed, falling back to binary search: ${String((_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e).slice(0, 200)}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const tip = await getLatestBlock({ chain });
|
|
642
|
+
if (tip.timestamp <= timestamp)
|
|
643
|
+
return tip;
|
|
644
|
+
let lo = 0;
|
|
645
|
+
let hi = tip.number;
|
|
646
|
+
let best;
|
|
647
|
+
while (lo <= hi) {
|
|
648
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
649
|
+
const block = await getBlockByHeight(chain, mid);
|
|
650
|
+
if (block.timestamp <= timestamp) {
|
|
651
|
+
best = block;
|
|
652
|
+
lo = mid + 1;
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
hi = mid - 1;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (!best)
|
|
659
|
+
throw new Error(`utxo: no ${chain} block at or before ${timestamp}`);
|
|
660
|
+
return best;
|
|
661
|
+
}
|
|
662
|
+
// ---------------------------------------------------------------------------
|
|
663
|
+
// misc
|
|
664
|
+
// ---------------------------------------------------------------------------
|
|
665
|
+
function uniqueAddresses(addresses) {
|
|
666
|
+
const seen = new Set();
|
|
667
|
+
const res = [];
|
|
668
|
+
for (const address of addresses !== null && addresses !== void 0 ? addresses : []) {
|
|
669
|
+
if (typeof address !== 'string')
|
|
670
|
+
continue;
|
|
671
|
+
const trimmed = address.trim();
|
|
672
|
+
if (!trimmed || seen.has(trimmed))
|
|
673
|
+
continue;
|
|
674
|
+
seen.add(trimmed);
|
|
675
|
+
res.push(trimmed);
|
|
676
|
+
}
|
|
677
|
+
return res;
|
|
678
|
+
}
|