@anywalletkit/wallet-btc 0.2.4 → 0.3.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/dist/index.d.cts CHANGED
@@ -30,6 +30,24 @@ declare class TooManyInputsError extends Error {
30
30
  declare class UnsupportedAddressTypeError extends Error {
31
31
  constructor(message: string);
32
32
  }
33
+ /**
34
+ * The transaction was handed to the network but no definitive answer came back (no response, a
35
+ * timeout, a 5xx, an unreadable body): it MAY be in the mempool. Do not build another spend of the
36
+ * same coins — watch `hash`, the txid computed before sending (round 3, M12).
37
+ */
38
+ declare class BroadcastUnknownError extends Error {
39
+ /** Always true: the transaction may be on its way. */
40
+ readonly maybeSent = true;
41
+ /** The transaction id (hex). */
42
+ readonly hash: string;
43
+ constructor(hash: string, cause: unknown);
44
+ }
45
+
46
+ /**
47
+ * An endpoint answered with a refusal (HTTP 4xx other than 408, or an error body): nothing was
48
+ * taken. A thrown fetch, a timeout, a 5xx or a body that cannot be read may have been.
49
+ */
50
+ declare function isAnsweredRefusal(e: unknown): boolean;
33
51
 
34
52
  /**
35
53
  * Esplora REST transport — the API served by mempool.space (`https://mempool.space/api`)
@@ -335,13 +353,13 @@ declare class WalletAccountBtc extends WalletAccountReadOnlyBtc implements IWall
335
353
  */
336
354
  private signInputs;
337
355
  private ctx;
338
- private buildAndSign;
356
+ private signPlan;
339
357
  /**
340
358
  * Plan → sign → measure. The plan's fee assumes 72-byte DER signatures; if the
341
359
  * signed vsize somehow needs more, the difference comes out of change (never the
342
360
  * recipient) and the tx is re-signed once.
343
361
  */
344
- protected buildSignedTransaction(spend: BtcSpend): Promise<Built>;
362
+ protected signSpend(spend: BtcSpend): Promise<Built>;
345
363
  private checkMax;
346
364
  /**
347
365
  * BIP-143 (p2wpkh) commits each input's signature to that input's CLAIMED amount
@@ -367,6 +385,12 @@ declare class WalletAccountBtc extends WalletAccountReadOnlyBtc implements IWall
367
385
  sendTransaction(tx: BtcTransaction, options?: WriteOptions): Promise<TransferResult>;
368
386
  /** Native BTC only (`token` is rejected). Subject to `transferMaxFee`. */
369
387
  transfer(o: BtcTransferOptions, writeOptions?: WriteOptions): Promise<TransferResult>;
388
+ /**
389
+ * Send once through the client. Only a node's refusal means nothing went out; "already in the
390
+ * mempool / block chain" means an earlier attempt delivered it; anything else may have landed and
391
+ * is a {@link BroadcastUnknownError} with the txid computed before sending (round 3, M12).
392
+ */
393
+ private broadcast;
370
394
  /** The same address as a {@link WalletAccountReadOnlyBtc}. */
371
395
  toReadOnlyAccount(): IWalletAccountReadOnly;
372
396
  dispose(): void;
@@ -402,4 +426,4 @@ declare class WalletManagerBtc extends WalletManager<WalletAccountBtc, BtcConfig
402
426
  getFeeRates(): Promise<FeeRates>;
403
427
  }
404
428
 
