@anywalletkit/wallet-btc 0.0.0-stage → 0.2.1

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/dist/index.cjs ADDED
@@ -0,0 +1,1275 @@
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 src_exports = {};
22
+ __export(src_exports, {
23
+ BlockbookClient: () => BlockbookClient,
24
+ CAIP2_GENESIS: () => CAIP2_GENESIS,
25
+ DEFAULT_MAX_FEE_RATE: () => DEFAULT_MAX_FEE_RATE,
26
+ DUST_LIMIT_P2TR: () => DUST_LIMIT_P2TR,
27
+ DUST_LIMIT_P2WPKH: () => DUST_LIMIT_P2WPKH,
28
+ EsploraClient: () => EsploraClient,
29
+ FINAL_SEQUENCE: () => FINAL_SEQUENCE,
30
+ HistoryUnavailableError: () => HistoryUnavailableError,
31
+ INPUT_WEIGHT: () => INPUT_WEIGHT,
32
+ MAX_UTXO_INPUTS: () => MAX_UTXO_INPUTS,
33
+ RBF_SEQUENCE: () => RBF_SEQUENCE,
34
+ REGTEST_NETWORK: () => REGTEST_NETWORK,
35
+ TooManyInputsError: () => TooManyInputsError,
36
+ UnsafeTransactionError: () => UnsafeTransactionError,
37
+ UnsupportedAddressTypeError: () => UnsupportedAddressTypeError,
38
+ WalletAccountBtc: () => WalletAccountBtc,
39
+ WalletAccountReadOnlyBtc: () => WalletAccountReadOnlyBtc,
40
+ WalletManagerBtc: () => WalletManagerBtc,
41
+ addressFor: () => addressFor,
42
+ addressFromPublicKey: () => addressFromPublicKey,
43
+ addressFromScript: () => addressFromScript,
44
+ assertSafe: () => assertSafe,
45
+ btcExplorerTxUrl: () => btcExplorerTxUrl,
46
+ coinType: () => coinType,
47
+ createBtcHistory: () => createBtcHistory,
48
+ createClient: () => createClient,
49
+ decodeAddress: () => decodeAddress,
50
+ decodeHistoryCursor: () => decodeCursor,
51
+ default: () => WalletManagerBtc,
52
+ describeTransaction: () => describeTransaction,
53
+ dustLimit: () => dustLimit,
54
+ encodeHistoryCursor: () => encodeCursor,
55
+ encodeMessageSignature: () => encodeMessageSignature,
56
+ estimateVsize: () => estimateVsize,
57
+ magicHash: () => magicHash,
58
+ mapBlockbookTx: () => mapBlockbookTx,
59
+ networkParams: () => networkParams,
60
+ outputVBytes: () => outputVBytes,
61
+ parseRawTransaction: () => parseRawTransaction,
62
+ purposeFor: () => purposeFor,
63
+ recoverMessagePublicKey: () => recoverMessagePublicKey,
64
+ scriptKind: () => scriptKind,
65
+ selectAll: () => selectAll,
66
+ selectCoins: () => selectCoins,
67
+ signMessage: () => signMessage,
68
+ signMessageWith: () => signMessageWith,
69
+ spendableUtxos: () => spendableUtxos,
70
+ unconfirmedOutgoing: () => unconfirmedOutgoing
71
+ });
72
+ module.exports = __toCommonJS(src_exports);
73
+
74
+ // src/types.ts
75
+ var CAIP2_GENESIS = {
76
+ bitcoin: "bip122:000000000019d6689c085ae165831e93",
77
+ testnet: "bip122:000000000933ea01ad0ee984209779ba",
78
+ regtest: "bip122:0f9188f13cb7b2c71f2a335e3a4fc328"
79
+ };
80
+
81
+ // src/errors.ts
82
+ var import_core = require("@anywalletkit/core");
83
+ var UnsafeTransactionError = class extends Error {
84
+ code;
85
+ constructor(code, message) {
86
+ super(message);
87
+ this.name = "UnsafeTransactionError";
88
+ this.code = code;
89
+ }
90
+ };
91
+ var TooManyInputsError = class extends Error {
92
+ maxInputs;
93
+ constructor(maxInputs) {
94
+ super(`The spend needs more than ${maxInputs} inputs; consolidate the UTXOs first or raise maxInputs.`);
95
+ this.name = "TooManyInputsError";
96
+ this.maxInputs = maxInputs;
97
+ }
98
+ };
99
+ var UnsupportedAddressTypeError = class extends Error {
100
+ constructor(message) {
101
+ super(message);
102
+ this.name = "UnsupportedAddressTypeError";
103
+ }
104
+ };
105
+
106
+ // src/blockbook.ts
107
+ var import_core3 = require("@anywalletkit/core");
108
+
109
+ // src/http.ts
110
+ var import_core2 = require("@anywalletkit/core");
111
+ var DEFAULT_TIMEOUT_MS = 15e3;
112
+ async function fetchWithTimeout(fetchFn, url, init, timeoutMs, provider) {
113
+ const signal = typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(timeoutMs) : void 0;
114
+ try {
115
+ return await fetchFn(url, { ...init, ...signal ? { signal } : {} });
116
+ } catch (e) {
117
+ throw new import_core2.NetworkError(`Request failed: ${url}`, { provider, cause: e });
118
+ }
119
+ }
120
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
121
+ var isHex64 = (v) => typeof v === "string" && /^[0-9a-fA-F]{64}$/.test(v);
122
+ var isIntString = (v) => typeof v === "string" && /^-?\d+$/.test(v);
123
+
124
+ // src/blockbook.ts
125
+ var MEMPOOL_SPACE_URL = "https://mempool.space";
126
+ function unconfirmedOutgoing(transactions, address) {
127
+ const pending = (transactions ?? []).filter((tx) => tx.blockHeight === -1);
128
+ const pendingIds = new Set(pending.map((t) => t.txid));
129
+ const byId = new Map(pending.map((t) => [t.txid, t]));
130
+ const trusted = /* @__PURE__ */ new Map();
131
+ const owned = (entries) => (entries ?? []).filter((e) => e.isAddress && e.addresses?.includes(address)).reduce((s, e) => s + BigInt(e.value ?? 0), 0n);
132
+ const isTrusted = (tx) => {
133
+ if (trusted.has(tx.txid)) return trusted.get(tx.txid);
134
+ trusted.set(tx.txid, false);
135
+ const vins = tx.vin ?? [];
136
+ const ours = vins.filter((v) => v.isAddress && v.addresses?.includes(address));
137
+ const ok = vins.length > 0 && ours.length === vins.length && ours.every((v) => !v.txid || !pendingIds.has(v.txid) || isTrusted(byId.get(v.txid)));
138
+ trusted.set(tx.txid, ok);
139
+ return ok;
140
+ };
141
+ const consumed = /* @__PURE__ */ new Set();
142
+ for (const tx of pending) for (const v of tx.vin ?? []) if (v.txid && pendingIds.has(v.txid)) consumed.add(`${v.txid}:${v.vout}`);
143
+ return pending.reduce((total, tx) => {
144
+ const spent = owned((tx.vin ?? []).filter((v) => !v.txid || !pendingIds.has(v.txid)));
145
+ const change = isTrusted(tx) ? owned((tx.vout ?? []).filter((o) => !consumed.has(`${tx.txid}:${o.n}`))) : 0n;
146
+ return total + (spent - change);
147
+ }, 0n);
148
+ }
149
+ var BlockbookClient = class _BlockbookClient {
150
+ base;
151
+ fetchFn;
152
+ timeoutMs;
153
+ constructor(config, fetchFn = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS) {
154
+ this.base = config.url.replace(/\/+$/, "").replace(/\/api$/, "");
155
+ this.fetchFn = fetchFn;
156
+ this.timeoutMs = timeoutMs;
157
+ }
158
+ async connect() {
159
+ }
160
+ async close() {
161
+ }
162
+ async get(path, check) {
163
+ const url = `${this.base}/api${path}`;
164
+ const res = await fetchWithTimeout(this.fetchFn, url, void 0, this.timeoutMs, this.base);
165
+ if (!res.ok) throw new import_core3.NetworkError(`Blockbook request failed: ${res.status} ${res.statusText} (${url})`, { provider: this.base });
166
+ let body;
167
+ try {
168
+ body = await res.json();
169
+ } catch (e) {
170
+ throw new import_core3.NetworkError(`Blockbook returned non-JSON for ${url}`, { provider: this.base, cause: e });
171
+ }
172
+ if (!check(body)) throw new import_core3.NetworkError(`Blockbook returned an unexpected shape for ${url}`, { provider: this.base });
173
+ return body;
174
+ }
175
+ async getBalance(address) {
176
+ const data = await this.get(`/v2/address/${address}?details=txs`, (v) => isRecord(v) && isIntString(v.balance) && isIntString(v.unconfirmedBalance) && (v.transactions === void 0 || Array.isArray(v.transactions)));
177
+ return { confirmed: BigInt(data.balance), unconfirmed: BigInt(data.unconfirmedBalance), unconfirmedOutgoing: unconfirmedOutgoing(data.transactions, address) };
178
+ }
179
+ async listUnspent(address) {
180
+ const data = await this.get(`/v2/utxo/${address}`, (v) => Array.isArray(v) && v.every((u) => isRecord(u) && typeof u.txid === "string" && typeof u.vout === "number" && isIntString(u.value)));
181
+ return data.map((u) => ({ txid: u.txid, vout: u.vout, value: BigInt(u.value), height: u.height ?? -1 }));
182
+ }
183
+ async getHistory(address) {
184
+ const items = [];
185
+ for (let page = 1; ; page++) {
186
+ const data = await this.get(`/v2/address/${address}?details=txslight&pageSize=1000&page=${page}`, (v) => isRecord(v) && (v.transactions === void 0 || Array.isArray(v.transactions)));
187
+ for (const tx of data.transactions ?? []) items.push({ txid: tx.txid, height: tx.blockHeight });
188
+ if (page >= (data.totalPages ?? 1)) break;
189
+ }
190
+ return items;
191
+ }
192
+ /** One page of the address's transactions, newest first, with inputs / outputs (what the activity history reads). */
193
+ async getAddressPage(address, page = 1, pageSize = 25) {
194
+ const data = await this.get(`/v2/address/${address}?details=txs&page=${page}&pageSize=${pageSize}`, (v) => isRecord(v) && (v.transactions === void 0 || Array.isArray(v.transactions) && v.transactions.every(_BlockbookClient.isTx)));
195
+ const out = { ...data };
196
+ for (const tx of out.transactions ?? []) delete tx.hex;
197
+ return out;
198
+ }
199
+ /** A transaction with its inputs / outputs; undefined when Blockbook does not know it. */
200
+ async getTransactionDetails(txid) {
201
+ try {
202
+ const tx = await this.get(`/v2/tx/${txid}`, (v) => _BlockbookClient.isTx(v));
203
+ const out = { ...tx };
204
+ delete out.hex;
205
+ return out;
206
+ } catch (e) {
207
+ if (e instanceof import_core3.NetworkError && /\b(400|404)\b/.test(e.message)) return void 0;
208
+ throw e;
209
+ }
210
+ }
211
+ static isTx(v) {
212
+ return isRecord(v) && typeof v.txid === "string" && typeof v.blockHeight === "number" && typeof v.confirmations === "number";
213
+ }
214
+ async getTransactionInfo(txid) {
215
+ try {
216
+ return await this.get(`/v2/tx/${txid}`, _BlockbookClient.isTx);
217
+ } catch (e) {
218
+ if (e instanceof import_core3.NetworkError && /\b(400|404)\b/.test(e.message)) return void 0;
219
+ throw e;
220
+ }
221
+ }
222
+ async getTransaction(txid) {
223
+ const data = await this.get(`/v2/tx/${txid}`, _BlockbookClient.isTx);
224
+ if (typeof data.hex !== "string" || !data.hex) throw new import_core3.NetworkError(`Transaction ${txid} has no hex data`, { provider: this.base });
225
+ return data.hex;
226
+ }
227
+ async broadcast(rawTx) {
228
+ const data = await this.get(`/v2/sendtx/${rawTx}`, (v) => isRecord(v));
229
+ if (data.error) throw new Error(typeof data.error === "string" ? data.error : data.error.message ?? "broadcast failed");
230
+ if (!data.result) throw new import_core3.NetworkError("Blockbook returned no txid", { provider: this.base });
231
+ return data.result;
232
+ }
233
+ /** sat/vB. Blockbook reports BTC/kB (×1e5 → sat/vB); falls back to mempool.space when it has no estimate. */
234
+ async estimateFee(blocks) {
235
+ try {
236
+ const data = await this.get(`/v1/estimatefee/${blocks}`, (v) => typeof v === "string" || isRecord(v));
237
+ const rate = Number(typeof data === "string" ? data : data.result);
238
+ if (rate > 0) return Math.max(rate * 1e5, 1);
239
+ } catch {
240
+ }
241
+ const res = await fetchWithTimeout(this.fetchFn, `${MEMPOOL_SPACE_URL}/api/v1/fees/recommended`, void 0, this.timeoutMs, MEMPOOL_SPACE_URL);
242
+ if (!res.ok) throw new import_core3.NetworkError("Fee estimation request failed", { provider: MEMPOOL_SPACE_URL });
243
+ const d = await res.json();
244
+ const satPerVb = blocks <= 1 ? d.fastestFee : blocks <= 3 ? d.halfHourFee : blocks <= 6 ? d.hourFee : d.economyFee;
245
+ if (!satPerVb || satPerVb <= 0) throw new import_core3.NetworkError("Fee estimation is unavailable", { provider: MEMPOOL_SPACE_URL });
246
+ return satPerVb;
247
+ }
248
+ async getBlockHeight() {
249
+ const data = await this.get("/v2", (v) => isRecord(v));
250
+ const h = data.blockbook?.bestHeight;
251
+ if (typeof h !== "number" || h <= 0) throw new import_core3.NetworkError("Blockbook did not report a block height", { provider: this.base });
252
+ return h;
253
+ }
254
+ };
255
+
256
+ // src/esplora.ts
257
+ var import_core4 = require("@anywalletkit/core");
258
+ var isStats = (v) => isRecord(v) && typeof v.funded_txo_sum === "number" && typeof v.spent_txo_sum === "number";
259
+ var isStatus = (v) => isRecord(v) && typeof v.confirmed === "boolean";
260
+ var EsploraClient = class {
261
+ base;
262
+ fetchFn;
263
+ timeoutMs;
264
+ constructor(config, fetchFn = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS) {
265
+ const trimmed = config.url.replace(/\/+$/, "");
266
+ this.base = trimmed.endsWith("/api") ? trimmed : `${trimmed}/api`;
267
+ this.fetchFn = fetchFn;
268
+ this.timeoutMs = timeoutMs;
269
+ }
270
+ async connect() {
271
+ }
272
+ async close() {
273
+ }
274
+ async request(path, init) {
275
+ const url = `${this.base}${path}`;
276
+ const res = await fetchWithTimeout(this.fetchFn, url, init, this.timeoutMs, this.base);
277
+ if (!res.ok) throw new import_core4.NetworkError(`Esplora request failed: ${res.status} ${res.statusText} (${url})`, { provider: this.base });
278
+ return res;
279
+ }
280
+ async json(path, check) {
281
+ const res = await this.request(path);
282
+ let body;
283
+ try {
284
+ body = await res.json();
285
+ } catch (e) {
286
+ throw new import_core4.NetworkError(`Esplora returned non-JSON for ${path}`, { provider: this.base, cause: e });
287
+ }
288
+ if (!check(body)) throw new import_core4.NetworkError(`Esplora returned an unexpected shape for ${path}`, { provider: this.base });
289
+ return body;
290
+ }
291
+ async text(path, init) {
292
+ return (await this.request(path, init)).text();
293
+ }
294
+ async getBalance(address) {
295
+ const d = await this.json(`/address/${address}`, (v) => isRecord(v) && isStats(v.chain_stats) && isStats(v.mempool_stats));
296
+ const confirmed = BigInt(d.chain_stats.funded_txo_sum) - BigInt(d.chain_stats.spent_txo_sum);
297
+ const unconfirmed = BigInt(d.mempool_stats.funded_txo_sum) - BigInt(d.mempool_stats.spent_txo_sum);
298
+ const outgoing = BigInt(d.mempool_stats.spent_txo_sum) - BigInt(d.mempool_stats.funded_txo_sum);
299
+ return { confirmed, unconfirmed, unconfirmedOutgoing: outgoing > 0n ? outgoing : 0n };
300
+ }
301
+ async listUnspent(address) {
302
+ const d = await this.json(`/address/${address}/utxo`, (v) => Array.isArray(v) && v.every((u) => isRecord(u) && isHex64(u.txid) && typeof u.vout === "number" && typeof u.value === "number" && isStatus(u.status)));
303
+ return d.map((u) => ({ txid: u.txid, vout: u.vout, value: BigInt(u.value), height: u.status.confirmed && u.status.block_height ? u.status.block_height : -1 }));
304
+ }
305
+ /** Esplora pages 25 (mempool.space: up to 50) txs per call; walk `/txs/chain/<last>` until a short page. */
306
+ async getHistory(address) {
307
+ const items = [];
308
+ const check = (v) => Array.isArray(v) && v.every((t) => isRecord(t) && isHex64(t.txid) && isStatus(t.status));
309
+ let page = await this.json(`/address/${address}/txs`, check);
310
+ for (; ; ) {
311
+ for (const t of page) items.push({ txid: t.txid, height: t.status.confirmed && t.status.block_height ? t.status.block_height : -1 });
312
+ if (page.length < 25) break;
313
+ const confirmedTail = [...page].reverse().find((t) => t.status.confirmed);
314
+ if (!confirmedTail) break;
315
+ page = await this.json(`/address/${address}/txs/chain/${confirmedTail.txid}`, check);
316
+ if (page.length === 0) break;
317
+ }
318
+ return items;
319
+ }
320
+ async getTransactionInfo(txid) {
321
+ let d;
322
+ try {
323
+ d = await this.json(`/tx/${txid}`, (v) => isRecord(v) && isHex64(v.txid) && isStatus(v.status));
324
+ } catch (e) {
325
+ if (e instanceof import_core4.NetworkError && /\b(400|404)\b/.test(e.message)) return void 0;
326
+ throw e;
327
+ }
328
+ const height = d.status.confirmed && d.status.block_height ? d.status.block_height : -1;
329
+ const confirmations = height > 0 ? Math.max(await this.getBlockHeight() - height + 1, 1) : 0;
330
+ return { txid: d.txid, blockHeight: height, confirmations, ...typeof d.fee === "number" ? { fees: String(d.fee) } : {} };
331
+ }
332
+ async getTransaction(txid) {
333
+ const hex3 = (await this.text(`/tx/${txid}/hex`)).trim();
334
+ if (!/^[0-9a-fA-F]+$/.test(hex3) || hex3.length < 20) throw new import_core4.NetworkError(`Esplora returned no hex for ${txid}`, { provider: this.base });
335
+ return hex3;
336
+ }
337
+ async broadcast(rawTx) {
338
+ const id = (await this.text("/tx", { method: "POST", body: rawTx, headers: { "content-type": "text/plain" } })).trim();
339
+ if (!/^[0-9a-fA-F]{64}$/.test(id)) throw new import_core4.NetworkError(`Esplora broadcast returned an unexpected body: ${id.slice(0, 120)}`, { provider: this.base });
340
+ return id;
341
+ }
342
+ /** `/fee-estimates` → { "1": sat/vB, "2": …, "25": …, "144": … }; picks the nearest target ≥ `blocks`. */
343
+ async estimateFee(blocks) {
344
+ const d = await this.json("/fee-estimates", (v) => isRecord(v) && Object.values(v).every((n) => typeof n === "number"));
345
+ const targets = Object.keys(d).map(Number).filter((n) => Number.isFinite(n) && n > 0).sort((a, b) => a - b);
346
+ const target = targets.find((t) => t >= blocks) ?? targets.at(-1);
347
+ const rate = target !== void 0 ? d[String(target)] : void 0;
348
+ if (!rate || rate <= 0) throw new import_core4.NetworkError("Esplora has no fee estimate", { provider: this.base });
349
+ return rate;
350
+ }
351
+ async getBlockHeight() {
352
+ const h = Number((await this.text("/blocks/tip/height")).trim());
353
+ if (!Number.isInteger(h) || h <= 0) throw new import_core4.NetworkError("Esplora did not report a block height", { provider: this.base });
354
+ return h;
355
+ }
356
+ };
357
+
358
+ // src/client.ts
359
+ var import_core5 = require("@anywalletkit/core");
360
+ var isClient = (c) => typeof c === "object" && c !== null && typeof c.listUnspent === "function";
361
+ function toClient(c, fetchFn, timeoutMs) {
362
+ if (isClient(c)) return c;
363
+ switch (c.type) {
364
+ case "blockbook-http":
365
+ return new BlockbookClient(c.clientConfig, fetchFn, timeoutMs);
366
+ case "esplora-http":
367
+ return new EsploraClient(c.clientConfig, fetchFn, timeoutMs);
368
+ default:
369
+ throw new Error(`Unknown BTC client type: ${String(c.type)}`);
370
+ }
371
+ }
372
+ var METHODS = ["connect", "close", "getBalance", "listUnspent", "getHistory", "getTransaction", "getTransactionInfo", "broadcast", "estimateFee", "getBlockHeight"];
373
+ function createClient(config) {
374
+ if (config.client === void 0) return void 0;
375
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
376
+ const list = (Array.isArray(config.client) ? config.client : [config.client]).map((c) => toClient(c, config.fetch, timeoutMs));
377
+ if (list.length === 0) return void 0;
378
+ if (list.length === 1) return list[0];
379
+ const attempts = list.length + (config.retries ?? 3);
380
+ const call = (method) => async (...args) => {
381
+ let last;
382
+ for (let i = 0; i < attempts; i++) {
383
+ const c = list[i % list.length];
384
+ const fn = c[method];
385
+ if (!fn) continue;
386
+ try {
387
+ return await fn.apply(c, args);
388
+ } catch (e) {
389
+ last = e;
390
+ }
391
+ }
392
+ throw new import_core5.NetworkError(`All BTC clients failed for ${method}`, { cause: last });
393
+ };
394
+ const out = {};
395
+ for (const m of METHODS) out[m] = call(m);
396
+ return out;
397
+ }
398
+
399
+ // src/message.ts
400
+ var import_sha2 = require("@noble/hashes/sha2.js");
401
+ var import_secp256k1 = require("@noble/curves/secp256k1.js");
402
+ var import_base = require("@scure/base");
403
+ var PREFIX = new TextEncoder().encode("Bitcoin Signed Message:\n");
404
+ function varint(n) {
405
+ if (n < 253) return Uint8Array.of(n);
406
+ if (n <= 65535) return Uint8Array.of(253, n & 255, n >> 8);
407
+ return Uint8Array.of(254, n & 255, n >> 8 & 255, n >> 16 & 255, n >>> 24 & 255);
408
+ }
409
+ function magicHash(message) {
410
+ const msg = typeof message === "string" ? new TextEncoder().encode(message) : message;
411
+ const len = varint(msg.length);
412
+ const buf = new Uint8Array(PREFIX.length + len.length + msg.length);
413
+ buf.set(PREFIX, 0);
414
+ buf.set(len, PREFIX.length);
415
+ buf.set(msg, PREFIX.length + len.length);
416
+ return (0, import_sha2.sha256)((0, import_sha2.sha256)(buf));
417
+ }
418
+ function headerFlag(recovery, segwitType) {
419
+ let flag = recovery;
420
+ if (segwitType !== void 0) {
421
+ flag += 8;
422
+ if (segwitType === "p2wpkh") flag += 4;
423
+ } else flag += 4;
424
+ return 27 + flag;
425
+ }
426
+ function encodeMessageSignature(rs, recovery, segwitType = "p2wpkh") {
427
+ if (rs.length !== 64 || recovery > 1) throw new Error("BIP-137: expected a 64-byte r\u2016s signature and a 0/1 recovery id.");
428
+ const out = new Uint8Array(65);
429
+ out[0] = headerFlag(recovery, segwitType);
430
+ out.set(rs, 1);
431
+ return import_base.base64.encode(out);
432
+ }
433
+ function signMessage(message, privateKey, segwitType = "p2wpkh") {
434
+ const sig = import_secp256k1.secp256k1.sign(magicHash(message), privateKey, { prehash: false, format: "recovered" });
435
+ return encodeMessageSignature(sig.subarray(1), sig[0], segwitType);
436
+ }
437
+ async function signMessageWith(message, signer, segwitType = "p2wpkh") {
438
+ const sig = await signer.sign(magicHash(message), { scheme: "ecdsa-recoverable", context: "btc-message" });
439
+ return encodeMessageSignature(sig.subarray(0, 64), sig[64], segwitType);
440
+ }
441
+ function recoverMessagePublicKey(message, signature) {
442
+ let bytes;
443
+ try {
444
+ bytes = import_base.base64.decode(signature);
445
+ } catch {
446
+ return void 0;
447
+ }
448
+ if (bytes.length !== 65) return void 0;
449
+ const flag = bytes[0] - 27;
450
+ if (flag < 0 || flag > 15) return void 0;
451
+ const recovery = flag & 3;
452
+ const compressed = !!(flag & 4) || !!(flag & 8);
453
+ const segwit = flag & 8 ? flag & 4 ? "p2wpkh" : "p2sh(p2wpkh)" : void 0;
454
+ const recovered = new Uint8Array(65);
455
+ recovered[0] = recovery;
456
+ recovered.set(bytes.subarray(1), 1);
457
+ try {
458
+ const pub = import_secp256k1.secp256k1.recoverPublicKey(recovered, magicHash(message), { prehash: false });
459
+ const publicKey = compressed ? pub : import_secp256k1.secp256k1.Point.fromBytes(pub).toBytes(false);
460
+ return { publicKey, segwitType: segwit };
461
+ } catch {
462
+ return void 0;
463
+ }
464
+ }
465
+
466
+ // src/coinselect.ts
467
+ var import_core6 = require("@anywalletkit/core");
468
+ var MAX_UTXO_INPUTS = 200;
469
+ var OVERHEAD_WEIGHT = 42;
470
+ function estimateVsize(inputs, inputWeight, outputVBytes2) {
471
+ const outputWeight = outputVBytes2.reduce((s, vb) => s + vb * 4, 0);
472
+ const extra = (inputs > 252 ? 2 : 0) + (outputVBytes2.length > 252 ? 2 : 0);
473
+ return Math.ceil((OVERHEAD_WEIGHT + extra * 4 + inputs * inputWeight + outputWeight) / 4);
474
+ }
475
+ var cmpUtxo = (a, b) => a.value > b.value ? -1 : a.value < b.value ? 1 : a.txid < b.txid ? -1 : a.txid > b.txid ? 1 : a.vout - b.vout;
476
+ function spendableUtxos(utxos, feeRate, inputWeight) {
477
+ const inputFee = BigInt(Math.ceil(inputWeight / 4)) * feeRate;
478
+ return utxos.filter((u) => u.value > inputFee).sort(cmpUtxo);
479
+ }
480
+ function selectCoins(o) {
481
+ if (o.feeRate < 1n) throw new Error("feeRate must be at least 1 sat/vB");
482
+ const maxInputs = o.maxInputs ?? MAX_UTXO_INPUTS;
483
+ const targetSum = o.targets.reduce((s, t) => s + t.value, 0n);
484
+ const targetVB = o.targets.map((t) => t.vbytes);
485
+ const candidates = spendableUtxos(o.utxos, o.feeRate, o.inputWeight);
486
+ const available = o.utxos.reduce((s, u) => s + u.value, 0n);
487
+ const feeFor = (n, withChange) => BigInt(estimateVsize(n, o.inputWeight, withChange ? [...targetVB, o.changeVBytes] : targetVB)) * o.feeRate;
488
+ const selected = [];
489
+ let sum = 0n;
490
+ for (const u of candidates) {
491
+ if (selected.length >= maxInputs) throw new TooManyInputsError(maxInputs);
492
+ selected.push(u);
493
+ sum += u.value;
494
+ const n = selected.length;
495
+ const feeNoChange = feeFor(n, false);
496
+ if (sum < targetSum + feeNoChange) continue;
497
+ const feeWithChange = feeFor(n, true);
498
+ const change = sum - targetSum - feeWithChange;
499
+ if (change >= o.changeDust) {
500
+ return { utxos: selected, fee: feeWithChange, change, changeFolded: false, vsize: estimateVsize(n, o.inputWeight, [...targetVB, o.changeVBytes]), feeRate: o.feeRate };
501
+ }
502
+ return { utxos: selected, fee: sum - targetSum, change: 0n, changeFolded: sum - targetSum > feeNoChange, vsize: estimateVsize(n, o.inputWeight, targetVB), feeRate: o.feeRate };
503
+ }
504
+ const need = targetSum + feeFor(Math.max(selected.length, 1), false);
505
+ throw new import_core6.InsufficientFundsError(need, available, "BTC");
506
+ }
507
+ function selectAll(o) {
508
+ const maxInputs = o.maxInputs ?? MAX_UTXO_INPUTS;
509
+ const utxos = spendableUtxos(o.utxos, o.feeRate, o.inputWeight).slice(0, maxInputs);
510
+ if (utxos.length === 0) return { utxos, fee: 0n, amount: 0n, vsize: 0 };
511
+ const vsize = estimateVsize(utxos.length, o.inputWeight, [o.outputVBytes]);
512
+ const fee = BigInt(vsize) * o.feeRate;
513
+ const amount = utxos.reduce((s, u) => s + u.value, 0n) - fee;
514
+ return { utxos, fee, amount: amount > 0n ? amount : 0n, vsize };
515
+ }
516
+
517
+ // src/describe.ts
518
+ var import_btc_signer2 = require("@scure/btc-signer");
519
+ var import_base2 = require("@scure/base");
520
+
521
+ // src/address.ts
522
+ var import_btc_signer = require("@scure/btc-signer");
523
+ function decodeAddress(address, network) {
524
+ let decoded;
525
+ try {
526
+ decoded = (0, import_btc_signer.Address)(network).decode(address);
527
+ } catch (e) {
528
+ throw new Error(`Invalid bitcoin address for this network: ${address}`, { cause: e });
529
+ }
530
+ return { script: import_btc_signer.OutScript.encode(decoded), kind: decoded.type };
531
+ }
532
+ function addressFromScript(script, network) {
533
+ try {
534
+ return (0, import_btc_signer.Address)(network).encode(import_btc_signer.OutScript.decode(script));
535
+ } catch {
536
+ return void 0;
537
+ }
538
+ }
539
+ function scriptKind(script) {
540
+ try {
541
+ return import_btc_signer.OutScript.decode(script).type;
542
+ } catch {
543
+ return "unknown";
544
+ }
545
+ }
546
+ function spendInfo(publicKey, network, type) {
547
+ if (type === "p2tr") return (0, import_btc_signer.p2tr)(publicKey.length === 33 ? publicKey.subarray(1) : publicKey, void 0, network);
548
+ return (0, import_btc_signer.p2wpkh)(publicKey, network);
549
+ }
550
+ function addressFromPublicKey(publicKey, network, type = "p2wpkh") {
551
+ const { address } = spendInfo(publicKey, network, type);
552
+ if (!address) throw new Error(`Could not derive a ${type} address`);
553
+ return address;
554
+ }
555
+ var INPUT_WEIGHT = { p2wpkh: 272, p2tr: 230 };
556
+ function outputVBytes(kind, script) {
557
+ switch (kind) {
558
+ case "wpkh":
559
+ return 31;
560
+ case "tr":
561
+ case "wsh":
562
+ return 43;
563
+ case "pkh":
564
+ return 34;
565
+ case "sh":
566
+ return 32;
567
+ default:
568
+ return 9 + (script?.length ?? 34);
569
+ }
570
+ }
571
+ function dustLimit(kind) {
572
+ switch (kind) {
573
+ case "wpkh":
574
+ return 294n;
575
+ case "tr":
576
+ case "wsh":
577
+ return 330n;
578
+ case "pkh":
579
+ return 546n;
580
+ case "sh":
581
+ return 540n;
582
+ default:
583
+ return 546n;
584
+ }
585
+ }
586
+ var DUST_LIMIT_P2WPKH = dustLimit("wpkh");
587
+ var DUST_LIMIT_P2TR = dustLimit("tr");
588
+ var kindOfType = (type) => type === "p2tr" ? "tr" : "wpkh";
589
+
590
+ // src/describe.ts
591
+ var DEFAULT_MAX_FEE_RATE = 500;
592
+ var RBF_SEQUENCE = 4294967293;
593
+ var FINAL_SEQUENCE = 4294967294;
594
+ function parseRawTransaction(rawHex) {
595
+ return import_btc_signer2.Transaction.fromRaw(import_base2.hex.decode(rawHex), { allowUnknownInputs: true, allowUnknownOutputs: true });
596
+ }
597
+ function vsizeOf(tx) {
598
+ try {
599
+ return tx.vsize;
600
+ } catch {
601
+ return tx.unsignedTx.length;
602
+ }
603
+ }
604
+ function describeTransaction(tx, inputValues, ctx) {
605
+ if (inputValues.length !== tx.inputsLength) throw new Error(`describeTransaction: ${tx.inputsLength} inputs but ${inputValues.length} values`);
606
+ const inputs = Array.from({ length: tx.inputsLength }, (_, i) => {
607
+ const inp = tx.getInput(i);
608
+ return { txid: inp.txid ? import_base2.hex.encode(inp.txid) : "", vout: inp.index ?? 0, value: inputValues[i] };
609
+ });
610
+ const outputs = [];
611
+ const warnings = /* @__PURE__ */ new Set();
612
+ for (let i = 0; i < tx.outputsLength; i++) {
613
+ const out = tx.getOutput(i);
614
+ const value = out.amount ?? 0n;
615
+ const address = tx.getOutputAddress(i, ctx.network);
616
+ const role = address !== void 0 && address === ctx.ownAddress ? "change" : ctx.intended === void 0 ? "recipient" : address !== void 0 && address === ctx.intended.to && value === ctx.intended.value ? "recipient" : "unknown";
617
+ if (out.script && value < dustLimit(scriptKind(out.script))) warnings.add("dust-output");
618
+ outputs.push({ address, value, role });
619
+ }
620
+ const inputSum = inputs.reduce((s, i) => s + i.value, 0n);
621
+ const outputSum = outputs.reduce((s, o) => s + o.value, 0n);
622
+ const fee = inputSum - outputSum;
623
+ const amount = outputs.filter((o) => o.role !== "change").reduce((s, o) => s + o.value, 0n);
624
+ const vsize = vsizeOf(tx);
625
+ const feeRate = vsize > 0 ? Math.round(Number(fee) / vsize * 100) / 100 : 0;
626
+ if (feeRate > (ctx.maxFeeRate ?? DEFAULT_MAX_FEE_RATE)) warnings.add("high-fee-rate");
627
+ if (amount > 0n && fee * 10n > amount) warnings.add("fee-over-10pct-of-amount");
628
+ if (!outputs.some((o) => o.role === "change")) warnings.add("no-change");
629
+ let rbf = false;
630
+ for (let i = 0; i < tx.inputsLength; i++) if ((tx.getInput(i).sequence ?? 4294967295) < FINAL_SEQUENCE) rbf = true;
631
+ const desc = { kind: "btc-transfer", inputs, outputs, amount, fee, feeRate, vsize, rbf, lockTime: tx.lockTime, warnings: [...warnings] };
632
+ if (tx.isFinal) desc.txid = tx.id;
633
+ return desc;
634
+ }
635
+ function assertSafe(desc, ctx) {
636
+ const maxFeeRate = ctx.maxFeeRate ?? DEFAULT_MAX_FEE_RATE;
637
+ if (desc.feeRate > maxFeeRate) {
638
+ throw new UnsafeTransactionError("fee-rate-too-high", `Fee rate ${desc.feeRate} sat/vB exceeds the ${maxFeeRate} sat/vB ceiling (maxFeeRate).`);
639
+ }
640
+ if (desc.fee < 0n) throw new UnsafeTransactionError("unexpected-output", "Outputs exceed inputs.");
641
+ for (const o of desc.outputs) {
642
+ if (o.role === "change" && o.address !== ctx.ownAddress) throw new UnsafeTransactionError("change-not-ours", `Change output pays ${o.address ?? "<no address>"}, not ${ctx.ownAddress}.`);
643
+ }
644
+ if (ctx.intended) {
645
+ const recipients = desc.outputs.filter((o) => o.role === "recipient");
646
+ if (recipients.length !== 1) throw new UnsafeTransactionError("recipient-mismatch", `Expected one output of ${ctx.intended.value} sat to ${ctx.intended.to}, found ${recipients.length}.`);
647
+ const unknown = desc.outputs.filter((o) => o.role === "unknown");
648
+ if (unknown.length) throw new UnsafeTransactionError("unexpected-output", `Transaction carries ${unknown.length} output(s) that are neither the recipient nor our change.`);
649
+ }
650
+ }
651
+
652
+ // src/network.ts
653
+ var import_btc_signer3 = require("@scure/btc-signer");
654
+ var REGTEST_NETWORK = Object.freeze({ bech32: "bcrt", pubKeyHash: 111, scriptHash: 196, wif: 239 });
655
+ function networkParams(network) {
656
+ switch (network) {
657
+ case "bitcoin":
658
+ return import_btc_signer3.NETWORK;
659
+ case "testnet":
660
+ return import_btc_signer3.TEST_NETWORK;
661
+ case "regtest":
662
+ return REGTEST_NETWORK;
663
+ default:
664
+ throw new Error(`Unknown bitcoin network: ${String(network)}`);
665
+ }
666
+ }
667
+ var coinType = (network) => network === "bitcoin" ? 0 : 1;
668
+ var purposeFor = (type) => type === "p2tr" ? 86 : 84;
669
+
670
+ // src/read-only-account.ts
671
+ var import_core7 = require("@anywalletkit/core");
672
+ var toBig = (v) => typeof v === "bigint" ? v : BigInt(Math.round(Number(v)));
673
+ var WalletAccountReadOnlyBtc = class {
674
+ /** The address. */
675
+ address;
676
+ chain;
677
+ /** Output script of `address`. */
678
+ scriptPubKey;
679
+ /** Script type of the address. */
680
+ scriptKind;
681
+ /** p2wpkh / p2tr when the address is one we know how to spend from; undefined for other script types. */
682
+ addressType;
683
+ /** Smallest output this address type may create, in satoshis. */
684
+ dustLimit;
685
+ _config;
686
+ _client;
687
+ _network;
688
+ _networkName;
689
+ constructor(address, config = {}) {
690
+ this._config = config;
691
+ this._networkName = config.network ?? "bitcoin";
692
+ this._network = networkParams(this._networkName);
693
+ const decoded = decodeAddress(address, this._network);
694
+ this.address = address;
695
+ this.scriptPubKey = decoded.script;
696
+ this.scriptKind = decoded.kind;
697
+ this.addressType = decoded.kind === "wpkh" ? "p2wpkh" : decoded.kind === "tr" ? "p2tr" : void 0;
698
+ this.dustLimit = dustLimit(decoded.kind);
699
+ this.chain = CAIP2_GENESIS[this._networkName];
700
+ this._client = createClient(config);
701
+ }
702
+ requireClient(what) {
703
+ if (!this._client) throw new import_core7.NetworkError(`The wallet must be connected to a client to ${what}.`);
704
+ return this._client;
705
+ }
706
+ /** Fee-rate ceiling in sat/vB: `BtcConfig.maxFeeRate` or {@link DEFAULT_MAX_FEE_RATE}. */
707
+ get maxFeeRate() {
708
+ return this._config.maxFeeRate ?? DEFAULT_MAX_FEE_RATE;
709
+ }
710
+ /** The address. */
711
+ async getAddress() {
712
+ return this.address;
713
+ }
714
+ /** Confirmed minus pending outgoing (Bitcoin Core wallet semantics). */
715
+ async getBalance() {
716
+ const { confirmed, unconfirmed, unconfirmedOutgoing: unconfirmedOutgoing2 } = await this.requireClient("read balances").getBalance(this.address);
717
+ return unconfirmedOutgoing2 !== void 0 ? confirmed - unconfirmedOutgoing2 : confirmed + unconfirmed;
718
+ }
719
+ /** Always throws: Bitcoin has no tokens. */
720
+ async getTokenBalance(_token) {
721
+ throw new Error("The 'getTokenBalance' method is not supported on the bitcoin blockchain.");
722
+ }
723
+ /** Weight one of OUR inputs adds; throws for address types we cannot spend from. */
724
+ inputWeight() {
725
+ if (!this.addressType) throw new UnsupportedAddressTypeError(`Cannot build spends from a ${this.scriptKind} address; only p2wpkh and p2tr accounts spend.`);
726
+ return INPUT_WEIGHT[this.addressType];
727
+ }
728
+ async resolveFeeRate(spend) {
729
+ if (spend.feeRate !== void 0) {
730
+ const r = toBig(spend.feeRate);
731
+ return r < 1n ? 1n : r;
732
+ }
733
+ const est = await this.requireClient("estimate fees").estimateFee(spend.confirmationTarget ?? 1);
734
+ return BigInt(Math.max(Math.ceil(est), 1));
735
+ }
736
+ /** Coin selection for a spend (see coinselect.ts). Validates the recipient and the dust floor first. */
737
+ async planSpend(spend) {
738
+ const amount = toBig(spend.value);
739
+ const recipient = decodeAddress(spend.to, this._network);
740
+ const floor = dustLimit(recipient.kind);
741
+ if (amount < floor) throw new Error(`The amount must be at least the dust limit of the recipient's address type (${floor} sat).`);
742
+ const inputWeight = this.inputWeight();
743
+ const feeRate = await this.resolveFeeRate(spend);
744
+ const utxos = await this.requireClient("plan a spend").listUnspent(this.address);
745
+ if (utxos.length === 0) throw new import_core7.InsufficientFundsError(amount, 0n, "BTC");
746
+ const own = kindOfType(this.addressType);
747
+ const selection = selectCoins({
748
+ utxos,
749
+ targets: [{ value: amount, vbytes: outputVBytes(recipient.kind, recipient.script) }],
750
+ feeRate,
751
+ inputWeight,
752
+ changeVBytes: outputVBytes(own),
753
+ changeDust: dustLimit(own),
754
+ maxInputs: this._config.maxInputs ?? MAX_UTXO_INPUTS
755
+ });
756
+ return { ...selection, amount, recipientScript: recipient.script, recipientKind: recipient.kind };
757
+ }
758
+ /** Fee of a spend in satoshis, from a coin selection at the current fee rate. */
759
+ async quoteSendTransaction(tx) {
760
+ const { fee } = await this.planSpend(tx);
761
+ return { fee };
762
+ }
763
+ /**
764
+ * Fee of a transfer in satoshis.
765
+ *
766
+ * @throws Error when `token` is set.
767
+ */
768
+ async quoteTransfer(o) {
769
+ if (o.token) throw new Error("Tokens are not supported on the bitcoin blockchain.");
770
+ return this.quoteSendTransaction({ to: o.recipient, value: o.amount });
771
+ }
772
+ /** Everything spendable in ONE transaction to a single output of our own address type, no change. */
773
+ async getMaxSpendable(opts = {}) {
774
+ const inputWeight = this.inputWeight();
775
+ const feeRate = await this.resolveFeeRate(opts);
776
+ const utxos = await this.requireClient("plan a spend").listUnspent(this.address);
777
+ const outKind = opts.to ? decodeAddress(opts.to, this._network).kind : kindOfType(this.addressType);
778
+ const sweep = selectAll({ utxos, feeRate, inputWeight, outputVBytes: outputVBytes(outKind), maxInputs: this._config.maxInputs ?? MAX_UTXO_INPUTS });
779
+ const amount = sweep.amount >= dustLimit(outKind) ? sweep.amount : 0n;
780
+ return { amount, fee: amount > 0n ? sweep.fee : 0n, changeValue: 0n, inputs: amount > 0n ? sweep.utxos.length : 0, feeRate };
781
+ }
782
+ /** Undefined until the transaction is mined. */
783
+ async getTransactionReceipt(hash) {
784
+ if (!/^[0-9a-fA-F]{64}$/.test(hash)) throw new Error("The 'getTransactionReceipt(hash)' method requires a valid transaction hash.");
785
+ const client = this.requireClient("fetch transaction receipts");
786
+ const info = client.getTransactionInfo ? await client.getTransactionInfo(hash) : void 0;
787
+ if (!info || !info.blockHeight || info.blockHeight <= 0) return void 0;
788
+ return {
789
+ hash,
790
+ status: 1,
791
+ blockNumber: BigInt(info.blockHeight),
792
+ confirmations: info.confirmations,
793
+ ...info.fees !== void 0 ? { fee: BigInt(info.fees) } : {},
794
+ ...info.hex !== void 0 ? { hex: info.hex } : {}
795
+ };
796
+ }
797
+ /** BIP-137: recover the key and compare its p2wpkh address with ours. Taproot addresses have no BIP-137 form. */
798
+ async verify(message, signature) {
799
+ if (this.addressType === "p2tr") throw new UnsupportedAddressTypeError("BIP-137 message signatures do not exist for taproot addresses (BIP-322 is not implemented).");
800
+ const rec = recoverMessagePublicKey(message, signature);
801
+ if (!rec || rec.publicKey.length !== 33) return false;
802
+ try {
803
+ return spendInfo(rec.publicKey, this._network, "p2wpkh").address === this.address;
804
+ } catch {
805
+ return false;
806
+ }
807
+ }
808
+ /** Chain tip for anti-fee-sniping; undefined when disabled or the transport cannot tell. */
809
+ async lockTimeHeight() {
810
+ if (this._config.antiFeeSniping === false || !this._client?.getBlockHeight) return 0;
811
+ try {
812
+ return await this._client.getBlockHeight();
813
+ } catch {
814
+ return 0;
815
+ }
816
+ }
817
+ };
818
+
819
+ // src/account.ts
820
+ var import_core8 = require("@anywalletkit/core");
821
+ var import_btc_signer4 = require("@scure/btc-signer");
822
+ var import_secp256k12 = require("@noble/curves/secp256k1.js");
823
+ var import_base3 = require("@scure/base");
824
+ function addressFor(publicKey, network = "bitcoin", type = "p2wpkh") {
825
+ return addressFromPublicKey(publicKey, typeof network === "string" ? networkParams(network) : network, type);
826
+ }
827
+ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc {
828
+ /** UTXOs, fee rate and tip height come from the network before signing. */
829
+ needsNetwork = true;
830
+ /** Derivation path, or `external` for an external signer. */
831
+ path;
832
+ /** Compressed secp256k1 public key. */
833
+ publicKey;
834
+ addressType;
835
+ /** The signer that holds the key. */
836
+ signer;
837
+ _disposed = false;
838
+ _readOnly;
839
+ /** `publicKey` is the signer's compressed secp256k1 key (33 bytes); it is needed synchronously for the address. */
840
+ constructor(signer, publicKey, path, config = {}) {
841
+ const type = config.addressType ?? "p2wpkh";
842
+ super(addressFromPublicKey(publicKey, networkParams(config.network ?? "bitcoin"), type), config);
843
+ this.addressType = type;
844
+ this.signer = (0, import_core8.requireCurve)(signer, "secp256k1");
845
+ const needed = type === "p2tr" ? "schnorr" : "ecdsa";
846
+ if (!this.signer.schemes.includes(needed)) throw new import_core8.SignerError(`A ${type} account needs a signer with the '${needed}' scheme (has: ${this.signer.schemes.join(", ")}).`);
847
+ this.publicKey = publicKey;
848
+ this.path = path;
849
+ }
850
+ /** Back-compat: wrap a raw derived key in a {@link LocalSigner} (the account takes ownership; it cannot derive). */
851
+ static fromPrivateKey(privateKey, publicKey, path, config = {}) {
852
+ return new _WalletAccountBtc(import_core8.LocalSigner.fromPrivateKey("secp256k1", privateKey, publicKey, path), publicKey, path, config);
853
+ }
854
+ /** Account index for the standard m/{84|86}'/{0|1}'/0'/0/i path. */
855
+ get index() {
856
+ const m = /^m\/8[46]'\/[01]'\/0'\/0\/(\d+)$/.exec(this.path);
857
+ return m ? Number(m[1]) : void 0;
858
+ }
859
+ live() {
860
+ if (this._disposed) throw new import_core8.SignerError("This account has been disposed.");
861
+ }
862
+ /** BIP-137 p2wpkh signature, base64 (Electrum / Bitcoin Core encoding). Not available for taproot accounts. */
863
+ async sign(message) {
864
+ this.live();
865
+ if (this.addressType === "p2tr") throw new UnsupportedAddressTypeError("BIP-137 message signatures do not exist for taproot accounts (BIP-322 is not implemented).");
866
+ return signMessageWith(message, this.signer, "p2wpkh");
867
+ }
868
+ /** The x-only internal key and its BIP-86 tweak (no script tree) for taproot key-path spends. */
869
+ taprootKeys() {
870
+ const xonly = this.publicKey.length === 33 ? this.publicKey.subarray(1) : this.publicKey;
871
+ return { xonly, tweak: import_secp256k12.schnorr.utils.taggedHash("TapTweak", xonly) };
872
+ }
873
+ /**
874
+ * Sign every input through the signer and attach the witness-ready signature:
875
+ * p2wpkh → `partialSig` (DER + SIGHASH_ALL), p2tr → `tapKeySig` (64 bytes, DEFAULT).
876
+ * btc-signer only computes the sighash and assembles the witness; it never sees a key.
877
+ */
878
+ async signInputs(tx, values) {
879
+ if (this.addressType === "p2tr") {
880
+ const { tweak } = this.taprootKeys();
881
+ const scripts = Array.from({ length: tx.inputsLength }, () => this.scriptPubKey);
882
+ for (let i = 0; i < tx.inputsLength; i++) {
883
+ const sighash = tx.preimageWitnessV1(i, scripts, import_btc_signer4.SigHash.DEFAULT, values);
884
+ const sig = await this.signer.sign(sighash, { scheme: "schnorr", context: "btc-taproot-keypath", tweak });
885
+ tx.updateInput(i, { tapKeySig: sig }, true);
886
+ }
887
+ return;
888
+ }
889
+ const scriptCode = (0, import_btc_signer4.p2pkh)(this.publicKey).script;
890
+ for (let i = 0; i < tx.inputsLength; i++) {
891
+ const sighash = tx.preimageWitnessV0(i, scriptCode, import_btc_signer4.SigHash.ALL, values[i]);
892
+ const rs = await this.signer.sign(sighash, { scheme: "ecdsa", context: "btc-sighash" });
893
+ const der = import_secp256k12.secp256k1.Signature.fromBytes(rs, "compact").toBytes("der");
894
+ const sig = new Uint8Array(der.length + 1);
895
+ sig.set(der, 0);
896
+ sig[der.length] = import_btc_signer4.SigHash.ALL;
897
+ tx.updateInput(i, { partialSig: [[this.publicKey, sig]] }, true);
898
+ }
899
+ }
900
+ ctx(intended) {
901
+ return { network: this._network, ownAddress: this.address, intended, maxFeeRate: this.maxFeeRate };
902
+ }
903
+ async buildAndSign(plan, to, recipientValue, change, lockTime, rbf) {
904
+ const spend = spendInfo(this.publicKey, this._network, this.addressType);
905
+ const tx = new import_btc_signer4.Transaction({ lockTime });
906
+ const sequence = rbf ? RBF_SEQUENCE : FINAL_SEQUENCE;
907
+ for (const u of plan.utxos) {
908
+ tx.addInput({ ...spend, txid: u.txid, index: u.vout, sequence, witnessUtxo: { script: this.scriptPubKey, amount: u.value } });
909
+ }
910
+ tx.addOutputAddress(to, recipientValue, this._network);
911
+ if (change > 0n) tx.addOutputAddress(this.address, change, this._network);
912
+ await this.signInputs(tx, plan.utxos.map((u) => u.value));
913
+ tx.finalize();
914
+ return tx;
915
+ }
916
+ /**
917
+ * Plan → sign → measure. The plan's fee assumes 72-byte DER signatures; if the
918
+ * signed vsize somehow needs more, the difference comes out of change (never the
919
+ * recipient) and the tx is re-signed once.
920
+ */
921
+ async buildSignedTransaction(spend) {
922
+ this.live();
923
+ const plan = await this.planSpend(spend);
924
+ await this.verifyPrevouts(plan.utxos);
925
+ const lockTime = await this.lockTimeHeight();
926
+ const rbf = spend.rbf ?? this._config.rbf ?? true;
927
+ let change = plan.change;
928
+ let fee = plan.fee;
929
+ let tx = await this.buildAndSign(plan, spend.to, plan.amount, change, lockTime, rbf);
930
+ const required = BigInt(tx.vsize) * plan.feeRate;
931
+ if (required > fee) {
932
+ const delta = required - fee;
933
+ if (change === 0n) {
934
+ const have = plan.utxos.reduce((s, u) => s + u.value, 0n);
935
+ throw new import_core8.InsufficientFundsError(plan.amount + required, have, "BTC");
936
+ }
937
+ change = change - delta;
938
+ if (change < this.dustLimit) {
939
+ fee = plan.fee + plan.change;
940
+ change = 0n;
941
+ } else fee = required;
942
+ tx = await this.buildAndSign(plan, spend.to, plan.amount, change, lockTime, rbf);
943
+ if (BigInt(tx.vsize) * plan.feeRate > fee) throw new Error("Fee shortfall after output rebalance.");
944
+ }
945
+ const ctx = this.ctx({ to: spend.to, value: plan.amount });
946
+ const desc = describeTransaction(tx, plan.utxos.map((u) => u.value), ctx);
947
+ assertSafe(desc, ctx);
948
+ return { tx, fee, plan, desc };
949
+ }
950
+ checkMax(fee, limit, op) {
951
+ if (limit !== void 0 && fee > BigInt(limit)) throw new Error(`Exceeded maximum fee cost for ${op} operation.`);
952
+ }
953
+ /**
954
+ * BIP-143 (p2wpkh) commits each input's signature to that input's CLAIMED amount
955
+ * only: an indexer that understates a different coin in each of two signing
956
+ * sessions can combine their signatures into one valid tx whose real fee the
957
+ * wallet never showed (scan 2026-10-23 F2). So every p2wpkh coin is read from
958
+ * its full previous transaction, which must hash to the claimed txid, and its
959
+ * amount and script must match before anything is signed. Taproot (BIP-341)
960
+ * commits to every input amount and skips the extra requests.
961
+ */
962
+ async verifyPrevouts(utxos) {
963
+ if (this.addressType === "p2tr") return;
964
+ const client = this.requireClient("verify the coins it spends");
965
+ const ours = import_base3.hex.encode(this.scriptPubKey);
966
+ const cache = /* @__PURE__ */ new Map();
967
+ for (const u of utxos) {
968
+ let prev = cache.get(u.txid);
969
+ if (!prev) {
970
+ prev = parseRawTransaction(await client.getTransaction(u.txid));
971
+ if (prev.id !== u.txid) throw new UnsafeTransactionError("prevout-mismatch", `The indexer's transaction for ${u.txid} hashes to ${prev.id}.`);
972
+ cache.set(u.txid, prev);
973
+ }
974
+ const out = u.vout < prev.outputsLength ? prev.getOutput(u.vout) : void 0;
975
+ if (out?.amount === void 0 || out.amount !== u.value) {
976
+ throw new UnsafeTransactionError("prevout-mismatch", `Coin ${u.txid}:${u.vout} is worth ${out?.amount ?? "nothing"} sat on chain, not the ${u.value} the indexer reported.`);
977
+ }
978
+ if (!out.script || import_base3.hex.encode(out.script) !== ours) throw new UnsafeTransactionError("prevout-mismatch", `Coin ${u.txid}:${u.vout} does not pay this account.`);
979
+ }
980
+ }
981
+ /** Prevout values of a raw tx, fetched through the client (the wire format carries none). */
982
+ async inputValues(tx) {
983
+ const client = this.requireClient("inspect a raw transaction");
984
+ const cache = /* @__PURE__ */ new Map();
985
+ const values = [];
986
+ for (let i = 0; i < tx.inputsLength; i++) {
987
+ const inp = tx.getInput(i);
988
+ if (!inp.txid || inp.index === void 0) throw new Error("Raw transaction input without a prevout.");
989
+ const id = import_base3.hex.encode(inp.txid);
990
+ let prev = cache.get(id);
991
+ if (!prev) {
992
+ prev = parseRawTransaction(await client.getTransaction(id));
993
+ if (prev.id !== id) throw new UnsafeTransactionError("prevout-mismatch", `The indexer's transaction for ${id} hashes to ${prev.id}.`);
994
+ cache.set(id, prev);
995
+ }
996
+ const out = prev.getOutput(inp.index);
997
+ if (out.amount === void 0) throw new Error(`Prevout ${id}:${inp.index} not found.`);
998
+ values.push(out.amount);
999
+ }
1000
+ return values;
1001
+ }
1002
+ /** Describe a raw signed tx (needs the network for prevout values) and run the safety checks. */
1003
+ async describeRaw(rawHex) {
1004
+ const tx = parseRawTransaction(rawHex);
1005
+ const ctx = this.ctx();
1006
+ const desc = describeTransaction(tx, await this.inputValues(tx), ctx);
1007
+ assertSafe(desc, ctx);
1008
+ return { tx, desc };
1009
+ }
1010
+ /** "What you sign": the fully resolved spend (signed, NOT broadcast) or a caller's raw hex. */
1011
+ async describeTransaction(tx) {
1012
+ if (typeof tx === "string") return (await this.describeRaw(tx)).desc;
1013
+ return (await this.buildSignedTransaction(tx)).desc;
1014
+ }
1015
+ /** Sign without broadcasting. Returns the raw transaction hex. Raw hex input is returned unchanged. */
1016
+ async signTransaction(tx) {
1017
+ this.live();
1018
+ if (typeof tx === "string") return tx;
1019
+ const { tx: built, fee } = await this.buildSignedTransaction(tx);
1020
+ this.checkMax(fee, this._config.transactionMaxFee, "transaction");
1021
+ return built.hex;
1022
+ }
1023
+ async quoteSendTransaction(tx) {
1024
+ if (typeof tx === "string") return { fee: (await this.describeRaw(tx)).desc.fee };
1025
+ return super.quoteSendTransaction(tx);
1026
+ }
1027
+ async sendTransaction(tx) {
1028
+ this.live();
1029
+ const client = this.requireClient("send transactions");
1030
+ let rawHex, hash, fee;
1031
+ if (typeof tx === "string") {
1032
+ const { tx: parsed, desc } = await this.describeRaw(tx);
1033
+ rawHex = tx;
1034
+ hash = parsed.id;
1035
+ fee = desc.fee;
1036
+ } else {
1037
+ const built = await this.buildSignedTransaction(tx);
1038
+ rawHex = built.tx.hex;
1039
+ hash = built.tx.id;
1040
+ fee = built.fee;
1041
+ }
1042
+ this.checkMax(fee, this._config.transactionMaxFee, "transaction");
1043
+ const txid = await client.broadcast(rawHex);
1044
+ return { hash: txid || hash, fee };
1045
+ }
1046
+ /** Native BTC only (`token` is rejected). Subject to `transferMaxFee`. */
1047
+ async transfer(o) {
1048
+ if (o.token) throw new Error("Tokens are not supported on the bitcoin blockchain.");
1049
+ this.live();
1050
+ const client = this.requireClient("transfer");
1051
+ const built = await this.buildSignedTransaction({ to: o.recipient, value: o.amount });
1052
+ this.checkMax(built.fee, this._config.transferMaxFee, "transfer");
1053
+ const txid = await client.broadcast(built.tx.hex);
1054
+ return { hash: txid || built.tx.id, fee: built.fee };
1055
+ }
1056
+ /** The same address as a {@link WalletAccountReadOnlyBtc}. */
1057
+ toReadOnlyAccount() {
1058
+ this._readOnly ??= new WalletAccountReadOnlyBtc(this.address, this._config);
1059
+ return this._readOnly;
1060
+ }
1061
+ dispose() {
1062
+ this._disposed = true;
1063
+ this.signer.dispose();
1064
+ }
1065
+ };
1066
+
1067
+ // src/manager.ts
1068
+ var import_core9 = require("@anywalletkit/core");
1069
+ var WalletManagerBtc = class extends import_core9.WalletManager {
1070
+ constructor(seedOrSigner, config = {}) {
1071
+ super(seedOrSigner, config);
1072
+ }
1073
+ /**
1074
+ * `m/84'/0'` for p2wpkh or `m/86'/0'` for p2tr on mainnet (coin type 1 on test networks); account
1075
+ * `i` is at `basePath/0'/0/i`.
1076
+ */
1077
+ get basePath() {
1078
+ return `m/${purposeFor(this._config.addressType ?? "p2wpkh")}'/${coinType(this._config.network ?? "bitcoin")}'`;
1079
+ }
1080
+ async getAccount(index = 0) {
1081
+ return this.getAccountByPath(`${this.basePath}/0'/0/${index}`);
1082
+ }
1083
+ async getAccountByPath(path) {
1084
+ const full = path.startsWith("m/") ? path : `${this.basePath}/${path}`;
1085
+ const cached = this._accounts.get(full);
1086
+ if (cached) return cached;
1087
+ const external = this.externalSigner();
1088
+ if (external) return this.fromSigner(external, full);
1089
+ const signer = import_core9.LocalSigner.leafFromSeed(this.seedBytes(), "secp256k1", full);
1090
+ const account = new WalletAccountBtc(signer, signer.publicKey, full, this._config);
1091
+ this._accounts.set(full, account);
1092
+ return account;
1093
+ }
1094
+ /** Resolve `path` against an injected signer: the signer itself, or a child when it can derive there. */
1095
+ async fromSigner(signer, path) {
1096
+ this.requireSigner("secp256k1");
1097
+ let leaf = signer;
1098
+ let key = "external";
1099
+ if ((0, import_core9.isSeedSigner)(signer)) {
1100
+ if (path === signer.path) key = path;
1101
+ else if (path.startsWith(`${signer.path}/`)) {
1102
+ leaf = await (signer.deriveLeaf ?? signer.derive).call(signer, path.slice(signer.path.length + 1));
1103
+ key = path;
1104
+ } else throw new import_core9.SignerError(`The injected signer sits at ${signer.path}; it cannot provide ${path}.`);
1105
+ } else if (path !== `${this.basePath}/0'/0/0` && path !== "external") {
1106
+ throw new import_core9.SignerError(`An external signer has one account; ${path} is not derivable from it.`);
1107
+ }
1108
+ const cached = this._accounts.get(key);
1109
+ if (cached) return cached;
1110
+ const account = new WalletAccountBtc(leaf, await leaf.getPublicKey(), key, this._config);
1111
+ this._accounts.set(key, account);
1112
+ return account;
1113
+ }
1114
+ /** A read-only account for any address, with this manager's config. */
1115
+ getReadOnlyAccount(address) {
1116
+ return new WalletAccountReadOnlyBtc(address, this._config);
1117
+ }
1118
+ /** sat/vB from the client's estimator: fast = 1 block, normal = 3, slow = 6. */
1119
+ async getFeeRates() {
1120
+ const probe = new WalletAccountReadOnlyBtc(
1121
+ (this._config.network ?? "bitcoin") === "bitcoin" ? "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu" : "tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl",
1122
+ this._config
1123
+ );
1124
+ const client = probe.requireClient("get fee rates");
1125
+ const [fast, normal, slow] = await Promise.all([1, 3, 6].map((b) => client.estimateFee(b)));
1126
+ const big2 = (n) => BigInt(Math.max(Math.ceil(n), 1));
1127
+ return { slow: big2(slow), normal: big2(normal), fast: big2(fast) };
1128
+ }
1129
+ };
1130
+
1131
+ // src/history.ts
1132
+ var HistoryUnavailableError = class extends Error {
1133
+ chain;
1134
+ reason;
1135
+ attempts;
1136
+ constructor(chain, reason, attempts = []) {
1137
+ super(`History unavailable for ${chain}: ${reason}`);
1138
+ this.name = "HistoryUnavailableError";
1139
+ this.chain = chain;
1140
+ this.reason = reason;
1141
+ this.attempts = attempts;
1142
+ }
1143
+ };
1144
+ var DEFAULT_LIMIT = 25;
1145
+ var MAX_PAGE_SIZE = 100;
1146
+ var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1147
+ function b64encode(s) {
1148
+ let out = "";
1149
+ for (let i = 0; i < s.length; i += 3) {
1150
+ const c = [s.charCodeAt(i), s.charCodeAt(i + 1), s.charCodeAt(i + 2)];
1151
+ const n = c[0] << 16 | (c[1] || 0) << 8 | (c[2] || 0);
1152
+ out += B64[n >> 18 & 63] + B64[n >> 12 & 63] + (Number.isNaN(c[1]) ? "=" : B64[n >> 6 & 63]) + (Number.isNaN(c[2]) ? "=" : B64[n & 63]);
1153
+ }
1154
+ return out;
1155
+ }
1156
+ function b64decode(s) {
1157
+ const clean = s.replace(/[^A-Za-z0-9+/]/g, "");
1158
+ let out = "";
1159
+ for (let i = 0; i < clean.length; i += 4) {
1160
+ const c = [0, 1, 2, 3].map((k) => i + k < clean.length ? B64.indexOf(clean[i + k]) : -1);
1161
+ if (c[0] < 0 || c[1] < 0) throw new Error("bad base64");
1162
+ const n = c[0] << 18 | c[1] << 12 | (c[2] < 0 ? 0 : c[2]) << 6 | (c[3] < 0 ? 0 : c[3]);
1163
+ out += String.fromCharCode(n >> 16 & 255);
1164
+ if (c[2] >= 0) out += String.fromCharCode(n >> 8 & 255);
1165
+ if (c[3] >= 0) out += String.fromCharCode(n & 255);
1166
+ }
1167
+ return out;
1168
+ }
1169
+ function encodeCursor(c) {
1170
+ return b64encode(JSON.stringify(c));
1171
+ }
1172
+ function decodeCursor(s) {
1173
+ let parsed;
1174
+ try {
1175
+ parsed = JSON.parse(b64decode(s));
1176
+ } catch {
1177
+ throw new Error("@anywalletkit/wallet-btc: malformed history cursor");
1178
+ }
1179
+ const c = parsed;
1180
+ if (!c || c.v !== 1 || !Number.isInteger(c.p) || c.p < 1 || !Number.isInteger(c.s) || c.s < 1) throw new Error("@anywalletkit/wallet-btc: malformed history cursor");
1181
+ return { v: 1, p: c.p, s: c.s };
1182
+ }
1183
+ var big = (v) => typeof v === "string" && /^-?\d+$/.test(v) ? BigInt(v) : typeof v === "number" ? BigInt(Math.trunc(v)) : 0n;
1184
+ function mapBlockbookTx(chain, address, tx) {
1185
+ const owns = (e) => e.isAddress !== false && (e.addresses ?? []).includes(address);
1186
+ let ownIn = 0n, ownOut = 0n, sentToOthers = 0n;
1187
+ for (const vin of tx.vin ?? []) if (owns(vin)) ownIn += big(vin.value);
1188
+ for (const vout of tx.vout ?? []) {
1189
+ if (owns(vout)) ownOut += big(vout.value);
1190
+ else sentToOthers += big(vout.value);
1191
+ }
1192
+ const firstInput = (tx.vin ?? []).find((v) => v.isAddress !== false && v.addresses?.[0])?.addresses?.[0];
1193
+ const firstOtherOutput = (tx.vout ?? []).find((v) => v.isAddress !== false && !owns(v) && v.addresses?.[0])?.addresses?.[0];
1194
+ const transfers = [];
1195
+ let from, to, nativeValue;
1196
+ if (ownIn > 0n) {
1197
+ from = address;
1198
+ to = firstOtherOutput ?? address;
1199
+ nativeValue = firstOtherOutput ? sentToOthers : ownOut;
1200
+ if (nativeValue > 0n) transfers.push({ token: null, from: address, to, amount: nativeValue });
1201
+ } else {
1202
+ from = firstInput ?? "coinbase";
1203
+ to = address;
1204
+ nativeValue = ownOut;
1205
+ if (nativeValue > 0n) transfers.push({ token: null, from, to: address, amount: nativeValue });
1206
+ }
1207
+ const confirmed = tx.confirmations > 0 && tx.blockHeight > 0;
1208
+ const out = {
1209
+ chain,
1210
+ hash: tx.txid,
1211
+ time: tx.blockTime ?? 0,
1212
+ status: confirmed ? "confirmed" : "pending",
1213
+ address,
1214
+ from,
1215
+ to,
1216
+ nativeValue,
1217
+ transfers,
1218
+ raw: { tx, ownIn, ownOut }
1219
+ };
1220
+ if (tx.fees !== void 0) out.fee = big(tx.fees);
1221
+ if (confirmed) out.block = BigInt(tx.blockHeight);
1222
+ return out;
1223
+ }
1224
+ function btcExplorerTxUrl(hash, network = "bitcoin") {
1225
+ return `https://mempool.space/${network === "testnet" ? "testnet/" : ""}tx/${hash}`;
1226
+ }
1227
+ function createBtcHistory(options = {}) {
1228
+ const network = options.network ?? "bitcoin";
1229
+ const chain = CAIP2_GENESIS[network];
1230
+ const urls = [...options.blockbookUrls ?? [], ...options.blockbookUrl ? [options.blockbookUrl] : []];
1231
+ if (urls.length === 0) throw new Error("@anywalletkit/wallet-btc: createBtcHistory needs blockbookUrl or blockbookUrls");
1232
+ const fetchFn = options.fetch ?? globalThis.fetch;
1233
+ if (typeof fetchFn !== "function") throw new Error("@anywalletkit/wallet-btc: no global fetch; pass `fetch` in the options");
1234
+ const clients = urls.map((url) => ({ url, client: new BlockbookClient({ url }, fetchFn, options.timeoutMs ?? DEFAULT_TIMEOUT_MS) }));
1235
+ async function failover(run) {
1236
+ const attempts = [];
1237
+ for (const { url, client } of clients) {
1238
+ try {
1239
+ return await run(client);
1240
+ } catch (e) {
1241
+ attempts.push(`${url}: ${e instanceof Error ? e.message : String(e)}`);
1242
+ }
1243
+ }
1244
+ throw new HistoryUnavailableError(chain, attempts[attempts.length - 1] ?? "every Blockbook failed", attempts);
1245
+ }
1246
+ return {
1247
+ chain,
1248
+ async listActivity(address, query = {}) {
1249
+ const cursor = query.cursor ? decodeCursor(query.cursor) : void 0;
1250
+ const pageSize = cursor?.s ?? Math.min(MAX_PAGE_SIZE, Math.max(1, Math.floor(options.pageSize ?? query.limit ?? DEFAULT_LIMIT)));
1251
+ const pageNo = cursor?.p ?? 1;
1252
+ const data = await failover((c) => c.getAddressPage(address, pageNo, pageSize));
1253
+ let items = (data.transactions ?? []).map((tx) => mapBlockbookTx(chain, address, tx));
1254
+ items.sort((a, b) => (b.block === void 0 ? 1 : a.block === void 0 ? -1 : Number(b.block - a.block)) || b.time - a.time);
1255
+ const totalPages = data.totalPages ?? 1;
1256
+ let more = pageNo < totalPages;
1257
+ if (query.since !== void 0) {
1258
+ const since = query.since;
1259
+ if (items.some((t) => t.status === "confirmed" && t.time < since)) more = false;
1260
+ items = items.filter((t) => t.status !== "confirmed" || t.time >= since);
1261
+ }
1262
+ const page = { items, page: pageNo, totalPages };
1263
+ if (more) page.cursor = encodeCursor({ v: 1, p: pageNo + 1, s: pageSize });
1264
+ return page;
1265
+ },
1266
+ async getTransaction(hash, address) {
1267
+ const tx = await failover((c) => c.getTransactionDetails(hash));
1268
+ return tx ? mapBlockbookTx(chain, address, tx) : void 0;
1269
+ },
1270
+ explorerTxUrl(hash) {
1271
+ return btcExplorerTxUrl(hash, network);
1272
+ }
1273
+ };
1274
+ }
1275
+ //# sourceMappingURL=index.cjs.map