@qbit-oss/api 0.0.0-stage → 0.1.0

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/lib/index.js ADDED
@@ -0,0 +1,1921 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DEFAULT_MAX_FEE_RATE_SAT_VB: () => DEFAULT_MAX_FEE_RATE_SAT_VB,
24
+ EsploraClient: () => EsploraClient,
25
+ MempoolQbitClient: () => MempoolQbitClient,
26
+ QbitApiError: () => QbitApiError,
27
+ addressBalanceFromStats: () => addressBalanceFromStats,
28
+ feeEstimatesFromMempoolRecommendedFees: () => feeEstimatesFromMempoolRecommendedFees,
29
+ feeTiersFromEstimates: () => feeTiersFromEstimates,
30
+ txidFromRawTx: () => txidFromRawTx
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+ var import_sha256 = require("@noble/hashes/sha256");
34
+ var unredactedCauses = /* @__PURE__ */ new WeakMap();
35
+ var QBIT_API_ERROR_BRAND = /* @__PURE__ */ Symbol.for("qbit-js.QbitApiError");
36
+ var QbitApiError = class extends Error {
37
+ /**
38
+ * The underlying error of `error` as the fetch implementation threw it, or
39
+ * undefined. Its message, stack and properties can contain the full request
40
+ * URL, base-URL secrets included, so it is kept off the error object, where
41
+ * no logger, error reporter or serializer can reach it. Do not log it where
42
+ * the base URL is secret.
43
+ */
44
+ static unredactedCause(error) {
45
+ return typeof error === "object" && error !== null ? unredactedCauses.get(error) : void 0;
46
+ }
47
+ /**
48
+ * Whether `value` is a `QbitApiError` (or a subclass instance) from any copy
49
+ * of `@qbit-oss/api`. `instanceof` only recognizes errors from the same copy,
50
+ * so it is false when an app loads both the CommonJS and the ESM build (or
51
+ * two installed versions) and the error comes from the other one. This checks
52
+ * a brand, `Symbol.for('qbit-js.QbitApiError')`, set on the class prototype
53
+ * instead. It identifies the class, not where the error came from: any code
54
+ * can set the brand. An error from another version can carry a `kind` this
55
+ * version does not list, so keep a default branch when switching on it.
56
+ */
57
+ static is(value) {
58
+ return typeof value === "object" && value !== null && value[QBIT_API_ERROR_BRAND] === true;
59
+ }
60
+ constructor(kind, message, options = {}) {
61
+ super(message);
62
+ this.name = "QbitApiError";
63
+ this.kind = kind;
64
+ if (options.status !== void 0) this.status = options.status;
65
+ if (options.url !== void 0) this.url = options.url;
66
+ if (options.retryAfterMs !== void 0) this.retryAfterMs = options.retryAfterMs;
67
+ if (options.cause !== void 0) this.cause = options.cause;
68
+ if (options.unredactedCause !== void 0) unredactedCauses.set(this, options.unredactedCause);
69
+ if (options.responseBody !== void 0) this.responseBody = options.responseBody;
70
+ if (options.txid !== void 0) this.txid = options.txid;
71
+ }
72
+ };
73
+ Object.defineProperty(QbitApiError.prototype, QBIT_API_ERROR_BRAND, { value: true });
74
+ var TRANSIENT_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
75
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
76
+ var UNPROCESSED_SERVER_ERROR_STATUSES = /* @__PURE__ */ new Set([501, 505, 511]);
77
+ var PRE_CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"]);
78
+ var CONNECT_STAGE_ERROR_CODES = /* @__PURE__ */ new Set(["EHOSTUNREACH", "ENETUNREACH"]);
79
+ var TRANSPORT_ERROR_CODE_PATTERN = /^(?:E[A-Z0-9_]+|UND_ERR_[A-Z0-9_]+|HPE_[A-Z0-9_]+)$/;
80
+ var TXID_PATTERN = /^[0-9a-f]{64}$/;
81
+ var DEFAULT_MAX_ADDRESS_HISTORY_PAGES = 256;
82
+ var DEFAULT_MAX_ADDRESS_HISTORY_TRANSACTIONS = 6400;
83
+ var DEFAULT_MAX_RETRY_AFTER_MS = 3e4;
84
+ var MAX_TIMER_DELAY_MS = 2147483647;
85
+ var MAX_RETRIES = 10;
86
+ var UNSAFE_BASE_URL_CHAR = /[\s\p{Cc}\\?#]|(?![\u200c\u200d])[\p{Cf}\p{Default_Ignorable_Code_Point}]/u;
87
+ function validBaseUrl(value, allowInsecureHttp) {
88
+ const invalid = () => new QbitApiError(
89
+ "http",
90
+ "baseUrl must be an absolute http(s) URL without whitespace, control or format characters, backslashes, credentials, query or fragment"
91
+ );
92
+ if (typeof value !== "string" || UNSAFE_BASE_URL_CHAR.test(value)) throw invalid();
93
+ if (followsUrlStandard() && standardHref(value) === void 0) throw invalid();
94
+ const url = schemeAndHost(value);
95
+ if (!url || url.protocol !== "http:" && url.protocol !== "https:" || !url.hostname || url.userinfo || !/^\d*$/.test(url.port) || Number(url.port) > 65535 || /%E2%80%8[CD]|[\u200c\u200d]/i.test(url.path)) {
96
+ throw invalid();
97
+ }
98
+ if (url.protocol === "http:" && allowInsecureHttp !== true && !isLoopbackHostname(url.hostname)) {
99
+ throw new QbitApiError(
100
+ "http",
101
+ "baseUrl must use https unless its host is loopback; pass allowInsecureHttp: true to allow plain http"
102
+ );
103
+ }
104
+ return value.replace(/\/+$/, "");
105
+ }
106
+ function integerOption(value, defaultValue, name, min, max) {
107
+ const option = value === void 0 ? defaultValue : value;
108
+ if (!Number.isInteger(option) || option < min || option > max) {
109
+ throw new QbitApiError("http", `${name} must be an integer from ${min} to ${max}`);
110
+ }
111
+ return option;
112
+ }
113
+ function retryBaseDelayOption(value) {
114
+ const delay = value === void 0 ? 300 : value;
115
+ if (typeof delay !== "number" || !Number.isFinite(delay) || delay < 0 || delay > MAX_TIMER_DELAY_MS) {
116
+ throw new QbitApiError(
117
+ "http",
118
+ `retryBaseDelayMs must be a finite number from 0 to ${MAX_TIMER_DELAY_MS}`
119
+ );
120
+ }
121
+ return delay;
122
+ }
123
+ function defaultSleep(ms, signal) {
124
+ return new Promise((resolve, reject) => {
125
+ if (signal?.aborted) {
126
+ reject(abortErrorFor(signal));
127
+ return;
128
+ }
129
+ const onAbort = () => {
130
+ clearTimeout(timer);
131
+ reject(abortErrorFor(signal));
132
+ };
133
+ const timer = setTimeout(() => {
134
+ signal?.removeEventListener("abort", onAbort);
135
+ resolve();
136
+ }, ms);
137
+ signal?.addEventListener("abort", onAbort, { once: true });
138
+ });
139
+ }
140
+ var internalSignals = /* @__PURE__ */ new WeakSet();
141
+ function internalAbortController() {
142
+ const controller = new AbortController();
143
+ internalSignals.add(controller.signal);
144
+ return controller;
145
+ }
146
+ function abortErrorFor(signal) {
147
+ const reason = signal.reason;
148
+ if (internalSignals.has(signal) && reason instanceof QbitApiError) return reason;
149
+ return new QbitApiError("aborted", `request aborted by caller${formatErrorCause(reason)}`, {
150
+ cause: reason
151
+ });
152
+ }
153
+ function openOperationScope(sources, deadlineMs) {
154
+ if (deadlineMs !== void 0 && !(Number.isFinite(deadlineMs) && deadlineMs > 0 && deadlineMs <= MAX_TIMER_DELAY_MS)) {
155
+ throw new QbitApiError(
156
+ "http",
157
+ `deadlineMs must be a positive number no greater than ${MAX_TIMER_DELAY_MS}`
158
+ );
159
+ }
160
+ const controller = internalAbortController();
161
+ const cleanups = [];
162
+ const dispose = () => {
163
+ for (const cleanup of cleanups) cleanup();
164
+ cleanups.length = 0;
165
+ };
166
+ const abort = (error) => {
167
+ if (controller.signal.aborted) return;
168
+ controller.abort(error);
169
+ dispose();
170
+ };
171
+ for (const source of sources) {
172
+ if (source === void 0) continue;
173
+ if (source.aborted) {
174
+ abort(abortErrorFor(source));
175
+ break;
176
+ }
177
+ const onAbort = () => abort(abortErrorFor(source));
178
+ source.addEventListener("abort", onAbort, { once: true });
179
+ cleanups.push(() => source.removeEventListener("abort", onAbort));
180
+ }
181
+ if (deadlineMs !== void 0 && !controller.signal.aborted) {
182
+ const timer = setTimeout(
183
+ () => abort(new QbitApiError("timeout", `operation exceeded its ${deadlineMs}ms deadline`)),
184
+ deadlineMs
185
+ );
186
+ cleanups.push(() => clearTimeout(timer));
187
+ }
188
+ if (controller.signal.aborted) {
189
+ dispose();
190
+ throw abortErrorFor(controller.signal);
191
+ }
192
+ return { signal: controller.signal, dispose };
193
+ }
194
+ function raceAbort(promise, signal) {
195
+ return new Promise((resolve, reject) => {
196
+ const onAbort = () => reject(abortErrorFor(signal));
197
+ if (signal.aborted) onAbort();
198
+ else signal.addEventListener("abort", onAbort, { once: true });
199
+ promise.then(
200
+ (value) => {
201
+ signal.removeEventListener("abort", onAbort);
202
+ resolve(value);
203
+ },
204
+ (error) => {
205
+ signal.removeEventListener("abort", onAbort);
206
+ reject(error);
207
+ }
208
+ );
209
+ });
210
+ }
211
+ var EsploraClient = class {
212
+ constructor(options) {
213
+ /** In-flight GET de-dup: concurrent identical GETs share one request. */
214
+ this.inflight = /* @__PURE__ */ new Map();
215
+ if (!options.baseUrl) {
216
+ throw new QbitApiError("http", "EsploraClient requires a baseUrl");
217
+ }
218
+ const baseUrl = validBaseUrl(options.baseUrl, options.allowInsecureHttp);
219
+ const timeoutMs = integerOption(options.timeoutMs, 15e3, "timeoutMs", 1, MAX_TIMER_DELAY_MS);
220
+ const retries = integerOption(options.retries, 2, "retries", 0, MAX_RETRIES);
221
+ const retryBaseDelayMs = retryBaseDelayOption(options.retryBaseDelayMs);
222
+ const maxRetryAfterMs = integerOption(
223
+ options.maxRetryAfterMs,
224
+ DEFAULT_MAX_RETRY_AFTER_MS,
225
+ "maxRetryAfterMs",
226
+ 0,
227
+ MAX_TIMER_DELAY_MS
228
+ );
229
+ const fetchImpl = options.fetchFn ?? globalThis.fetch;
230
+ if (typeof fetchImpl !== "function") {
231
+ throw new QbitApiError("network", "no fetch implementation available; pass options.fetchFn");
232
+ }
233
+ this.baseUrl = baseUrl;
234
+ this.fetchFn = options.fetchFn ? fetchImpl : fetchImpl.bind(globalThis);
235
+ this.timeoutMs = timeoutMs;
236
+ this.retries = retries;
237
+ this.retryBaseDelayMs = retryBaseDelayMs;
238
+ this.maxRetryAfterMs = maxRetryAfterMs;
239
+ this.headers = { ...options.headers ?? {} };
240
+ this.credentials = options.credentials ?? "omit";
241
+ this.sleepFn = options.sleepFn ?? defaultSleep;
242
+ this.signal = options.signal;
243
+ }
244
+ // --- esplora endpoints -------------------------------------------------
245
+ getAddressStats(address, options) {
246
+ return this.getJson(`/address/${encodeURIComponent(address)}`, options);
247
+ }
248
+ getAddressUtxos(address, options) {
249
+ return this.getJson(`/address/${encodeURIComponent(address)}/utxo`, options);
250
+ }
251
+ /**
252
+ * Confirmed transaction history for an address, newest first, 25 per page.
253
+ * Pass the last txid of the previous page as `afterTxid` to paginate.
254
+ */
255
+ getAddressTxs(address, afterTxid, options) {
256
+ const suffix = afterTxid ? `/${encodeURIComponent(afterTxid)}` : "";
257
+ return this.getJson(
258
+ `/address/${encodeURIComponent(address)}/txs/chain${suffix}`,
259
+ options
260
+ );
261
+ }
262
+ /**
263
+ * Unconfirmed (mempool) transactions touching an address. electrs lists at
264
+ * most `--rest-default-max-mempool-txs` (default 50) of them, with no way to
265
+ * page further; `getAddressHistory` detects that cap.
266
+ */
267
+ getAddressMempoolTxs(address, options) {
268
+ return this.getJson(
269
+ `/address/${encodeURIComponent(address)}/txs/mempool`,
270
+ options
271
+ );
272
+ }
273
+ /**
274
+ * Complete address history from a bounded, page-one scan. The confirmed
275
+ * chain route is exhausted explicitly; short pages are not assumed final.
276
+ *
277
+ * electrs answers a chain cursor it does not know (a lagging node behind a
278
+ * load balancer, or a reorg between pages) with HTTP 200 and `[]`, and
279
+ * `/txs/mempool` stops at 50 rows, so the scan alone cannot tell a short
280
+ * history from a complete one. The address stats are therefore read before
281
+ * and after the scan, each in a request of its own, and the history is
282
+ * returned only if both reads agree and it holds exactly
283
+ * `chain_stats.tx_count` confirmed and `mempool_stats.tx_count` mempool
284
+ * transactions. If the stats changed during the scan, the address is scanned
285
+ * once more; if they change again, or the counts differ, the call rejects
286
+ * with `invalid_cursor`. Stats without a non-negative safe-integer
287
+ * `tx_count` reject with `parse`. `maxPages` and `maxTransactions` cover
288
+ * both scans.
289
+ */
290
+ getAddressHistory(address, options = {}) {
291
+ return this.runOperation(options, (signal) => this.scanAddressHistory(address, options, signal));
292
+ }
293
+ async scanAddressHistory(address, options, signal) {
294
+ const maxPages = positiveAddressHistoryLimit(
295
+ options.maxPages,
296
+ DEFAULT_MAX_ADDRESS_HISTORY_PAGES,
297
+ "maxPages"
298
+ );
299
+ const maxTransactions = positiveAddressHistoryLimit(
300
+ options.maxTransactions,
301
+ DEFAULT_MAX_ADDRESS_HISTORY_TRANSACTIONS,
302
+ "maxTransactions"
303
+ );
304
+ const budget = createAddressHistoryBudget();
305
+ return verifiedAddressHistory(
306
+ address,
307
+ "esplora",
308
+ () => this.getJson(`/address/${encodeURIComponent(address)}`, { signal }, false),
309
+ () => this.scanAddressHistoryOnce(address, maxPages, maxTransactions, budget, signal)
310
+ );
311
+ }
312
+ async scanAddressHistoryOnce(address, maxPages, maxTransactions, budget, signal) {
313
+ if (budget.pageRequests >= maxPages) {
314
+ throw addressHistoryPageLimitError(address, maxPages, "esplora");
315
+ }
316
+ const state = createAddressHistoryScanState();
317
+ const mempoolRows = reserveAddressHistoryTransactions(
318
+ budget,
319
+ await this.getAddressMempoolTxs(address, { signal }),
320
+ maxTransactions,
321
+ address,
322
+ "esplora",
323
+ "esplora mempool"
324
+ );
325
+ const mempoolTxs = assertAddressHistoryPage(mempoolRows, address, "esplora mempool");
326
+ appendAddressHistoryTransactions(state, mempoolTxs, "mempool", address, "esplora");
327
+ let cursor;
328
+ const seenCursors = /* @__PURE__ */ new Set();
329
+ while (true) {
330
+ budget.pageRequests += 1;
331
+ const rows = reserveAddressHistoryTransactions(
332
+ budget,
333
+ await this.getAddressTxs(address, cursor, { signal }),
334
+ maxTransactions,
335
+ address,
336
+ "esplora",
337
+ "esplora confirmed"
338
+ );
339
+ if (rows.length === 0) break;
340
+ if (budget.pageRequests === maxPages) {
341
+ throw addressHistoryPageLimitError(address, maxPages, "esplora");
342
+ }
343
+ const txs = assertAddressHistoryPage(rows, address, "esplora confirmed");
344
+ cursor = nextAddressHistoryCursor(txs, cursor, seenCursors, address, "esplora");
345
+ appendAddressHistoryTransactions(state, txs, "confirmed", address, "esplora");
346
+ }
347
+ return { confirmed: state.confirmed, mempool: state.mempool };
348
+ }
349
+ getTx(txid, options) {
350
+ return this.getJson(`/tx/${encodeURIComponent(txid)}`, options);
351
+ }
352
+ /**
353
+ * Raw transaction hex as the backend serializes it (with witness data when
354
+ * the transaction has any), in lowercase. The reply is checked before it is
355
+ * returned: it must be non-empty, even-length hex that decodes to one
356
+ * complete transaction whose txid (the double SHA-256 of its
357
+ * witness-stripped serialization, as qbit Core computes it) is `txid`,
358
+ * compared case-insensitively. The transaction must be encoded as qbit
359
+ * Core's decoder requires: every count and length in its shortest
360
+ * CompactSize form and at most 0x02000000, and no witness flag when every
361
+ * witness stack is empty. Anything else, including the empty `200`
362
+ * reply mempool.qbit.org serves, throws a `parse` `QbitApiError`, so a
363
+ * backend cannot hand back an empty string or another transaction. Spaces,
364
+ * tabs and line breaks around the hex are ignored. The txid does not commit
365
+ * to witness data, so a reply with altered or stripped witnesses still
366
+ * passes; compare the wtxid if you need them.
367
+ */
368
+ async getTxHex(txid, options) {
369
+ const path = `/tx/${encodeURIComponent(txid)}/hex`;
370
+ const text = await this.getText(path, options);
371
+ const hex = trimAsciiWhitespace(text).toLowerCase();
372
+ const invalid = (reason) => new QbitApiError("parse", `transaction hex reply ${reason}`, {
373
+ url: this.errorUrl(path),
374
+ responseBody: responseBodyPreview(text, this.baseUrl)
375
+ });
376
+ if (hex === "") throw invalid("was empty");
377
+ if (hex.length % 2 !== 0 || !/^[0-9a-f]+$/.test(hex)) {
378
+ throw invalid(`was not even-length hex: ${quotedResponseText(text, this.baseUrl)}`);
379
+ }
380
+ const actual = tryTxidFromRawTx(hex);
381
+ if (actual === void 0) throw invalid("is not a well-formed transaction");
382
+ if (actual !== String(txid).toLowerCase()) throw invalid(`is transaction ${actual}, not the one requested`);
383
+ return hex;
384
+ }
385
+ getTxStatus(txid, options) {
386
+ return this.getJson(`/tx/${encodeURIComponent(txid)}/status`, options);
387
+ }
388
+ getFeeEstimates(options) {
389
+ return this.getJson("/fee-estimates", options);
390
+ }
391
+ async getTipHeight(options) {
392
+ const text = await this.getText("/blocks/tip/height", options);
393
+ const height = Number(text.trim());
394
+ if (!Number.isInteger(height)) {
395
+ throw new QbitApiError("parse", `tip height was not an integer: ${quotedResponseText(text, this.baseUrl)}`, {
396
+ url: this.errorUrl("/blocks/tip/height"),
397
+ responseBody: responseBodyPreview(text, this.baseUrl)
398
+ });
399
+ }
400
+ return height;
401
+ }
402
+ async getTipHash(options) {
403
+ return (await this.getText("/blocks/tip/hash", options)).trim();
404
+ }
405
+ /**
406
+ * Broadcast a raw transaction hex. Resolves with the txid once the node has
407
+ * the transaction, including when it reports the transaction as already in
408
+ * its mempool or chain. A submission is never re-sent after an attempt that
409
+ * may have reached the node: a timeout, an ambiguous network error, or a
410
+ * 5xx reply whose body carries no verdict from the node rejects with
411
+ * `unknown_outcome` carrying the txid so the caller can check
412
+ * `getTxStatus(txid)` before resubmitting the same hex. Only 429 replies and
413
+ * connection failures that happened before anything was sent are retried; a
414
+ * 429 whose `Retry-After` exceeds `maxRetryAfterMs` is not waited on and
415
+ * rejects at once as `rate_limit` with `retryAfterMs`, without a re-send.
416
+ * A redirect is never followed. A refused one (a 3xx, a browser opaque
417
+ * redirect, or a reply from a redirect the runtime followed) rejects with
418
+ * `unknown_outcome` too, with the `http` refusal as `cause`: the POST
419
+ * reached the backend, which may have accepted it before redirecting, and
420
+ * a runtime that followed the redirect may have re-sent it.
421
+ * Coded node rejections fail closed as `http` errors whatever the HTTP
422
+ * status, since qbit Core's own JSON-RPC server answers 500 for every RPC
423
+ * error and a backend or proxy may relay that status. When no txid
424
+ * can be derived from the hex and none is supplied, a 2xx reply resolves only
425
+ * if its body is itself a 64-hex txid; any other 2xx body is `unknown_outcome`.
426
+ * Hex `txidFromRawTx` cannot read, including encodings qbit Core refuses to
427
+ * decode, is still sent as it is, and the node gives the verdict.
428
+ * A malformed `rawTxHex` argument or an `options.txid` that is not a 64-hex
429
+ * transaction id matching the hex throws a `TypeError` before anything is sent.
430
+ *
431
+ * `options.signal` / `options.deadlineMs` bound the whole call. An abort or
432
+ * expired deadline before the POST is sent rejects with `aborted` /
433
+ * `timeout`; once the POST may have been sent (in flight or while its reply
434
+ * is read) it rejects with `unknown_outcome`, with that error as `cause`.
435
+ * A reply whose body stalls past `timeoutMs` or cannot be read is
436
+ * `unknown_outcome` too. A 429 reply is the exception throughout: it proves
437
+ * nothing was submitted, so one whose body is lost is retried (then
438
+ * `rate_limit`), and an abort or deadline during its body is definite.
439
+ */
440
+ async broadcastTx(rawTxHex, options) {
441
+ return this.submitTx("/tx", rawTxHex, resolveBroadcastTxid(rawTxHex, options), options);
442
+ }
443
+ // --- adapters (clean, wallet-friendly shapes) --------------------------
444
+ /** Confirmed + mempool balance for an address, in satoshis. */
445
+ async getAddressBalance(address, options) {
446
+ return addressBalanceFromStats(await this.getAddressStats(address, options));
447
+ }
448
+ /** Fee-rate tiers (sat/vB), ascending by confirmation target. */
449
+ async getFeeTiers(options) {
450
+ return feeTiersFromEstimates(await this.getFeeEstimates(options));
451
+ }
452
+ // --- internals ---------------------------------------------------------
453
+ /**
454
+ * Run one public call under a single signal that combines the client's
455
+ * signal, the caller's signal, and the caller's deadline. Pass `{ signal }`
456
+ * to dependent calls so they share the same cancellation and budget.
457
+ */
458
+ async runOperation(options, run) {
459
+ const scope = openOperationScope([this.signal, options?.signal], options?.deadlineMs);
460
+ try {
461
+ return await run(scope.signal);
462
+ } finally {
463
+ scope.dispose();
464
+ }
465
+ }
466
+ /**
467
+ * GET and parse JSON. With `dedup` false the request is sent on its own
468
+ * instead of joining an identical one already in flight, whose reply may
469
+ * predate requests this call has made since.
470
+ */
471
+ getJson(path, options, dedup = true) {
472
+ return this.runOperation(options, async (signal) => {
473
+ const response = await this.request(path, { method: "GET", dedup }, { signal });
474
+ const url = this.errorUrl(path);
475
+ const text = await readBody(response, url, this.baseUrl, signal);
476
+ if (!response.ok) throw httpError(response, text, url, this.baseUrl);
477
+ try {
478
+ return JSON.parse(text);
479
+ } catch (cause) {
480
+ throw new QbitApiError("parse", `failed to parse JSON from ${path}`, {
481
+ url,
482
+ responseBody: responseBodyPreview(text, this.baseUrl),
483
+ cause: new SyntaxError("response body is not valid JSON"),
484
+ unredactedCause: cause
485
+ });
486
+ }
487
+ });
488
+ }
489
+ getText(path, options) {
490
+ return this.runOperation(options, async (signal) => {
491
+ const response = await this.request(path, { method: "GET", dedup: true }, { signal });
492
+ const url = this.errorUrl(path);
493
+ const text = await readBody(response, url, this.baseUrl, signal);
494
+ if (!response.ok) throw httpError(response, text, url, this.baseUrl);
495
+ return text;
496
+ });
497
+ }
498
+ /**
499
+ * POST a transaction submission under the submission retry policy and
500
+ * classify the node's reply. `txid` identifies the submission in
501
+ * `unknown_outcome` errors and is the result for already-known replies.
502
+ * An abort or deadline once the POST may have been sent, including while
503
+ * the reply body is read, is an `unknown_outcome` too, unless the reply is
504
+ * a 429.
505
+ */
506
+ submitTx(path, body, txid, options) {
507
+ return this.runOperation(options, async (signal) => {
508
+ const url = `${this.baseUrl}${path}`;
509
+ const errorUrl = this.errorUrl(path);
510
+ const response = await this.requestWithRetry(
511
+ url,
512
+ { method: "POST", body, headers: { "content-type": "text/plain" } },
513
+ signal,
514
+ txid
515
+ );
516
+ let text;
517
+ try {
518
+ text = (await readBody(response, errorUrl, this.baseUrl, signal)).trim();
519
+ } catch (cause) {
520
+ if (response.status === 429 && signal.aborted) throw abortErrorFor(signal);
521
+ throw unknownOutcomeError(errorUrl, txid, `HTTP ${response.status} reply body could not be read`, {
522
+ status: response.status,
523
+ cause
524
+ });
525
+ }
526
+ const detail = responseBodyPreview(text, this.baseUrl);
527
+ const reply = httpReplyMessage(response.status, errorUrl, detail);
528
+ if (response.ok) {
529
+ const echoed = text.toLowerCase();
530
+ if (txid === void 0) {
531
+ if (TXID_PATTERN.test(echoed)) return echoed;
532
+ } else if (echoed === txid) {
533
+ return txid;
534
+ }
535
+ const reason = txid === void 0 ? "reply is not a txid" : "reply did not echo the txid";
536
+ throw unknownOutcomeError(errorUrl, txid, `${reason} (${reply})`, {
537
+ status: response.status,
538
+ responseBody: detail
539
+ });
540
+ }
541
+ if (UNPROCESSED_SERVER_ERROR_STATUSES.has(response.status)) throw httpError(response, text, errorUrl, this.baseUrl);
542
+ const rejection = parseBroadcastReply(text);
543
+ if (isAlreadyKnownBroadcastReply(rejection)) {
544
+ if (txid !== void 0) return txid;
545
+ throw unknownOutcomeError(errorUrl, void 0, `node already knows the transaction: ${reply}`, {
546
+ status: response.status,
547
+ responseBody: detail
548
+ });
549
+ }
550
+ if (response.status >= 500) {
551
+ if (isDefiniteNodeRejection(rejection)) throw httpError(response, text, errorUrl, this.baseUrl);
552
+ throw unknownOutcomeError(errorUrl, txid, reply, { status: response.status, responseBody: detail });
553
+ }
554
+ if (isRelayedTransportFailure(rejection)) {
555
+ throw unknownOutcomeError(errorUrl, txid, `backend lost its node connection (${reply})`, {
556
+ status: response.status,
557
+ responseBody: detail
558
+ });
559
+ }
560
+ throw httpError(response, text, errorUrl, this.baseUrl);
561
+ });
562
+ }
563
+ request(path, init, options) {
564
+ return this.runOperation(options, (signal) => {
565
+ const url = `${this.baseUrl}${path}`;
566
+ return init.dedup ? this.requestShared(url, init, signal) : this.requestWithRetry(url, init, signal);
567
+ });
568
+ }
569
+ /**
570
+ * Concurrent identical GETs share one request. One caller giving up only
571
+ * stops waiting; the shared request is aborted once every caller has left.
572
+ */
573
+ async requestShared(url, init, signal) {
574
+ let pending = this.inflight.get(url);
575
+ if (pending === void 0) {
576
+ const controller = internalAbortController();
577
+ pending = {
578
+ response: this.requestWithRetry(url, init, controller.signal),
579
+ consumers: 0,
580
+ controller
581
+ };
582
+ this.inflight.set(url, pending);
583
+ }
584
+ pending.consumers += 1;
585
+ let response;
586
+ try {
587
+ response = await raceAbort(pending.response, signal);
588
+ } catch (error) {
589
+ pending.consumers -= 1;
590
+ if (pending.consumers === 0) {
591
+ if (this.inflight.get(url) === pending) this.inflight.delete(url);
592
+ if (signal.aborted) {
593
+ const errorUrl = redactBaseUrl(url, this.baseUrl);
594
+ pending.controller.abort(new QbitApiError("aborted", `request abandoned: ${errorUrl}`, { url: errorUrl }));
595
+ pending.response.then(discardResponseBody, () => void 0);
596
+ }
597
+ }
598
+ throw error;
599
+ }
600
+ pending.consumers -= 1;
601
+ if (pending.consumers === 0) {
602
+ if (this.inflight.get(url) === pending) this.inflight.delete(url);
603
+ return response;
604
+ }
605
+ return response.clone();
606
+ }
607
+ /**
608
+ * Idempotent requests (GET/HEAD) retry on network errors, timeouts, and
609
+ * transient statuses. Any other method is a submission: an attempt is only
610
+ * repeated when it provably never reached the server (429, or a connection
611
+ * failure before anything was sent), and an attempt that may have been
612
+ * processed fails with `unknown_outcome` instead of being re-sent; that
613
+ * includes an abort or deadline that fires while a submission is in flight,
614
+ * and a refused redirect. `txid` labels that error when the submission is a
615
+ * transaction.
616
+ */
617
+ async requestWithRetry(url, init, signal, txid) {
618
+ const idempotent = IDEMPOTENT_METHODS.has(init.method.toUpperCase());
619
+ const errorUrl = redactBaseUrl(url, this.baseUrl);
620
+ let lastError;
621
+ for (let attempt = 0; attempt <= this.retries; attempt += 1) {
622
+ if (signal.aborted) throw abortErrorFor(signal);
623
+ const reply = {};
624
+ let response;
625
+ try {
626
+ response = await this.fetchOnce(url, init, signal, reply);
627
+ } catch (error) {
628
+ lastError = QbitApiError.is(error) ? error : new QbitApiError("network", redactBaseUrl(String(error), this.baseUrl), {
629
+ url: errorUrl,
630
+ ...causeOptions(error, this.baseUrl)
631
+ });
632
+ if (!idempotent && reply.refusedRedirect) {
633
+ throw unknownOutcomeError(errorUrl, txid, lastError.message, {
634
+ ...lastError.status === void 0 ? {} : { status: lastError.status },
635
+ cause: lastError
636
+ });
637
+ }
638
+ if (!idempotent && reply.status === 429) {
639
+ if (signal.aborted) throw abortErrorFor(signal);
640
+ const { retryAfterMs } = reply;
641
+ if (attempt < this.retries && (retryAfterMs === void 0 || retryAfterMs <= this.maxRetryAfterMs)) {
642
+ await this.sleep(retryAfterMs ?? this.backoffMs(attempt), signal);
643
+ continue;
644
+ }
645
+ throw new QbitApiError("rate_limit", `HTTP 429 from ${errorUrl} (reply body could not be read)`, {
646
+ status: 429,
647
+ url: errorUrl,
648
+ cause: lastError,
649
+ ...retryAfterMs === void 0 ? {} : { retryAfterMs }
650
+ });
651
+ }
652
+ if (!idempotent && reply.status !== void 0) {
653
+ throw unknownOutcomeError(errorUrl, txid, `HTTP ${reply.status} reply body could not be read`, {
654
+ status: reply.status,
655
+ cause: lastError
656
+ });
657
+ }
658
+ if (idempotent && lastError.kind === "parse" && reply.status !== void 0 && TRANSIENT_STATUSES.has(reply.status)) {
659
+ const { retryAfterMs } = reply;
660
+ const overCap = retryAfterMs !== void 0 && retryAfterMs > this.maxRetryAfterMs;
661
+ if (attempt < this.retries && !overCap && !signal.aborted) {
662
+ await this.sleep(retryAfterMs ?? this.backoffMs(attempt), signal);
663
+ continue;
664
+ }
665
+ throw lastError;
666
+ }
667
+ if (lastError.kind !== "network" && lastError.kind !== "timeout" && lastError.kind !== "aborted") {
668
+ throw lastError;
669
+ }
670
+ const unsent = idempotent || lastError.kind === "network" && lastError instanceof QbitApiError && isPreConnectionFailure(QbitApiError.unredactedCause(lastError) ?? lastError);
671
+ if (!unsent) {
672
+ const reason = redactBaseUrl(String(lastError.message), this.baseUrl);
673
+ throw unknownOutcomeError(errorUrl, txid, reason, { cause: lastError });
674
+ }
675
+ if (lastError.kind !== "aborted" && attempt < this.retries && !signal.aborted) {
676
+ await this.sleep(this.backoffMs(attempt), signal);
677
+ continue;
678
+ }
679
+ throw lastError;
680
+ }
681
+ const retryable = idempotent ? TRANSIENT_STATUSES.has(response.status) : response.status === 429;
682
+ if (retryable && attempt < this.retries) {
683
+ const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
684
+ if (retryAfterMs !== void 0 && retryAfterMs > this.maxRetryAfterMs) return response;
685
+ void discardResponseBody(response);
686
+ await this.sleep(retryAfterMs ?? this.backoffMs(attempt), signal);
687
+ continue;
688
+ }
689
+ return response;
690
+ }
691
+ throw lastError ?? new QbitApiError("network", `request failed: ${errorUrl}`, { url: errorUrl });
692
+ }
693
+ /** Backoff that rejects as soon as `signal` aborts, even if `sleepFn` ignores it. */
694
+ async sleep(ms, signal) {
695
+ if (signal.aborted) throw abortErrorFor(signal);
696
+ await raceAbort(
697
+ Promise.resolve().then(() => this.sleepFn(ms, signal)),
698
+ signal
699
+ );
700
+ }
701
+ /**
702
+ * One attempt under one timeout that spans the headers and the whole body,
703
+ * also bounded by the call's signal until the body is buffered. Every
704
+ * response it returns is fully buffered, whatever its status, so no caller
705
+ * (retry loop, de-duplicated clone, or error path) can block on the network
706
+ * after the timer is cleared. A body that misses the attempt timeout fails
707
+ * the attempt as `timeout`, even for a status that would otherwise be
708
+ * retried; an abort or call deadline, before or after the headers, rejects
709
+ * with the call's `aborted` / `timeout` error. `reply.status` is set once
710
+ * the headers arrive. A redirect is refused before its body is read, which
711
+ * is cancelled instead, and `reply.refusedRedirect` is set; see
712
+ * `refuseRedirect`.
713
+ */
714
+ async fetchOnce(url, init, signal, reply) {
715
+ if (signal.aborted) throw abortErrorFor(signal);
716
+ const controller = new AbortController();
717
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
718
+ const onAbort = () => controller.abort(signal.reason);
719
+ signal.addEventListener("abort", onAbort, { once: true });
720
+ const requestInit = {
721
+ method: init.method,
722
+ headers: { ...this.headers, ...init.headers ?? {} },
723
+ mode: "cors",
724
+ redirect: "manual",
725
+ credentials: this.credentials,
726
+ signal: controller.signal
727
+ };
728
+ if (init.body !== void 0) requestInit.body = init.body;
729
+ let pending;
730
+ try {
731
+ let response;
732
+ try {
733
+ pending = this.fetchFn(url, requestInit);
734
+ response = await raceAbort(pending, signal);
735
+ } catch (cause) {
736
+ if (signal.aborted) {
737
+ pending?.then(discardResponseBody, () => void 0);
738
+ throw abortErrorFor(signal);
739
+ }
740
+ if (QbitApiError.is(cause)) throw cause;
741
+ if (isAbortError(cause)) throw this.timeoutError(url, cause);
742
+ const errorUrl = redactBaseUrl(url, this.baseUrl);
743
+ throw new QbitApiError("network", `network error: ${errorUrl}${redactBaseUrl(formatErrorCause(cause), this.baseUrl)}`, {
744
+ url: errorUrl,
745
+ ...causeOptions(cause, this.baseUrl)
746
+ });
747
+ }
748
+ try {
749
+ refuseRedirect(response, url, this.baseUrl);
750
+ } catch (refusal) {
751
+ reply.refusedRedirect = true;
752
+ throw refusal;
753
+ }
754
+ reply.status = response.status;
755
+ const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
756
+ if (retryAfterMs !== void 0) reply.retryAfterMs = retryAfterMs;
757
+ let text;
758
+ try {
759
+ text = await readBodyUntilAborted(response, controller.signal);
760
+ } catch (cause) {
761
+ if (signal.aborted) throw abortErrorFor(signal);
762
+ if (controller.signal.aborted) throw this.timeoutError(url, cause, response.status);
763
+ throw bodyReadError(response, cause, redactBaseUrl(url, this.baseUrl), this.baseUrl);
764
+ }
765
+ return bufferedResponse(response, text);
766
+ } finally {
767
+ clearTimeout(timer);
768
+ signal.removeEventListener("abort", onAbort);
769
+ }
770
+ }
771
+ /** `status` is set when the headers arrived and the body missed the deadline. */
772
+ timeoutError(url, cause, status) {
773
+ const errorUrl = redactBaseUrl(url, this.baseUrl);
774
+ return new QbitApiError(
775
+ "timeout",
776
+ `request timed out after ${this.timeoutMs}ms: ${errorUrl}${redactBaseUrl(formatErrorCause(cause), this.baseUrl)}`,
777
+ { url: errorUrl, ...causeOptions(cause, this.baseUrl), ...status === void 0 ? {} : { status } }
778
+ );
779
+ }
780
+ backoffMs(attempt) {
781
+ return Math.min(this.retryBaseDelayMs * 2 ** attempt, MAX_TIMER_DELAY_MS);
782
+ }
783
+ /** The request URL for `path` as errors show it: see `redactedBaseUrl`. */
784
+ errorUrl(path) {
785
+ return `${redactedBaseUrl(this.baseUrl)}${path}`;
786
+ }
787
+ };
788
+ var MEMPOOL_QBIT_ADDRESS_PAGE_SIZE = 10;
789
+ var MempoolQbitClient = class extends EsploraClient {
790
+ constructor(options) {
791
+ super(options);
792
+ const maxAddressHistoryPages = options.maxAddressHistoryPages ?? DEFAULT_MAX_ADDRESS_HISTORY_PAGES;
793
+ if (!Number.isSafeInteger(maxAddressHistoryPages) || maxAddressHistoryPages < 1) {
794
+ throw new QbitApiError("http", "maxAddressHistoryPages must be a positive integer");
795
+ }
796
+ this.maxAddressHistoryPages = maxAddressHistoryPages;
797
+ }
798
+ /**
799
+ * Complete address history from one bounded traversal of mempool-qbit's
800
+ * combined endpoint. Page size is intentionally not assumed.
801
+ *
802
+ * The result is checked against the address stats, read before and after
803
+ * the traversal, as `EsploraClient.getAddressHistory` describes, and a
804
+ * traversal during which they changed is repeated once. qbit-electrs
805
+ * answers a cursor that is the last mempool row with `[]` instead of the
806
+ * confirmed rows that follow, so an address whose mempool rows fill whole
807
+ * pages (50 each by default) rejects with `invalid_cursor` while it has
808
+ * confirmed history, rather than returning none.
809
+ */
810
+ getAddressHistory(address, options = {}) {
811
+ return this.runOperation(
812
+ options,
813
+ (signal) => this.scanCombinedAddressHistory(address, options, signal)
814
+ );
815
+ }
816
+ async scanCombinedAddressHistory(address, options, signal) {
817
+ const requestedMaxPages = positiveAddressHistoryLimit(
818
+ options.maxPages,
819
+ this.maxAddressHistoryPages,
820
+ "maxPages"
821
+ );
822
+ const maxPages = Math.min(requestedMaxPages, this.maxAddressHistoryPages);
823
+ const maxTransactions = positiveAddressHistoryLimit(
824
+ options.maxTransactions,
825
+ DEFAULT_MAX_ADDRESS_HISTORY_TRANSACTIONS,
826
+ "maxTransactions"
827
+ );
828
+ const budget = createAddressHistoryBudget();
829
+ return verifiedAddressHistory(
830
+ address,
831
+ "mempool-qbit",
832
+ () => this.getJson(`/address/${encodeURIComponent(address)}`, { signal }, false),
833
+ () => this.scanCombinedAddressHistoryOnce(address, maxPages, maxTransactions, budget, signal)
834
+ );
835
+ }
836
+ async scanCombinedAddressHistoryOnce(address, maxPages, maxTransactions, budget, signal) {
837
+ if (budget.pageRequests >= maxPages) {
838
+ throw this.addressHistoryPaginationLimitError(address, maxPages);
839
+ }
840
+ const state = createAddressHistoryScanState();
841
+ let cursor;
842
+ const seenCursors = /* @__PURE__ */ new Set();
843
+ while (true) {
844
+ budget.pageRequests += 1;
845
+ const suffix = cursor ? `?after_txid=${encodeURIComponent(cursor)}` : "";
846
+ const rows = reserveAddressHistoryTransactions(
847
+ budget,
848
+ await this.getJson(`/address/${encodeURIComponent(address)}/txs${suffix}`, {
849
+ signal
850
+ }),
851
+ maxTransactions,
852
+ address,
853
+ "mempool-qbit",
854
+ "mempool-qbit combined"
855
+ );
856
+ if (rows.length === 0) break;
857
+ if (budget.pageRequests === maxPages) {
858
+ throw this.addressHistoryPaginationLimitError(address, maxPages);
859
+ }
860
+ const txs = assertAddressHistoryPage(rows, address, "mempool-qbit combined");
861
+ cursor = nextAddressHistoryCursor(txs, cursor, seenCursors, address, "mempool-qbit");
862
+ appendAddressHistoryTransactions(state, txs, "combined", address, "mempool-qbit");
863
+ }
864
+ return { confirmed: state.confirmed, mempool: state.mempool };
865
+ }
866
+ /**
867
+ * Confirmed transaction history for an address, newest first.
868
+ * mempool-qbit paginates combined history with `?after_txid=...` and sorts
869
+ * mempool entries first, so full mempool-only pages must still advance.
870
+ * Without `afterTxid`, an empty page right after a mempool row is checked
871
+ * against the address stats: qbit-electrs answers that cursor with `[]`
872
+ * instead of the confirmed rows that follow, so the call rejects with
873
+ * `invalid_cursor` if the stats count more confirmed transactions than it
874
+ * found.
875
+ */
876
+ getAddressTxs(address, afterTxid, options) {
877
+ return this.runOperation(options, (signal) => this.scanAddressTxs(address, afterTxid, signal));
878
+ }
879
+ async scanAddressTxs(address, afterTxid, signal) {
880
+ const confirmedTxs = [];
881
+ let cursor;
882
+ let continuationFound = afterTxid === void 0;
883
+ let cursorIsMempoolTx = false;
884
+ const seenCursors = /* @__PURE__ */ new Set();
885
+ const seenTxids = /* @__PURE__ */ new Set();
886
+ let pageCount = 0;
887
+ while (confirmedTxs.length < 25) {
888
+ if (pageCount >= this.maxAddressHistoryPages) {
889
+ throw this.addressHistoryPaginationLimitError(address);
890
+ }
891
+ pageCount += 1;
892
+ const suffix = cursor ? `?after_txid=${encodeURIComponent(cursor)}` : "";
893
+ const txs = await this.getJson(
894
+ `/address/${encodeURIComponent(address)}/txs${suffix}`,
895
+ { signal }
896
+ );
897
+ if (txs.length === 0) {
898
+ if (!continuationFound) throw this.invalidAddressHistoryCursorError(address, afterTxid);
899
+ if (afterTxid === void 0 && cursorIsMempoolTx) {
900
+ await this.assertConfirmedHistoryReached(address, confirmedTxs.length, signal);
901
+ }
902
+ break;
903
+ }
904
+ for (const tx of txs) {
905
+ if (!continuationFound) {
906
+ if (isConfirmedTx(tx)) seenTxids.add(tx.txid);
907
+ if (tx.txid !== afterTxid) continue;
908
+ if (!isConfirmedTx(tx)) {
909
+ throw this.invalidAddressHistoryCursorError(address, afterTxid);
910
+ }
911
+ continuationFound = true;
912
+ continue;
913
+ }
914
+ if (isConfirmedTx(tx) && !seenTxids.has(tx.txid)) {
915
+ confirmedTxs.push(tx);
916
+ seenTxids.add(tx.txid);
917
+ }
918
+ }
919
+ const nextCursor = txs[txs.length - 1]?.txid;
920
+ if (confirmedTxs.length >= 25) break;
921
+ if (txs.length < MEMPOOL_QBIT_ADDRESS_PAGE_SIZE) {
922
+ if (!continuationFound) throw this.invalidAddressHistoryCursorError(address, afterTxid);
923
+ break;
924
+ }
925
+ if (!nextCursor || nextCursor === cursor || seenCursors.has(nextCursor)) {
926
+ if (!continuationFound) throw this.invalidAddressHistoryCursorError(address, afterTxid);
927
+ throw this.nonAdvancingAddressHistoryCursorError(address, nextCursor ?? cursor);
928
+ }
929
+ seenCursors.add(nextCursor);
930
+ cursor = nextCursor;
931
+ cursorIsMempoolTx = !isConfirmedTx(txs[txs.length - 1]);
932
+ }
933
+ return confirmedTxs.slice(0, 25);
934
+ }
935
+ /**
936
+ * qbit-electrs's combined route answers an `after_txid` that is the last
937
+ * mempool row with `[]`, not the confirmed rows that follow: it passes the
938
+ * mempool txid on as the chain cursor, which `history()` never finds
939
+ * (`src/rest.rs:992-998` in Qbit-Org/qbit-electrs@53102e9). Reject that
940
+ * empty page when the address stats count confirmed transactions the scan
941
+ * has not seen.
942
+ */
943
+ async assertConfirmedHistoryReached(address, confirmedSeen, signal) {
944
+ const stats = addressHistoryStats(
945
+ await this.getJson(`/address/${encodeURIComponent(address)}`, { signal }, false),
946
+ address,
947
+ "mempool-qbit"
948
+ );
949
+ if (stats.chain_stats.tx_count > confirmedSeen) {
950
+ throw new QbitApiError(
951
+ "invalid_cursor",
952
+ `mempool-qbit combined history ended after a mempool transaction with ${confirmedSeen} of ${stats.chain_stats.tx_count} confirmed transactions: ${address}`
953
+ );
954
+ }
955
+ }
956
+ /**
957
+ * mempool-qbit does not expose `/txs/mempool`. Page through the combined
958
+ * history until confirmed entries begin, collecting the leading mempool set.
959
+ */
960
+ getAddressMempoolTxs(address, options) {
961
+ return this.runOperation(options, (signal) => this.scanAddressMempoolTxs(address, signal));
962
+ }
963
+ async scanAddressMempoolTxs(address, signal) {
964
+ const mempoolTxs = [];
965
+ let cursor;
966
+ const seenCursors = /* @__PURE__ */ new Set();
967
+ const seenTxids = /* @__PURE__ */ new Set();
968
+ let pageCount = 0;
969
+ while (true) {
970
+ if (pageCount >= this.maxAddressHistoryPages) {
971
+ throw this.addressHistoryPaginationLimitError(address);
972
+ }
973
+ pageCount += 1;
974
+ const suffix = cursor ? `?after_txid=${encodeURIComponent(cursor)}` : "";
975
+ const txs = await this.getJson(
976
+ `/address/${encodeURIComponent(address)}/txs${suffix}`,
977
+ { signal }
978
+ );
979
+ if (txs.length === 0) break;
980
+ for (const tx of txs) {
981
+ if (!isConfirmedTx(tx) && !seenTxids.has(tx.txid)) {
982
+ mempoolTxs.push(tx);
983
+ seenTxids.add(tx.txid);
984
+ }
985
+ }
986
+ const nextCursor = txs[txs.length - 1]?.txid;
987
+ if (txs.some(isConfirmedTx) || txs.length < MEMPOOL_QBIT_ADDRESS_PAGE_SIZE) break;
988
+ if (!nextCursor || nextCursor === cursor || seenCursors.has(nextCursor)) {
989
+ throw this.nonAdvancingAddressHistoryCursorError(address, nextCursor ?? cursor);
990
+ }
991
+ seenCursors.add(nextCursor);
992
+ cursor = nextCursor;
993
+ }
994
+ return mempoolTxs;
995
+ }
996
+ getMempoolRecommendedFees(options) {
997
+ return this.getJson("/fees/recommended", options);
998
+ }
999
+ async getFeeEstimates(options) {
1000
+ return feeEstimatesFromMempoolRecommendedFees(await this.getMempoolRecommendedFees(options));
1001
+ }
1002
+ /**
1003
+ * Broadcast via `/tx`, falling back to the `/tx/push` form route only when
1004
+ * `/tx` reported that it did not submit the transaction. The fallback is a
1005
+ * second submission, so it follows the same policy: an already-known reply
1006
+ * resolves with the txid and an uncertain attempt rejects with
1007
+ * `unknown_outcome`. An uncertain `/tx` attempt never falls back.
1008
+ *
1009
+ * One signal and deadline cover both requests, with the same abort rules as
1010
+ * `EsploraClient.broadcastTx`. An abort or expired deadline during `/tx`
1011
+ * never sends the `/tx/push` fallback.
1012
+ */
1013
+ async broadcastTx(rawTxHex, options) {
1014
+ const txid = resolveBroadcastTxid(rawTxHex, options);
1015
+ return this.runOperation(options, async (signal) => {
1016
+ try {
1017
+ return await this.submitTx("/tx", rawTxHex, txid, { signal });
1018
+ } catch (error) {
1019
+ if (!shouldRetryBroadcastViaTxPush(error)) {
1020
+ throw error;
1021
+ }
1022
+ return this.submitTx("/tx/push", `txHash=${rawTxHex}`, txid, { signal });
1023
+ }
1024
+ });
1025
+ }
1026
+ addressHistoryPaginationLimitError(address, maxPages = this.maxAddressHistoryPages) {
1027
+ return new QbitApiError(
1028
+ "pagination_limit",
1029
+ `mempool-qbit address history exceeded ${maxPages} pages: ${address}`
1030
+ );
1031
+ }
1032
+ invalidAddressHistoryCursorError(address, afterTxid) {
1033
+ return new QbitApiError(
1034
+ "invalid_cursor",
1035
+ `mempool-qbit address history cursor is stale or not confirmed: ${afterTxid} (${address})`
1036
+ );
1037
+ }
1038
+ nonAdvancingAddressHistoryCursorError(address, cursor) {
1039
+ return new QbitApiError(
1040
+ "invalid_cursor",
1041
+ `mempool-qbit address history cursor stopped advancing: ${quotedResponseText(String(cursor ?? "(missing)"), this.baseUrl)} (${address})`
1042
+ );
1043
+ }
1044
+ };
1045
+ function addressBalanceFromStats(stats) {
1046
+ if (!isRecord(stats) || typeof stats.address !== "string" || !hasSafeTxoSums(stats.chain_stats) || !hasSafeTxoSums(stats.mempool_stats)) {
1047
+ throw new QbitApiError("parse", "address stats lacked a string address or non-negative safe-integer funded/spent sums");
1048
+ }
1049
+ const confirmedSats = stats.chain_stats.funded_txo_sum - stats.chain_stats.spent_txo_sum;
1050
+ const mempoolSats = stats.mempool_stats.funded_txo_sum - stats.mempool_stats.spent_txo_sum;
1051
+ const totalSats = confirmedSats + mempoolSats;
1052
+ if (confirmedSats < 0 || !isSafeSatAmount(totalSats)) {
1053
+ throw new QbitApiError("parse", "address stats implied a negative or unsafe balance");
1054
+ }
1055
+ return {
1056
+ address: stats.address,
1057
+ confirmedSats,
1058
+ mempoolSats,
1059
+ totalSats
1060
+ };
1061
+ }
1062
+ var DEFAULT_MAX_FEE_RATE_SAT_VB = 1e4;
1063
+ var MAX_FEE_ESTIMATE_TARGET = 1008;
1064
+ function isPositiveFeeRate(value) {
1065
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
1066
+ }
1067
+ function isSafeSatAmount(value) {
1068
+ return Number.isSafeInteger(value) && value >= 0;
1069
+ }
1070
+ function hasSafeTxoSums(value) {
1071
+ return isRecord(value) && isSafeSatAmount(value["funded_txo_sum"]) && isSafeSatAmount(value["spent_txo_sum"]);
1072
+ }
1073
+ function feeEstimatesFromMempoolRecommendedFees(fees) {
1074
+ if (!isRecord(fees)) {
1075
+ throw new QbitApiError("parse", "mempool-qbit recommended fees were not a JSON object");
1076
+ }
1077
+ for (const field of ["fastestFee", "halfHourFee", "hourFee"]) {
1078
+ if (!isPositiveFeeRate(fees[field])) {
1079
+ throw new QbitApiError("parse", `mempool-qbit recommended fees had an invalid ${field}`);
1080
+ }
1081
+ }
1082
+ return {
1083
+ "1": fees.fastestFee,
1084
+ "2": fees.halfHourFee,
1085
+ "3": fees.hourFee
1086
+ };
1087
+ }
1088
+ function feeTiersFromEstimates(estimates, options = {}) {
1089
+ const maxFeeRate = options.maxFeeRate ?? DEFAULT_MAX_FEE_RATE_SAT_VB;
1090
+ if (!isPositiveFeeRate(maxFeeRate)) {
1091
+ throw new TypeError("maxFeeRate must be a finite number above 0");
1092
+ }
1093
+ if (!isRecord(estimates)) {
1094
+ throw new QbitApiError("parse", "fee estimates were not a JSON object");
1095
+ }
1096
+ return Object.entries(estimates).map(([key, feeRate]) => {
1097
+ const target = Number(key);
1098
+ if (!/^[1-9]\d*$/.test(key) || target > MAX_FEE_ESTIMATE_TARGET) {
1099
+ throw new QbitApiError(
1100
+ "parse",
1101
+ `fee estimates had a confirmation target that is not a decimal block count from 1 to ${MAX_FEE_ESTIMATE_TARGET}`
1102
+ );
1103
+ }
1104
+ if (!isPositiveFeeRate(feeRate) || feeRate > maxFeeRate) {
1105
+ throw new QbitApiError(
1106
+ "parse",
1107
+ `fee estimate for target ${target} was not a finite rate above 0 and at most ${maxFeeRate} sat/vB`
1108
+ );
1109
+ }
1110
+ return { target, feeRate };
1111
+ }).sort((a, b) => a.target - b.target);
1112
+ }
1113
+ function txidFromRawTx(rawTxHex) {
1114
+ const txid = tryTxidFromRawTx(rawTxHex);
1115
+ if (txid === void 0) {
1116
+ throw new QbitApiError("parse", "raw transaction hex is not a well-formed transaction");
1117
+ }
1118
+ return txid;
1119
+ }
1120
+ function trimAsciiWhitespace(text) {
1121
+ let start = 0;
1122
+ let end = text.length;
1123
+ while (start < end && isAsciiWhitespace(text.charCodeAt(start))) start += 1;
1124
+ while (end > start && isAsciiWhitespace(text.charCodeAt(end - 1))) end -= 1;
1125
+ return text.slice(start, end);
1126
+ }
1127
+ function isAsciiWhitespace(code) {
1128
+ return code === 32 || code === 9 || code === 10 || code === 13;
1129
+ }
1130
+ function tryTxidFromRawTx(rawTxHex) {
1131
+ const bytes = hexToBytes(rawTxHex);
1132
+ const stripped = bytes === void 0 ? void 0 : witnessStrippedTx(bytes);
1133
+ if (stripped === void 0) return void 0;
1134
+ return bytesToHex((0, import_sha256.sha256)((0, import_sha256.sha256)(stripped)).reverse());
1135
+ }
1136
+ function resolveBroadcastTxid(rawTxHex, options) {
1137
+ if (typeof rawTxHex !== "string") {
1138
+ throw new TypeError("broadcastTx: rawTxHex must be a hex string");
1139
+ }
1140
+ const requested = (options ?? {}).txid;
1141
+ const derived = tryTxidFromRawTx(rawTxHex);
1142
+ if (requested === void 0) return derived;
1143
+ const provided = typeof requested === "string" ? requested.toLowerCase() : "";
1144
+ if (!TXID_PATTERN.test(provided)) {
1145
+ throw new TypeError("broadcastTx: options.txid must be a 64-character hex transaction id");
1146
+ }
1147
+ if (derived !== void 0 && derived !== provided) {
1148
+ throw new TypeError(
1149
+ `broadcastTx: options.txid ${provided} does not match the transaction hex (${derived})`
1150
+ );
1151
+ }
1152
+ return provided;
1153
+ }
1154
+ var COMPACT_SIZE_MAX = 33554432;
1155
+ var RawTxReader = class {
1156
+ constructor(bytes) {
1157
+ this.bytes = bytes;
1158
+ this.offset = 0;
1159
+ }
1160
+ get position() {
1161
+ return this.offset;
1162
+ }
1163
+ get remaining() {
1164
+ return this.bytes.length - this.offset;
1165
+ }
1166
+ skip(length) {
1167
+ if (length > this.remaining) throw new RangeError("raw transaction is truncated");
1168
+ this.offset += length;
1169
+ }
1170
+ readUint8() {
1171
+ this.skip(1);
1172
+ return this.bytes[this.offset - 1];
1173
+ }
1174
+ /**
1175
+ * qbit Core `ReadCompactSize` with its default range check (serialize.h:330
1176
+ * at v1.0.0). Only the shortest encoding of a value is valid: a `fd` prefix
1177
+ * must carry at least 0xfd, `fe` at least 0x10000 and `ff` at least
1178
+ * 0x100000000, or Core throws `non-canonical ReadCompactSize()`. A value
1179
+ * above MAX_SIZE (serialize.h:32) fails `size too large`, so every `ff`
1180
+ * encoding fails one check or the other. Core reads every count and length
1181
+ * in a transaction this way, witness stacks included.
1182
+ */
1183
+ readCompactSize() {
1184
+ const first = this.readUint8();
1185
+ if (first < 253) return first;
1186
+ const width = first === 253 ? 2 : first === 254 ? 4 : 8;
1187
+ let value = 0;
1188
+ for (let i = 0; i < width; i += 1) value += this.readUint8() * 2 ** (8 * i);
1189
+ const minimum = first === 253 ? 253 : first === 254 ? 65536 : 2 ** 32;
1190
+ if (value < minimum) throw new RangeError("raw transaction has a non-canonical CompactSize");
1191
+ if (value > COMPACT_SIZE_MAX) throw new RangeError("raw transaction length is too large");
1192
+ return value;
1193
+ }
1194
+ skipVarBytes() {
1195
+ this.skip(this.readCompactSize());
1196
+ }
1197
+ };
1198
+ function witnessStrippedTx(bytes) {
1199
+ const reader = new RawTxReader(bytes);
1200
+ try {
1201
+ reader.skip(4);
1202
+ const witness = reader.remaining >= 2 && bytes[4] === 0;
1203
+ if (witness) {
1204
+ if (bytes[5] !== 1) return void 0;
1205
+ reader.skip(2);
1206
+ }
1207
+ const bodyStart = reader.position;
1208
+ const inputCount = reader.readCompactSize();
1209
+ for (let i = 0; i < inputCount; i += 1) {
1210
+ reader.skip(36);
1211
+ reader.skipVarBytes();
1212
+ reader.skip(4);
1213
+ }
1214
+ const outputCount = reader.readCompactSize();
1215
+ for (let i = 0; i < outputCount; i += 1) {
1216
+ reader.skip(8);
1217
+ reader.skipVarBytes();
1218
+ }
1219
+ const bodyEnd = reader.position;
1220
+ if (witness) {
1221
+ let hasWitness = false;
1222
+ for (let i = 0; i < inputCount; i += 1) {
1223
+ const items = reader.readCompactSize();
1224
+ if (items > 0) hasWitness = true;
1225
+ for (let j = 0; j < items; j += 1) reader.skipVarBytes();
1226
+ }
1227
+ if (!hasWitness) return void 0;
1228
+ }
1229
+ reader.skip(4);
1230
+ if (reader.remaining !== 0) return void 0;
1231
+ if (!witness) return bytes;
1232
+ const stripped = new Uint8Array(4 + (bodyEnd - bodyStart) + 4);
1233
+ stripped.set(bytes.subarray(0, 4), 0);
1234
+ stripped.set(bytes.subarray(bodyStart, bodyEnd), 4);
1235
+ stripped.set(bytes.subarray(bytes.length - 4), stripped.length - 4);
1236
+ return stripped;
1237
+ } catch {
1238
+ return void 0;
1239
+ }
1240
+ }
1241
+ function hexToBytes(hex) {
1242
+ if (hex.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(hex)) return void 0;
1243
+ const bytes = new Uint8Array(hex.length / 2);
1244
+ for (let i = 0; i < bytes.length; i += 1) {
1245
+ bytes[i] = parseInt(hex.slice(2 * i, 2 * i + 2), 16);
1246
+ }
1247
+ return bytes;
1248
+ }
1249
+ function bytesToHex(bytes) {
1250
+ let hex = "";
1251
+ for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
1252
+ return hex;
1253
+ }
1254
+ function createAddressHistoryScanState() {
1255
+ return {
1256
+ confirmed: [],
1257
+ mempool: [],
1258
+ seenTxids: /* @__PURE__ */ new Set()
1259
+ };
1260
+ }
1261
+ function createAddressHistoryBudget() {
1262
+ return { pageRequests: 0, rawTransactions: 0 };
1263
+ }
1264
+ var MAX_ADDRESS_HISTORY_RESCANS = 1;
1265
+ var ADDRESS_STATS_FIELDS = [
1266
+ "funded_txo_count",
1267
+ "funded_txo_sum",
1268
+ "spent_txo_count",
1269
+ "spent_txo_sum",
1270
+ "tx_count"
1271
+ ];
1272
+ async function verifiedAddressHistory(address, backend, readStats, scan) {
1273
+ let before = addressHistoryStats(await readStats(), address, backend);
1274
+ for (let scans = 1; ; scans += 1) {
1275
+ const history = await scan();
1276
+ const after = addressHistoryStats(await readStats(), address, backend);
1277
+ if (!sameAddressHistoryStats(before, after)) {
1278
+ if (scans > MAX_ADDRESS_HISTORY_RESCANS) {
1279
+ throw new QbitApiError(
1280
+ "invalid_cursor",
1281
+ `${backend} address stats changed during each of ${scans} history scans: ${address}`
1282
+ );
1283
+ }
1284
+ before = after;
1285
+ continue;
1286
+ }
1287
+ const confirmed = after.chain_stats.tx_count;
1288
+ const mempool = after.mempool_stats.tx_count;
1289
+ if (history.confirmed.length !== confirmed || history.mempool.length !== mempool) {
1290
+ throw new QbitApiError(
1291
+ "invalid_cursor",
1292
+ `${backend} address history returned ${history.confirmed.length} confirmed and ${history.mempool.length} mempool transactions, but the address stats count ${confirmed} and ${mempool}: ${address}`
1293
+ );
1294
+ }
1295
+ return history;
1296
+ }
1297
+ }
1298
+ function addressHistoryStats(value, address, backend) {
1299
+ if (!isRecord(value) || !hasTxCount(value["chain_stats"]) || !hasTxCount(value["mempool_stats"])) {
1300
+ throw new QbitApiError(
1301
+ "parse",
1302
+ `${backend} address stats lacked a non-negative safe-integer chain or mempool tx_count: ${address}`
1303
+ );
1304
+ }
1305
+ return value;
1306
+ }
1307
+ function hasTxCount(value) {
1308
+ return isRecord(value) && Number.isSafeInteger(value["tx_count"]) && value["tx_count"] >= 0;
1309
+ }
1310
+ function sameAddressHistoryStats(a, b) {
1311
+ return ADDRESS_STATS_FIELDS.every(
1312
+ (field) => a.chain_stats[field] === b.chain_stats[field] && a.mempool_stats[field] === b.mempool_stats[field]
1313
+ );
1314
+ }
1315
+ function positiveAddressHistoryLimit(value, defaultValue, name) {
1316
+ const limit = value ?? defaultValue;
1317
+ if (!Number.isSafeInteger(limit) || limit < 1) {
1318
+ throw new QbitApiError("http", `${name} must be a positive integer`);
1319
+ }
1320
+ return limit;
1321
+ }
1322
+ function assertAddressHistoryPage(value, address, source) {
1323
+ value.forEach((tx, index) => {
1324
+ const status = isRecord(tx) ? tx["status"] : void 0;
1325
+ const vin = isRecord(tx) ? tx["vin"] : void 0;
1326
+ const vout = isRecord(tx) ? tx["vout"] : void 0;
1327
+ if (!isRecord(tx) || typeof tx["txid"] !== "string" || !/^[0-9a-f]{64}$/i.test(tx["txid"]) || !isRecord(status) || typeof status["confirmed"] !== "boolean" || !Array.isArray(vin) || !vin.every(isRecord) || !Array.isArray(vout) || !vout.every(isRecord)) {
1328
+ throw new QbitApiError(
1329
+ "parse",
1330
+ `${source} address history transaction ${index} had an invalid txid, status, vin, or vout: ${address}`
1331
+ );
1332
+ }
1333
+ const spendsFromAddress = vin.some(
1334
+ (input) => isRecord(input) && isRecord(input["prevout"]) && addressHistoryAddressMatches(input["prevout"]["scriptpubkey_address"], address)
1335
+ );
1336
+ const paysToAddress = vout.some(
1337
+ (output) => isRecord(output) && addressHistoryAddressMatches(output["scriptpubkey_address"], address)
1338
+ );
1339
+ if (!spendsFromAddress && !paysToAddress) {
1340
+ throw new QbitApiError(
1341
+ "parse",
1342
+ `${source} address history transaction ${index} did not touch ${address}`
1343
+ );
1344
+ }
1345
+ });
1346
+ return value;
1347
+ }
1348
+ function reserveAddressHistoryTransactions(budget, value, maxTransactions, address, backend, source) {
1349
+ if (!Array.isArray(value)) {
1350
+ throw new QbitApiError("parse", `${source} address history was not an array: ${address}`);
1351
+ }
1352
+ budget.rawTransactions += value.length;
1353
+ if (budget.rawTransactions > maxTransactions) {
1354
+ throw new QbitApiError(
1355
+ "pagination_limit",
1356
+ `${backend} address history exceeded ${maxTransactions} transactions: ${address}`
1357
+ );
1358
+ }
1359
+ return value;
1360
+ }
1361
+ function addressHistoryAddressMatches(value, address) {
1362
+ if (typeof value !== "string") return false;
1363
+ if (value === address) return true;
1364
+ if (!isUniformCaseQbitBech32(value) || !isUniformCaseQbitBech32(address)) return false;
1365
+ return value.toLowerCase() === address.toLowerCase();
1366
+ }
1367
+ var QBIT_BECH32_HRPS = /* @__PURE__ */ new Set(["qb", "tq", "qbrt"]);
1368
+ function isUniformCaseQbitBech32(value) {
1369
+ const normalized = value.toLowerCase();
1370
+ const separator = value.lastIndexOf("1");
1371
+ const hrp = normalized.slice(0, separator);
1372
+ const data = value.slice(separator + 1);
1373
+ const hasUniformCase = value === value.toLowerCase() || value === value.toUpperCase();
1374
+ return separator > 0 && QBIT_BECH32_HRPS.has(hrp) && data.length >= 6 && hasUniformCase && /^[qpzry9x8gf2tvdw0s3jn54khce6mua7l]+$/i.test(data);
1375
+ }
1376
+ function nextAddressHistoryCursor(txs, cursor, seenCursors, address, backend) {
1377
+ const nextCursor = txs[txs.length - 1]?.txid;
1378
+ if (!nextCursor) {
1379
+ throw new QbitApiError(
1380
+ "invalid_cursor",
1381
+ `${backend} address history cursor stopped advancing: (missing) (${address})`
1382
+ );
1383
+ }
1384
+ const normalizedNextCursor = nextCursor.toLowerCase();
1385
+ if (normalizedNextCursor === cursor?.toLowerCase() || seenCursors.has(normalizedNextCursor)) {
1386
+ throw new QbitApiError(
1387
+ "invalid_cursor",
1388
+ `${backend} address history cursor stopped advancing: ${nextCursor} (${address})`
1389
+ );
1390
+ }
1391
+ seenCursors.add(normalizedNextCursor);
1392
+ return nextCursor;
1393
+ }
1394
+ function appendAddressHistoryTransactions(state, txs, source, address, backend) {
1395
+ for (const tx of txs) {
1396
+ const normalizedTxid = tx.txid.toLowerCase();
1397
+ if (state.seenTxids.has(normalizedTxid)) {
1398
+ throw new QbitApiError(
1399
+ "invalid_cursor",
1400
+ `${backend} address history repeated transaction ${tx.txid}: ${address}`
1401
+ );
1402
+ }
1403
+ state.seenTxids.add(normalizedTxid);
1404
+ const confirmed = isConfirmedTx(tx);
1405
+ if (source === "confirmed" && !confirmed) {
1406
+ throw new QbitApiError(
1407
+ "parse",
1408
+ `esplora confirmed history returned an unconfirmed transaction ${tx.txid}: ${address}`
1409
+ );
1410
+ }
1411
+ if (source === "mempool" && confirmed) {
1412
+ throw new QbitApiError(
1413
+ "parse",
1414
+ `esplora mempool history returned a confirmed transaction ${tx.txid}: ${address}`
1415
+ );
1416
+ }
1417
+ if (confirmed) state.confirmed.push(tx);
1418
+ else state.mempool.push(tx);
1419
+ }
1420
+ }
1421
+ function addressHistoryPageLimitError(address, maxPages, backend) {
1422
+ return new QbitApiError(
1423
+ "pagination_limit",
1424
+ `${backend} address history exceeded ${maxPages} page fetches: ${address}`
1425
+ );
1426
+ }
1427
+ function isConfirmedTx(tx) {
1428
+ return tx.status?.confirmed === true;
1429
+ }
1430
+ function unknownOutcomeError(url, txid, reason, options = {}) {
1431
+ const subject = txid === void 0 ? "the transaction" : `txid ${txid}`;
1432
+ return new QbitApiError(
1433
+ "unknown_outcome",
1434
+ `transaction submission outcome unknown (${reason}); ${subject} may have been accepted, check its status before resubmitting`,
1435
+ { ...options, url, ...txid === void 0 ? {} : { txid } }
1436
+ );
1437
+ }
1438
+ function isPreConnectionFailure(error) {
1439
+ const entries = collectErrorCodes(error, 0, []);
1440
+ return entries.length > 0 && entries.every(
1441
+ (entry) => PRE_CONNECTION_ERROR_CODES.has(entry.code) || CONNECT_STAGE_ERROR_CODES.has(entry.code) && entry.syscall === "connect"
1442
+ );
1443
+ }
1444
+ function collectErrorCodes(error, depth, entries) {
1445
+ if (!isRecord(error)) return entries;
1446
+ if (depth > 5) {
1447
+ entries.push({ code: "UNJUDGED", syscall: void 0 });
1448
+ return entries;
1449
+ }
1450
+ const nested = error["errors"];
1451
+ if (Array.isArray(nested) && nested.length > 0) {
1452
+ for (const entry of nested) collectErrorCodes(entry, depth + 1, entries);
1453
+ } else if (typeof error["code"] === "string") {
1454
+ const syscall = error["syscall"];
1455
+ entries.push({ code: error["code"], syscall: typeof syscall === "string" ? syscall : void 0 });
1456
+ }
1457
+ collectErrorCodes(error["cause"], depth + 1, entries);
1458
+ return entries;
1459
+ }
1460
+ var ALREADY_KNOWN_BROADCAST_REPLY = /\b(?:txn already in mempool|txn already known|txn same nonwitness data in mempool|transaction already in block ?chain|transaction outputs already in utxo set)\b/;
1461
+ function isAlreadyKnownBroadcastReply(reply) {
1462
+ if (reply.code === -27) return true;
1463
+ return ALREADY_KNOWN_BROADCAST_REPLY.test(normalizeReplyText(reply.message));
1464
+ }
1465
+ function isRelayedTransportFailure(reply) {
1466
+ if (typeof reply.code === "string") return !PRE_CONNECTION_ERROR_CODES.has(reply.code);
1467
+ if (reply.code === -32603 || reply.code === -32602) return true;
1468
+ return /^connection error\b/.test(normalizeReplyText(reply.message));
1469
+ }
1470
+ var RPC_REQUEST_ERROR_CODES = /* @__PURE__ */ new Set([-32700, -32600, -32601]);
1471
+ function isNodeVerdictCode(code) {
1472
+ return code >= -99 && code <= -1 || RPC_REQUEST_ERROR_CODES.has(code);
1473
+ }
1474
+ var DEFINITE_REJECTION_REPLY = /^(?:bad txns |bad witness nonstandard|bad cb length|txn mempool conflict|tx decode failed|tx size\b|tx output not p2mr|scriptsig (?:size|not pushonly)|scriptpubkey\b|bare multisig|datacarrier|dust\b|non (?:bip68 )?final|(?:mempool |block |(?:non )?mandatory )script verify flag|min relay fee not met|mempool min fee not met|insufficient fee|max feerate exceeded|mempool full|too long mempool chain|truc violation|missing ephemeral spends|bip125 replacement disallowed|too many potential replacements|replacement adds unconfirmed|missing inputs|no mempool\b)/;
1475
+ function isDefiniteNodeRejection(reply) {
1476
+ if (typeof reply.code === "number") return isNodeVerdictCode(reply.code);
1477
+ if (typeof reply.code === "string") return PRE_CONNECTION_ERROR_CODES.has(reply.code);
1478
+ return DEFINITE_REJECTION_REPLY.test(normalizeReplyText(reply.message));
1479
+ }
1480
+ function shouldRetryBroadcastViaTxPush(error) {
1481
+ if (!QbitApiError.is(error)) return false;
1482
+ if (error.kind === "not_found" && error.status === 404 || error.kind === "http" && (error.status === 405 || error.status === 501)) {
1483
+ return true;
1484
+ }
1485
+ const body = error instanceof QbitApiError ? rawResponseBodies.get(error) ?? error.responseBody : void 0;
1486
+ if (error.kind !== "http" || error.status !== 400 || body === void 0) {
1487
+ return false;
1488
+ }
1489
+ const reply = parseBroadcastReply(body);
1490
+ if (reply.code !== void 0) return false;
1491
+ return /^(?:(?:failed|unable) to|could not) (?:send|submit|broadcast)(?: raw)? transaction$/.test(
1492
+ normalizeReplyText(reply.message)
1493
+ );
1494
+ }
1495
+ function parseBroadcastReply(body) {
1496
+ let text = body;
1497
+ try {
1498
+ const parsed = JSON.parse(body);
1499
+ if (typeof parsed === "string") {
1500
+ text = parsed;
1501
+ } else if (isRecord(parsed)) {
1502
+ const nested = parsed["error"];
1503
+ const code = errorCodeOf(nested) ?? errorCodeOf(parsed);
1504
+ let message = body;
1505
+ if (typeof nested === "string") message = nested;
1506
+ else if (isRecord(nested) && typeof nested["message"] === "string") message = nested["message"];
1507
+ else if (typeof parsed["message"] === "string") message = parsed["message"];
1508
+ return { code: code ?? embeddedErrorCode(message), message };
1509
+ }
1510
+ } catch {
1511
+ }
1512
+ return { code: embeddedErrorCode(text), message: text };
1513
+ }
1514
+ function errorCodeOf(value) {
1515
+ if (!isRecord(value)) return void 0;
1516
+ const code = value["code"];
1517
+ if (typeof code === "number") return Number.isInteger(code) && code < 0 ? code : void 0;
1518
+ if (typeof code !== "string") return void 0;
1519
+ const transportCode = code.toUpperCase();
1520
+ return TRANSPORT_ERROR_CODE_PATTERN.test(transportCode) ? transportCode : void 0;
1521
+ }
1522
+ function embeddedErrorCode(text) {
1523
+ const start = text.indexOf("{");
1524
+ const end = text.lastIndexOf("}");
1525
+ if (start === -1 || end <= start) return void 0;
1526
+ try {
1527
+ const parsed = JSON.parse(text.slice(start, end + 1));
1528
+ if (isRecord(parsed)) return errorCodeOf(parsed["error"]) ?? errorCodeOf(parsed);
1529
+ } catch {
1530
+ }
1531
+ return void 0;
1532
+ }
1533
+ function normalizeReplyText(text) {
1534
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
1535
+ }
1536
+ function isRecord(value) {
1537
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1538
+ }
1539
+ function parseRetryAfter(value) {
1540
+ if (!value) return void 0;
1541
+ let ms;
1542
+ const seconds = Number(value);
1543
+ if (!Number.isNaN(seconds)) {
1544
+ ms = seconds * 1e3;
1545
+ } else {
1546
+ const date = Date.parse(value);
1547
+ if (!Number.isFinite(date)) return void 0;
1548
+ ms = date - Date.now();
1549
+ }
1550
+ return Math.min(Math.max(0, Math.ceil(ms)), Number.MAX_SAFE_INTEGER);
1551
+ }
1552
+ function isAbortError(error) {
1553
+ return typeof error === "object" && error !== null && error.name === "AbortError";
1554
+ }
1555
+ function isLoopbackHostname(hostname) {
1556
+ return hostname === "localhost" || hostname === "[::1]" || /^127\.\d+\.\d+\.\d+$/.test(hostname);
1557
+ }
1558
+ var DEFAULT_PORTS = {
1559
+ "http:": "80",
1560
+ "https:": "443",
1561
+ "ws:": "80",
1562
+ "wss:": "443",
1563
+ "ftp:": "21"
1564
+ };
1565
+ function schemeAndHost(url, base) {
1566
+ const match = /^([a-z][a-z\d+.-]*:)[/\\]*([^/?#\\]*)([^#]*)/i.exec(standardHref(url, base) ?? url);
1567
+ if (!match) return void 0;
1568
+ const protocol = match[1].toLowerCase();
1569
+ const at = match[2].lastIndexOf("@");
1570
+ const authority = match[2].slice(at + 1);
1571
+ const [, hostname, port] = /^(\[[^\]]*\]|[^:]*):?(.*)$/s.exec(authority);
1572
+ return {
1573
+ protocol,
1574
+ hostname: hostname.toLowerCase().replace(/\.$/, ""),
1575
+ userinfo: at !== -1,
1576
+ port: port === DEFAULT_PORTS[protocol] ? "" : port,
1577
+ path: match[3].startsWith("/") ? match[3] : `/${match[3]}`
1578
+ };
1579
+ }
1580
+ function standardHref(url, base) {
1581
+ if (!followsUrlStandard()) return void 0;
1582
+ try {
1583
+ return new URL(url, base).href;
1584
+ } catch {
1585
+ return void 0;
1586
+ }
1587
+ }
1588
+ function followsUrlStandard() {
1589
+ try {
1590
+ return new URL("HTTP://Q.EXAMPLE:80/a/../b").href === "http://q.example/b";
1591
+ } catch {
1592
+ return false;
1593
+ }
1594
+ }
1595
+ function fromAnotherUrl(response, url) {
1596
+ if (typeof response.url !== "string" || response.url === "") return false;
1597
+ const requested = schemeAndHost(url);
1598
+ const replied = schemeAndHost(response.url);
1599
+ if (!requested || !replied) return true;
1600
+ return replied.protocol !== requested.protocol || replied.hostname !== requested.hostname || replied.port !== requested.port || replied.path !== requested.path;
1601
+ }
1602
+ function refuseRedirect(response, url, baseUrl) {
1603
+ const opaque = response.type === "opaqueredirect";
1604
+ const redirectStatus = response.status >= 300 && response.status <= 399;
1605
+ if (!opaque && !redirectStatus && response.redirected !== true && !fromAnotherUrl(response, url)) {
1606
+ return response;
1607
+ }
1608
+ void discardResponseBody(response);
1609
+ const errorUrl = redactBaseUrl(url, baseUrl);
1610
+ if (opaque) {
1611
+ throw new QbitApiError("http", `redirect refused (target hidden by the browser): ${errorUrl}`, { url: errorUrl });
1612
+ }
1613
+ if (redirectStatus) {
1614
+ const target2 = redactRedirectTarget(redirectTargetOrigin(response.headers.get("location"), url), baseUrl);
1615
+ throw new QbitApiError("http", `HTTP ${response.status} redirect to ${target2} refused: ${errorUrl}`, {
1616
+ status: response.status,
1617
+ url: errorUrl
1618
+ });
1619
+ }
1620
+ const target = redactRedirectTarget(redirectTargetOrigin(response.url, url), baseUrl);
1621
+ throw new QbitApiError("http", `fetch followed a redirect to ${target}; reply discarded: ${errorUrl}`, {
1622
+ url: errorUrl
1623
+ });
1624
+ }
1625
+ function redirectTargetOrigin(location, url) {
1626
+ if (!location) return "an unspecified location";
1627
+ const target = schemeAndHost(location, url) ?? schemeAndHost(/^[/\\]{2}/.test(location) ? `${schemeAndHost(url)?.protocol ?? ""}${location}` : url);
1628
+ if (!target) return "an invalid location";
1629
+ if (!/^(?:https?|wss?|ftp):$/.test(target.protocol)) return `a ${target.protocol} URL`;
1630
+ if (!/^(?:[a-z\d._~-]+|\[[a-f\d:.]+\])$/.test(target.hostname) || !/^\d*$/.test(target.port)) {
1631
+ return "an invalid location";
1632
+ }
1633
+ return `${target.protocol}//${target.hostname}${target.port ? `:${target.port}` : ""}`;
1634
+ }
1635
+ function redactedBaseUrl(baseUrl) {
1636
+ let href = baseUrl;
1637
+ try {
1638
+ href = new URL(baseUrl).href;
1639
+ } catch {
1640
+ }
1641
+ const match = /^(https?:)[/\\]*([^/?#\\]*)/i.exec(href);
1642
+ if (!match) return "...";
1643
+ const host = match[2].slice(match[2].lastIndexOf("@") + 1).toLowerCase();
1644
+ if (!/^(?:[a-z\d._~-]+|\[[a-f\d:.]+\])(?::\d*)?$/.test(host) || href.slice(match[0].length).includes("@")) {
1645
+ return "...";
1646
+ }
1647
+ const origin = `${match[1].toLowerCase()}//${host}`;
1648
+ return basePath(href) ? `${origin}/...` : origin;
1649
+ }
1650
+ function redactBaseUrl(text, baseUrl) {
1651
+ const spellings = [baseUrl];
1652
+ try {
1653
+ spellings.push(new URL(baseUrl).href.replace(/\/+$/, ""));
1654
+ } catch {
1655
+ }
1656
+ const redacted = redactedBaseUrl(baseUrl);
1657
+ const replacements = /* @__PURE__ */ new Map();
1658
+ for (const part of spellings.flatMap(baseUrlParts)) replacements.set(part, "...");
1659
+ for (const path of spellings.map(basePath)) if (path) replacements.set(path, "/...");
1660
+ for (const spelling of spellings) if (spelling) replacements.set(spelling, redacted);
1661
+ if (replacements.size === 0) return text;
1662
+ const pattern = [...replacements.keys()].sort((a, b) => b.length - a.length).map((needle) => needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
1663
+ return text.replace(new RegExp(pattern, "g"), (match) => replacements.get(match));
1664
+ }
1665
+ function baseUrlParts(url) {
1666
+ const match = /^(?:[a-z][a-z\d+.-]*:[/\\]*)?([^?#]*)(?:\?([^#]*))?/i.exec(url);
1667
+ const beforeQuery = match[1];
1668
+ const at = beforeQuery.lastIndexOf("@");
1669
+ const userinfo = at === -1 ? "" : beforeQuery.slice(0, at);
1670
+ const colon = userinfo.indexOf(":");
1671
+ const parts = [
1672
+ ...colon === -1 ? [userinfo] : [userinfo.slice(0, colon), userinfo.slice(colon + 1)],
1673
+ ...beforeQuery.split(/[/\\]/).slice(1),
1674
+ ...beforeQuery.slice(at + 1).split(/[/\\]/).slice(1),
1675
+ ...(match[2] ?? "").split("&").map((pair) => pair.slice(pair.indexOf("=") + 1))
1676
+ ];
1677
+ return parts.flatMap((part) => [part, percentDecoded(part)]).filter((part) => part.length >= 8);
1678
+ }
1679
+ function percentDecoded(text) {
1680
+ try {
1681
+ return decodeURIComponent(text);
1682
+ } catch {
1683
+ return text;
1684
+ }
1685
+ }
1686
+ function basePath(url) {
1687
+ const path = /^[a-z][a-z\d+.-]*:[/\\]*[^/?#\\]*(.*)$/i.exec(url)?.[1] ?? "";
1688
+ return path === "/" ? "" : path;
1689
+ }
1690
+ function redactRedirectTarget(target, baseUrl) {
1691
+ const redacted = redactBaseUrl(target, baseUrl);
1692
+ const parts = [...new Set(baseUrlParts(baseUrl))].sort((a, b) => b.length - a.length);
1693
+ if (parts.length === 0) return redacted;
1694
+ const pattern = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
1695
+ return redacted.replace(new RegExp(pattern, "gi"), "...");
1696
+ }
1697
+ function responseBodyPreview(text, baseUrl) {
1698
+ return redactBaseUrl(text.trim(), baseUrl).slice(0, 200);
1699
+ }
1700
+ function quotedResponseText(text, baseUrl) {
1701
+ return escapeControlChars(responseBodyPreview(text, baseUrl));
1702
+ }
1703
+ var REDACTED_CAUSE_DEPTH = 5;
1704
+ var NATIVE_ERRORS = [
1705
+ Error,
1706
+ TypeError,
1707
+ RangeError,
1708
+ SyntaxError,
1709
+ ReferenceError,
1710
+ EvalError,
1711
+ URIError
1712
+ ];
1713
+ function causeOptions(cause, baseUrl) {
1714
+ return { cause: redactedCause(cause, baseUrl, REDACTED_CAUSE_DEPTH), unredactedCause: cause };
1715
+ }
1716
+ function redactedCause(value, baseUrl, depth) {
1717
+ if (typeof value === "string") return redactBaseUrl(value, baseUrl);
1718
+ if (typeof value === "function") return void 0;
1719
+ if (typeof value !== "object" || value === null) return value;
1720
+ if (depth === 0) return void 0;
1721
+ if (Array.isArray(value)) return value.map((item) => redactedCause(item, baseUrl, depth - 1));
1722
+ try {
1723
+ let copy;
1724
+ const prototype = Object.getPrototypeOf(value);
1725
+ if (value instanceof Error) {
1726
+ const ErrorClass = NATIVE_ERRORS.find((errorClass) => errorClass === value.constructor) ?? Error;
1727
+ const error = new ErrorClass(redactBaseUrl(String(value.message), baseUrl));
1728
+ if (error.name !== value.name) {
1729
+ Object.defineProperty(error, "name", { value: String(value.name), writable: true, configurable: true });
1730
+ }
1731
+ error.stack = typeof value.stack === "string" ? redactBaseUrl(value.stack, baseUrl) : `${error.name}: ${error.message}`;
1732
+ copy = error;
1733
+ } else if (prototype === Object.prototype || prototype === null) {
1734
+ copy = {};
1735
+ } else {
1736
+ return void 0;
1737
+ }
1738
+ for (const key of Object.getOwnPropertyNames(value)) {
1739
+ if (value instanceof Error && (key === "message" || key === "stack")) continue;
1740
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1741
+ if (descriptor === void 0 || !("value" in descriptor)) continue;
1742
+ const redacted = redactedCause(descriptor.value, baseUrl, depth - 1);
1743
+ if (redacted === void 0 && descriptor.value !== void 0) continue;
1744
+ Object.defineProperty(copy, key, {
1745
+ value: redacted,
1746
+ enumerable: descriptor.enumerable ?? false,
1747
+ writable: true,
1748
+ configurable: true
1749
+ });
1750
+ }
1751
+ return copy;
1752
+ } catch {
1753
+ return void 0;
1754
+ }
1755
+ }
1756
+ function escapeControlChars(text) {
1757
+ return text.replace(/[\p{Cc}\p{Zl}\p{Zp}\p{Bidi_Control}]/gu, (char) => {
1758
+ if (char === "\n") return "\\n";
1759
+ if (char === "\r") return "\\r";
1760
+ if (char === " ") return "\\t";
1761
+ return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
1762
+ });
1763
+ }
1764
+ function formatErrorCause(error) {
1765
+ if (error instanceof Error) {
1766
+ return ` (${error.name}: ${error.message})`;
1767
+ }
1768
+ if (typeof error === "string") {
1769
+ return ` (${error})`;
1770
+ }
1771
+ return "";
1772
+ }
1773
+ async function readBody(response, url, baseUrl, signal) {
1774
+ if (signal.aborted) {
1775
+ const error = abortErrorFor(signal);
1776
+ void discardResponseBody(response, error);
1777
+ throw error;
1778
+ }
1779
+ try {
1780
+ return await readBodyUntilAborted(response, signal);
1781
+ } catch (cause) {
1782
+ if (signal.aborted) throw abortErrorFor(signal);
1783
+ throw bodyReadError(response, cause, url, baseUrl);
1784
+ }
1785
+ }
1786
+ function bodyReadError(response, cause, url, baseUrl) {
1787
+ return new QbitApiError("parse", `failed to read response body from ${url}`, {
1788
+ url,
1789
+ status: response.status,
1790
+ ...causeOptions(cause, baseUrl)
1791
+ });
1792
+ }
1793
+ async function readBodyUntilAborted(response, signal) {
1794
+ let rejectAborted = () => {
1795
+ };
1796
+ const aborted = new Promise((_, reject) => {
1797
+ rejectAborted = reject;
1798
+ });
1799
+ aborted.catch(() => {
1800
+ });
1801
+ const onAbort = () => rejectAborted(signal.reason);
1802
+ if (signal.aborted) onAbort();
1803
+ else signal.addEventListener("abort", onAbort, { once: true });
1804
+ try {
1805
+ const body = response.body;
1806
+ if (!body || typeof body.getReader !== "function" || typeof TextDecoder !== "function") {
1807
+ try {
1808
+ return await Promise.race([response.text(), aborted]);
1809
+ } catch (error) {
1810
+ if (signal.aborted) releaseBody(body, signal.reason);
1811
+ throw error;
1812
+ }
1813
+ }
1814
+ const reader = body.getReader();
1815
+ const decoder = new TextDecoder();
1816
+ let text = "";
1817
+ try {
1818
+ while (true) {
1819
+ const chunk = await Promise.race([reader.read(), aborted]);
1820
+ if (chunk.done) return text + decoder.decode();
1821
+ text += decoder.decode(chunk.value, { stream: true });
1822
+ }
1823
+ } catch (error) {
1824
+ reader.cancel(error).catch(() => {
1825
+ });
1826
+ throw error;
1827
+ }
1828
+ } finally {
1829
+ signal.removeEventListener("abort", onAbort);
1830
+ }
1831
+ }
1832
+ function releaseBody(body, reason) {
1833
+ if (body === null || typeof body !== "object") return;
1834
+ const stream = body;
1835
+ try {
1836
+ if (typeof stream.cancel === "function") {
1837
+ const result = stream.cancel(reason);
1838
+ if (result && typeof result.then === "function") result.then(void 0, () => {
1839
+ });
1840
+ } else if (typeof stream.destroy === "function") {
1841
+ const handled = typeof stream.listenerCount === "function" && Number(stream.listenerCount("error")) > 0;
1842
+ if (handled) stream.destroy(reason);
1843
+ else stream.destroy();
1844
+ }
1845
+ } catch {
1846
+ }
1847
+ }
1848
+ var NULL_BODY_STATUSES = /* @__PURE__ */ new Set([204, 205, 304]);
1849
+ function bufferedResponse(response, text) {
1850
+ const body = NULL_BODY_STATUSES.has(response.status) ? null : text;
1851
+ const buffered = constructedResponse(response, body) ?? plainBufferedResponse(response, body ?? "");
1852
+ Object.defineProperties(buffered, {
1853
+ url: { value: response.url },
1854
+ headers: { value: response.headers },
1855
+ clone: { value: () => bufferedResponse(response, text) }
1856
+ });
1857
+ return buffered;
1858
+ }
1859
+ function constructedResponse(response, body) {
1860
+ if (typeof Response !== "function") return void 0;
1861
+ try {
1862
+ return new Response(body, { status: response.status, statusText: response.statusText });
1863
+ } catch {
1864
+ return void 0;
1865
+ }
1866
+ }
1867
+ function plainBufferedResponse(response, text) {
1868
+ let bodyUsed = false;
1869
+ const consume = async () => {
1870
+ if (bodyUsed) throw new TypeError("Body has already been consumed");
1871
+ bodyUsed = true;
1872
+ return text;
1873
+ };
1874
+ return {
1875
+ status: response.status,
1876
+ statusText: response.statusText,
1877
+ ok: response.status >= 200 && response.status <= 299,
1878
+ redirected: response.redirected ?? false,
1879
+ type: response.type ?? "default",
1880
+ body: null,
1881
+ get bodyUsed() {
1882
+ return bodyUsed;
1883
+ },
1884
+ text: consume,
1885
+ json: async () => JSON.parse(await consume())
1886
+ };
1887
+ }
1888
+ async function discardResponseBody(response, reason) {
1889
+ if (response.body === null || response.bodyUsed) return;
1890
+ try {
1891
+ await response.body.cancel(reason);
1892
+ } catch {
1893
+ }
1894
+ }
1895
+ function httpReplyMessage(status, url, detail) {
1896
+ return `HTTP ${status} from ${url}${detail ? `: ${escapeControlChars(detail)}` : ""}`;
1897
+ }
1898
+ var rawResponseBodies = /* @__PURE__ */ new WeakMap();
1899
+ function httpError(response, body, url, baseUrl) {
1900
+ const status = response.status;
1901
+ const detail = responseBodyPreview(body, baseUrl);
1902
+ const message = httpReplyMessage(status, url, detail);
1903
+ const options = { status, url, responseBody: detail };
1904
+ const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
1905
+ if (retryAfterMs !== void 0) options.retryAfterMs = retryAfterMs;
1906
+ const error = status === 429 ? new QbitApiError("rate_limit", message, options) : status === 413 ? new QbitApiError("payload_too_large", message, options) : status === 404 ? new QbitApiError("not_found", message, options) : new QbitApiError("http", message, options);
1907
+ rawResponseBodies.set(error, body.trim().slice(0, 200));
1908
+ return error;
1909
+ }
1910
+ // Annotate the CommonJS export names for ESM import in node:
1911
+ 0 && (module.exports = {
1912
+ DEFAULT_MAX_FEE_RATE_SAT_VB,
1913
+ EsploraClient,
1914
+ MempoolQbitClient,
1915
+ QbitApiError,
1916
+ addressBalanceFromStats,
1917
+ feeEstimatesFromMempoolRecommendedFees,
1918
+ feeTiersFromEstimates,
1919
+ txidFromRawTx
1920
+ });
1921
+ //# sourceMappingURL=index.js.map