405
- export { BlockbookTx, BtcAddressType, BtcClient, BtcConfig, BtcMaxSpendableResult, BtcNetwork, BtcReceipt, BtcSpend, BtcTransaction, BtcTransferOptions, BtcTxDescription, type CoinSelectOptions, type CoinSelection, DEFAULT_MAX_FEE_RATE, DUST_LIMIT_P2TR, DUST_LIMIT_P2WPKH, type DecodedAddress, type DescribeContext, EsploraClient, FINAL_SEQUENCE, INPUT_WEIGHT, MAX_UTXO_INPUTS, RBF_SEQUENCE, REGTEST_NETWORK, type ScriptKind, type SegwitType, type SelectionTarget, type SpendPlan, TooManyInputsError, type UnsafeTransactionCode, UnsafeTransactionError, UnsupportedAddressTypeError, Utxo, WalletAccountBtc, WalletAccountReadOnlyBtc, WalletManagerBtc, addressFor, addressFromPublicKey, addressFromScript, assertSafe, coinType, createClient, decodeAddress, WalletManagerBtc as default, describeTransaction, dustLimit, encodeMessageSignature, estimateVsize, magicHash, networkParams, outputVBytes, parseRawTransaction, purposeFor, recoverMessagePublicKey, scriptKind, selectAll, selectCoins, signMessage, signMessageWith, spendableUtxos };
429
+ export { BlockbookTx, BroadcastUnknownError, BtcAddressType, BtcClient, BtcConfig, BtcMaxSpendableResult, BtcNetwork, BtcReceipt, BtcSpend, BtcTransaction, BtcTransferOptions, BtcTxDescription, type CoinSelectOptions, type CoinSelection, DEFAULT_MAX_FEE_RATE, DUST_LIMIT_P2TR, DUST_LIMIT_P2WPKH, type DecodedAddress, type DescribeContext, EsploraClient, FINAL_SEQUENCE, INPUT_WEIGHT, MAX_UTXO_INPUTS, RBF_SEQUENCE, REGTEST_NETWORK, type ScriptKind, type SegwitType, type SelectionTarget, type SpendPlan, TooManyInputsError, type UnsafeTransactionCode, UnsafeTransactionError, UnsupportedAddressTypeError, Utxo, WalletAccountBtc, WalletAccountReadOnlyBtc, WalletManagerBtc, addressFor, addressFromPublicKey, addressFromScript, assertSafe, coinType, createClient, decodeAddress, WalletManagerBtc as default, describeTransaction, dustLimit, encodeMessageSignature, estimateVsize, isAnsweredRefusal, magicHash, networkParams, outputVBytes, parseRawTransaction, purposeFor, recoverMessagePublicKey, scriptKind, selectAll, selectCoins, signMessage, signMessageWith, spendableUtxos };
package/dist/index.d.ts CHANGED
@@ -30,6 +30,24 @@ declare class TooManyInputsError extends Error {
30
30
  declare class UnsupportedAddressTypeError extends Error {
31
31
  constructor(message: string);
32
32
  }
33
+ /**
34
+ * The transaction was handed to the network but no definitive answer came back (no response, a
35
+ * timeout, a 5xx, an unreadable body): it MAY be in the mempool. Do not build another spend of the
36
+ * same coins — watch `hash`, the txid computed before sending (round 3, M12).
37
+ */
38
+ declare class BroadcastUnknownError extends Error {
39
+ /** Always true: the transaction may be on its way. */
40
+ readonly maybeSent = true;
41
+ /** The transaction id (hex). */
42
+ readonly hash: string;
43
+ constructor(hash: string, cause: unknown);
44
+ }
45
+
46
+ /**
47
+ * An endpoint answered with a refusal (HTTP 4xx other than 408, or an error body): nothing was
48
+ * taken. A thrown fetch, a timeout, a 5xx or a body that cannot be read may have been.
49
+ */
50
+ declare function isAnsweredRefusal(e: unknown): boolean;
33
51
 
34
52
  /**
35
53
  * Esplora REST transport — the API served by mempool.space (`https://mempool.space/api`)
@@ -335,13 +353,13 @@ declare class WalletAccountBtc extends WalletAccountReadOnlyBtc implements IWall
335
353
  */
336
354
  private signInputs;
337
355
  private ctx;
338
- private buildAndSign;
356
+ private signPlan;
339
357
  /**
340
358
  * Plan → sign → measure. The plan's fee assumes 72-byte DER signatures; if the
341
359
  * signed vsize somehow needs more, the difference comes out of change (never the
342
360
  * recipient) and the tx is re-signed once.
343
361
  */
344
- protected buildSignedTransaction(spend: BtcSpend): Promise<Built>;
362
+ protected signSpend(spend: BtcSpend): Promise<Built>;
345
363
  private checkMax;
346
364
  /**
347
365
  * BIP-143 (p2wpkh) commits each input's signature to that input's CLAIMED amount
@@ -367,6 +385,12 @@ declare class WalletAccountBtc extends WalletAccountReadOnlyBtc implements IWall
367
385
  sendTransaction(tx: BtcTransaction, options?: WriteOptions): Promise<TransferResult>;
368
386
  /** Native BTC only (`token` is rejected). Subject to `transferMaxFee`. */
369
387
  transfer(o: BtcTransferOptions, writeOptions?: WriteOptions): Promise<TransferResult>;
388
+ /**
389
+ * Send once through the client. Only a node's refusal means nothing went out; "already in the
390
+ * mempool / block chain" means an earlier attempt delivered it; anything else may have landed and
391
+ * is a {@link BroadcastUnknownError} with the txid computed before sending (round 3, M12).
392
+ */
393
+ private broadcast;
370
394
  /** The same address as a {@link WalletAccountReadOnlyBtc}. */
371
395
  toReadOnlyAccount(): IWalletAccountReadOnly;
372
396
  dispose(): void;
@@ -402,4 +426,4 @@ declare class WalletManagerBtc extends WalletManager<WalletAccountBtc, BtcConfig
402
426
  getFeeRates(): Promise<FeeRates>;
403
427
  }
