@anywalletkit/wallet-btc 0.2.5 → 0.3.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.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { B as BtcClient, U as Utxo, a as BlockbookTx, b as BtcConfig, c as BtcTxDescription, d as BtcAddressType, e as BtcNetwork, f as BtcSpend, g as BtcTransferOptions, h as BtcMaxSpendableResult, i as BtcReceipt, j as BtcTransaction } from './history-B5fmsCjZ.cjs';
2
- export { k as BlockbookAddressPage, l as BlockbookClient, m as BlockbookTxDetails, n as BlockbookVin, o as BlockbookVout, p as BtcChainTx, q as BtcClientConfig, r as BtcHistoryCursor, s as BtcHistoryOptions, t as BtcHistoryPage, u as BtcHistoryRaw, v as BtcHistorySource, w as BtcTxInputDescription, x as BtcTxOutputDescription, y as BtcWarning, C as CAIP2_GENESIS, H as HistoryUnavailableError, z as btcExplorerTxUrl, A as createBtcHistory, D as decodeHistoryCursor, E as encodeHistoryCursor, F as mapBlockbookTx, G as unconfirmedOutgoing } from './history-B5fmsCjZ.cjs';
1
+ import { B as BtcClient, U as Utxo, a as BlockbookTx, b as BtcConfig, c as BtcTxDescription, d as BtcAddressType, e as BtcNetwork, f as BtcSpend, g as BtcTransferOptions, h as BtcMaxSpendableResult, i as BtcReceipt, j as BtcTransaction } from './history-CasPAf7l.cjs';
2
+ export { k as BlockbookAddressPage, l as BlockbookAddressTx, m as BlockbookClient, n as BlockbookTxDetails, o as BlockbookVin, p as BlockbookVout, q as BtcChainTx, r as BtcClientConfig, s as BtcHistoryCursor, t as BtcHistoryOptions, u as BtcHistoryPage, v as BtcHistoryRaw, w as BtcHistorySource, x as BtcTxInputDescription, y as BtcTxOutputDescription, z as BtcWarning, C as CAIP2_GENESIS, H as HistoryUnavailableError, A as btcExplorerTxUrl, D as createBtcHistory, E as decodeHistoryCursor, F as encodeHistoryCursor, G as mapBlockbookTx, I as unconfirmedOutgoing } from './history-CasPAf7l.cjs';
3
3
  import { Signer, IWalletAccountReadOnly, IWalletAccount, WriteOptions, TransferResult, WalletManager, FeeRates } from '@anywalletkit/core';
4
4
  import { Transaction } from '@scure/btc-signer';
5
5
  import { BTC_NETWORK } from '@scure/btc-signer/utils.js';
@@ -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`)
@@ -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 Built, 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
@@ -1,5 +1,5 @@
1
- import { B as BtcClient, U as Utxo, a as BlockbookTx, b as BtcConfig, c as BtcTxDescription, d as BtcAddressType, e as BtcNetwork, f as BtcSpend, g as BtcTransferOptions, h as BtcMaxSpendableResult, i as BtcReceipt, j as BtcTransaction } from './history-B5fmsCjZ.js';
2
- export { k as BlockbookAddressPage, l as BlockbookClient, m as BlockbookTxDetails, n as BlockbookVin, o as BlockbookVout, p as BtcChainTx, q as BtcClientConfig, r as BtcHistoryCursor, s as BtcHistoryOptions, t as BtcHistoryPage, u as BtcHistoryRaw, v as BtcHistorySource, w as BtcTxInputDescription, x as BtcTxOutputDescription, y as BtcWarning, C as CAIP2_GENESIS, H as HistoryUnavailableError, z as btcExplorerTxUrl, A as createBtcHistory, D as decodeHistoryCursor, E as encodeHistoryCursor, F as mapBlockbookTx, G as unconfirmedOutgoing } from './history-B5fmsCjZ.js';
1
+ import { B as BtcClient, U as Utxo, a as BlockbookTx, b as BtcConfig, c as BtcTxDescription, d as BtcAddressType, e as BtcNetwork, f as BtcSpend, g as BtcTransferOptions, h as BtcMaxSpendableResult, i as BtcReceipt, j as BtcTransaction } from './history-CasPAf7l.js';
2
+ export { k as BlockbookAddressPage, l as BlockbookAddressTx, m as BlockbookClient, n as BlockbookTxDetails, o as BlockbookVin, p as BlockbookVout, q as BtcChainTx, r as BtcClientConfig, s as BtcHistoryCursor, t as BtcHistoryOptions, u as BtcHistoryPage, v as BtcHistoryRaw, w as BtcHistorySource, x as BtcTxInputDescription, y as BtcTxOutputDescription, z as BtcWarning, C as CAIP2_GENESIS, H as HistoryUnavailableError, A as btcExplorerTxUrl, D as createBtcHistory, E as decodeHistoryCursor, F as encodeHistoryCursor, G as mapBlockbookTx, I as unconfirmedOutgoing } from './history-CasPAf7l.js';
3
3
  import { Signer, IWalletAccountReadOnly, IWalletAccount, WriteOptions, TransferResult, WalletManager, FeeRates } from '@anywalletkit/core';
4
4
  import { Transaction } from '@scure/btc-signer';
5
5
  import { BTC_NETWORK } from '@scure/btc-signer/utils.js';
@@ -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`)
@@ -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 Built, 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-SIR2SRK2.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
  }
@@ -831,8 +870,7 @@ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc
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 = {}) {
@@ -841,8 +879,21 @@ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc
841
879
  const client = this.requireClient("transfer");
842
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,