404
428
 
405
- export { BlockbookTx, BtcAddressType, BtcClient, BtcConfig, BtcMaxSpendableResult, BtcNetwork, BtcReceipt, BtcSpend, BtcTransaction, BtcTransferOptions, BtcTxDescription, type CoinSelectOptions, type CoinSelection, DEFAULT_MAX_FEE_RATE, DUST_LIMIT_P2TR, DUST_LIMIT_P2WPKH, type DecodedAddress, type DescribeContext, EsploraClient, FINAL_SEQUENCE, INPUT_WEIGHT, MAX_UTXO_INPUTS, RBF_SEQUENCE, REGTEST_NETWORK, type ScriptKind, type SegwitType, type SelectionTarget, type SpendPlan, TooManyInputsError, type UnsafeTransactionCode, UnsafeTransactionError, UnsupportedAddressTypeError, Utxo, WalletAccountBtc, WalletAccountReadOnlyBtc, WalletManagerBtc, addressFor, addressFromPublicKey, addressFromScript, assertSafe, coinType, createClient, decodeAddress, WalletManagerBtc as default, describeTransaction, dustLimit, encodeMessageSignature, estimateVsize, magicHash, networkParams, outputVBytes, parseRawTransaction, purposeFor, recoverMessagePublicKey, scriptKind, selectAll, selectCoins, signMessage, signMessageWith, spendableUtxos };
429
+ export { BlockbookTx, BroadcastUnknownError, BtcAddressType, BtcClient, BtcConfig, BtcMaxSpendableResult, BtcNetwork, BtcReceipt, BtcSpend, BtcTransaction, BtcTransferOptions, BtcTxDescription, type CoinSelectOptions, type CoinSelection, DEFAULT_MAX_FEE_RATE, DUST_LIMIT_P2TR, DUST_LIMIT_P2WPKH, type DecodedAddress, type DescribeContext, EsploraClient, FINAL_SEQUENCE, INPUT_WEIGHT, MAX_UTXO_INPUTS, RBF_SEQUENCE, REGTEST_NETWORK, type ScriptKind, type SegwitType, type SelectionTarget, type SpendPlan, TooManyInputsError, type UnsafeTransactionCode, UnsafeTransactionError, UnsupportedAddressTypeError, Utxo, WalletAccountBtc, WalletAccountReadOnlyBtc, WalletManagerBtc, addressFor, addressFromPublicKey, addressFromScript, assertSafe, coinType, createClient, decodeAddress, WalletManagerBtc as default, describeTransaction, dustLimit, encodeMessageSignature, estimateVsize, isAnsweredRefusal, magicHash, networkParams, outputVBytes, parseRawTransaction, purposeFor, recoverMessagePublicKey, scriptKind, selectAll, selectCoins, signMessage, signMessageWith, spendableUtxos };
package/dist/index.js CHANGED
@@ -8,11 +8,15 @@ import {
8
8
  decodeCursor,
9
9
  encodeCursor,
10
10
  fetchWithTimeout,
11
+ isAlreadyKnown,
12
+ isAnsweredRefusal,
11
13
  isHex64,
12
14
  isRecord,
13
15
  mapBlockbookTx,
16
+ refusal,
17
+ refusedStatus,
14
18
  unconfirmedOutgoing
15
- } from "./chunk-4WUAN3DC.js";
19
+ } from "./chunk-TZNOUBSP.js";
16
20
 
17
21
  // src/errors.ts
18
22
  import { InsufficientFundsError, NetworkError, SignerError } from "@anywalletkit/core";
@@ -38,6 +42,17 @@ var UnsupportedAddressTypeError = class extends Error {
38
42
  this.name = "UnsupportedAddressTypeError";
39
43
  }
40
44
  };
45
+ var BroadcastUnknownError = class extends Error {
46
+ /** Always true: the transaction may be on its way. */
47
+ maybeSent = true;
48
+ /** The transaction id (hex). */
49
+ hash;
50
+ constructor(hash, cause) {
51
+ super(`Broadcast outcome unknown for ${hash}: ${cause instanceof Error ? cause.message.slice(0, 200) : String(cause).slice(0, 200)}`, { cause });
52
+ this.name = "BroadcastUnknownError";
53
+ this.hash = hash;
54
+ }
55
+ };
41
56
 
42
57
  // src/esplora.ts
43
58
  import { NetworkError as NetworkError2 } from "@anywalletkit/core";
@@ -60,7 +75,11 @@ var EsploraClient = class {
60
75
  async request(path, init) {
61
76
  const url = `${this.base}${path}`;
62
77
  const res = await fetchWithTimeout(this.fetchFn, url, init, this.timeoutMs, this.base);
63
- if (!res.ok) throw new NetworkError2(`Esplora request failed: ${res.status} ${res.statusText} (${url})`, { provider: this.base });
78
+ if (!res.ok) {
79
+ const reason = refusedStatus(res.status) ? (await res.text().catch(() => "")).slice(0, 200) : "";
80
+ const err = new NetworkError2(`Esplora request failed: ${res.status} ${res.statusText} (${url})${reason ? `: ${reason}` : ""}`, { provider: this.base });
81
+ throw refusedStatus(res.status) ? refusal(err) : err;
82
+ }
64
83
  return res;
65
84
  }
66
85
  async json(path, check) {
@@ -165,6 +184,7 @@ function createClient(config) {
165
184
  const attempts = list.length + (config.retries ?? 3);
166
185
  const call = (method) => async (...args) => {
167
186
  let last;
187
+ let answered = true;
168
188
  for (let i = 0; i < attempts; i++) {
169
189
  const c = list[i % list.length];
170
190
  const fn = c[method];
@@ -173,9 +193,11 @@ function createClient(config) {
173
193
  return await fn.apply(c, args);
174
194
  } catch (e) {
175
195
  last = e;
196
+ answered &&= isAnsweredRefusal(e);
176
197
  }
177
198
  }
178
- throw new NetworkError3(`All BTC clients failed for ${method}`, { cause: last });
199
+ const err = new NetworkError3(`All BTC clients failed for ${method}`, { cause: last });
200
+ throw answered && last !== void 0 ? refusal(err) : err;
179
201
  };
180
202
  const out = {};
181
203
  for (const m of METHODS) out[m] = call(m);
@@ -396,11 +418,28 @@ function describeTransaction(tx, inputValues, ctx) {
396
418
  });
397
419
  const outputs = [];
398
420
  const warnings = /* @__PURE__ */ new Set();
421
+ const scriptOf = (address) => {
422
+ try {
423
+ return hex.encode(decodeAddress(address, ctx.network).script);
424
+ } catch {
425
+ return void 0;
426
+ }
427
+ };
428
+ const ownScript = scriptOf(ctx.ownAddress);
429
+ const intendedScript = ctx.intended ? scriptOf(ctx.intended.to) : void 0;
430
+ let recipientSeen = false;
399
431
  for (let i = 0; i < tx.outputsLength; i++) {
400
432
  const out = tx.getOutput(i);
401
433
  const value = out.amount ?? 0n;
402
434
  const address = tx.getOutputAddress(i, ctx.network);
403
- 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";
435
+ const script = out.script ? hex.encode(out.script) : void 0;
436
+ const mine = script !== void 0 && script === ownScript;
437
+ let role;
438
+ if (ctx.intended === void 0) role = mine ? "change" : "recipient";
439
+ else if (!recipientSeen && script !== void 0 && script === intendedScript && value === ctx.intended.value) {
440
+ role = "recipient";
441
+ recipientSeen = true;
442
+ } else role = mine ? "change" : "unknown";
404
443
  if (out.script && value < dustLimit(scriptKind(out.script))) warnings.add("dust-output");
405
444
  outputs.push({ address, value, role });
406
445
  }
@@ -687,7 +726,7 @@ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc
687
726
  ctx(intended) {
688
727
  return { network: this._network, ownAddress: this.address, intended, maxFeeRate: this.maxFeeRate };
689
728
  }
690
- async buildAndSign(plan, to, recipientValue, change, lockTime, rbf) {
729
+ async signPlan(plan, to, recipientValue, change, lockTime, rbf) {
691
730
  const spend = spendInfo(this.publicKey, this._network, this.addressType);
692
731
  const tx = new Transaction2({ lockTime });
693
732
  const sequence = rbf ? RBF_SEQUENCE : FINAL_SEQUENCE;
@@ -705,7 +744,7 @@ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc
705
744
  * signed vsize somehow needs more, the difference comes out of change (never the
706
745
  * recipient) and the tx is re-signed once.
707
746
  */
708
- async buildSignedTransaction(spend) {
747
+ async signSpend(spend) {
709
748
  this.live();
710
749
  const plan = await this.planSpend(spend);
711
750
  await this.verifyPrevouts(plan.utxos);
@@ -713,7 +752,7 @@ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc
713
752
  const rbf = spend.rbf ?? this._config.rbf ?? true;
714
753
  let change = plan.change;
715
754
  let fee = plan.fee;
716
- let tx = await this.buildAndSign(plan, spend.to, plan.amount, change, lockTime, rbf);
755
+ let tx = await this.signPlan(plan, spend.to, plan.amount, change, lockTime, rbf);
717
756
  const required = BigInt(tx.vsize) * plan.feeRate;
718
757
  if (required > fee) {
719
758
  const delta = required - fee;
@@ -726,7 +765,7 @@ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc
726
765
  fee = plan.fee + plan.change;
727
766
  change = 0n;
728
767
  } else fee = required;
729
- tx = await this.buildAndSign(plan, spend.to, plan.amount, change, lockTime, rbf);
768
+ tx = await this.signPlan(plan, spend.to, plan.amount, change, lockTime, rbf);
730
769
  if (BigInt(tx.vsize) * plan.feeRate > fee) throw new Error("Fee shortfall after output rebalance.");
731
770
  }
732
771
  const ctx = this.ctx({ to: spend.to, value: plan.amount });
@@ -797,7 +836,7 @@ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc
797
836
  /** "What you sign": the fully resolved spend (signed, NOT broadcast) or a caller's raw hex. */
798
837
  async describeTransaction(tx) {
799
838
  if (typeof tx === "string") return (await this.describeRaw(tx)).desc;
800
- return (await this.buildSignedTransaction(tx)).desc;
839
+ return (await this.signSpend(tx)).desc;
801
840
  }
802
841
  /** Sign without broadcasting. Returns the raw transaction hex. Raw hex input gets the checks `sendTransaction` runs (prevouts through the client) and is returned unchanged, never re-signed. */
803
842
  async signTransaction(tx, options = {}) {
@@ -807,7 +846,7 @@ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc
807
846
  this.checkMax(desc.fee, feeLimit(this._config.transactionMaxFee, options), "transaction");
808
847
  return tx;
809
848
  }
810
- const { tx: built, fee } = await this.buildSignedTransaction(tx);
849
+ const { tx: built, fee } = await this.signSpend(tx);
811
850
  this.checkMax(fee, feeLimit(this._config.transactionMaxFee, options), "transaction");
812
851
  return built.hex;
813
852
  }
@@ -825,24 +864,36 @@ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc
825
864
  hash = parsed.id;
826
865
  fee = desc.fee;
827
866
  } else {
828
- const built = await this.buildSignedTransaction(tx);
867
+ const built = await this.signSpend(tx);
829
868
  rawHex = built.tx.hex;
830
869
  hash = built.tx.id;
831
870
  fee = built.fee;
832
871
  }
833
872
  this.checkMax(fee, feeLimit(this._config.transactionMaxFee, options), "transaction");
834
- const txid = await client.broadcast(rawHex);
835
- return { hash: txid || hash, fee };
873
+ return { hash: await this.broadcast(client, rawHex, hash), fee };
836
874
  }
837
875
  /** Native BTC only (`token` is rejected). Subject to `transferMaxFee`. */
838
876
  async transfer(o, writeOptions = {}) {
839
877
  if (o.token) throw new Error("Tokens are not supported on the bitcoin blockchain.");
840
878
  this.live();
841
879
  const client = this.requireClient("transfer");
842
- const built = await this.buildSignedTransaction({ to: o.recipient, value: o.amount });
880
+ const built = await this.signSpend({ to: o.recipient, value: o.amount });
843
881
  this.checkMax(built.fee, feeLimit(this._config.transferMaxFee, writeOptions), "transfer");
844
- const txid = await client.broadcast(built.tx.hex);
845
- return { hash: txid || built.tx.id, fee: built.fee };
882
+ return { hash: await this.broadcast(client, built.tx.hex, built.tx.id), fee: built.fee };
883
+ }
884
+ /**
885
+ * Send once through the client. Only a node's refusal means nothing went out; "already in the
886
+ * mempool / block chain" means an earlier attempt delivered it; anything else may have landed and
887
+ * is a {@link BroadcastUnknownError} with the txid computed before sending (round 3, M12).
888
+ */
889
+ async broadcast(client, rawHex, txid) {
890
+ try {
891
+ return await client.broadcast(rawHex) || txid;
892
+ } catch (e) {
893
+ if (isAlreadyKnown(e)) return txid;
894
+ if (isAnsweredRefusal(e)) throw e;
895
+ throw new BroadcastUnknownError(txid, e);
896
+ }
846
897
  }
847
898
  /** The same address as a {@link WalletAccountReadOnlyBtc}. */
848
899
  toReadOnlyAccount() {
@@ -916,6 +967,7 @@ var WalletManagerBtc = class extends WalletManager {
916
967
  };
917
968
  export {
918
969
  BlockbookClient,
970
+ BroadcastUnknownError,
919
971
  CAIP2_GENESIS,
920
972
  DEFAULT_MAX_FEE_RATE,
921
973
  DUST_LIMIT_P2TR,
@@ -949,6 +1001,7 @@ export {
949
1001
  encodeCursor as encodeHistoryCursor,
950
1002
  encodeMessageSignature,
951
1003
  estimateVsize,
1004
+ isAnsweredRefusal,
952
1005
  magicHash,
953
1006
  mapBlockbookTx,
954
1007
  networkParams,