@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/README.md +8 -0
- package/dist/{chunk-4WUAN3DC.js → chunk-TZNOUBSP.js} +24 -3
- package/dist/chunk-TZNOUBSP.js.map +1 -0
- package/dist/history.cjs +10 -2
- package/dist/history.cjs.map +1 -1
- package/dist/history.js +1 -1
- package/dist/index.cjs +84 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -3
- package/dist/index.d.ts +27 -3
- package/dist/index.js +69 -16
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/dist/chunk-4WUAN3DC.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
BlockbookClient: () => BlockbookClient,
|
|
24
|
+
BroadcastUnknownError: () => BroadcastUnknownError,
|
|
24
25
|
CAIP2_GENESIS: () => CAIP2_GENESIS,
|
|
25
26
|
DEFAULT_MAX_FEE_RATE: () => DEFAULT_MAX_FEE_RATE,
|
|
26
27
|
DUST_LIMIT_P2TR: () => DUST_LIMIT_P2TR,
|
|
@@ -54,6 +55,7 @@ __export(src_exports, {
|
|
|
54
55
|
encodeHistoryCursor: () => encodeCursor,
|
|
55
56
|
encodeMessageSignature: () => encodeMessageSignature,
|
|
56
57
|
estimateVsize: () => estimateVsize,
|
|
58
|
+
isAnsweredRefusal: () => isAnsweredRefusal,
|
|
57
59
|
magicHash: () => magicHash,
|
|
58
60
|
mapBlockbookTx: () => mapBlockbookTx,
|
|
59
61
|
networkParams: () => networkParams,
|
|
@@ -102,9 +104,17 @@ var UnsupportedAddressTypeError = class extends Error {
|
|
|
102
104
|
this.name = "UnsupportedAddressTypeError";
|
|
103
105
|
}
|
|
104
106
|
};
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
var BroadcastUnknownError = class extends Error {
|
|
108
|
+
/** Always true: the transaction may be on its way. */
|
|
109
|
+
maybeSent = true;
|
|
110
|
+
/** The transaction id (hex). */
|
|
111
|
+
hash;
|
|
112
|
+
constructor(hash, cause) {
|
|
113
|
+
super(`Broadcast outcome unknown for ${hash}: ${cause instanceof Error ? cause.message.slice(0, 200) : String(cause).slice(0, 200)}`, { cause });
|
|
114
|
+
this.name = "BroadcastUnknownError";
|
|
115
|
+
this.hash = hash;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
108
118
|
|
|
109
119
|
// src/http.ts
|
|
110
120
|
var import_core2 = require("@anywalletkit/core");
|
|
@@ -117,11 +127,25 @@ async function fetchWithTimeout(fetchFn, url, init, timeoutMs, provider) {
|
|
|
117
127
|
throw new import_core2.NetworkError(`Request failed: ${url}`, { provider, cause: e });
|
|
118
128
|
}
|
|
119
129
|
}
|
|
130
|
+
var refusedStatus = (status) => status >= 400 && status < 500 && status !== 408;
|
|
131
|
+
function refusal(e) {
|
|
132
|
+
return Object.assign(e, { answered: true });
|
|
133
|
+
}
|
|
134
|
+
function isAnsweredRefusal(e) {
|
|
135
|
+
return e?.answered === true;
|
|
136
|
+
}
|
|
137
|
+
function isAlreadyKnown(e) {
|
|
138
|
+
for (let x = e, depth = 0; x instanceof Error && depth < 5; x = x.cause, depth++) {
|
|
139
|
+
if (/txn-already-(in-mempool|known)|already (known|in (the )?(mempool|block ?chain))/i.test(x.message)) return true;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
120
143
|
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
121
144
|
var isHex64 = (v) => typeof v === "string" && /^[0-9a-fA-F]{64}$/.test(v);
|
|
122
145
|
var isIntString = (v) => typeof v === "string" && /^-?\d+$/.test(v);
|
|
123
146
|
|
|
124
147
|
// src/blockbook.ts
|
|
148
|
+
var import_core3 = require("@anywalletkit/core");
|
|
125
149
|
var MEMPOOL_SPACE_URL = "https://mempool.space";
|
|
126
150
|
function unconfirmedOutgoing(transactions, address) {
|
|
127
151
|
const pending = (transactions ?? []).filter((tx) => tx.blockHeight === -1);
|
|
@@ -162,7 +186,11 @@ var BlockbookClient = class _BlockbookClient {
|
|
|
162
186
|
async get(path, check) {
|
|
163
187
|
const url = `${this.base}/api${path}`;
|
|
164
188
|
const res = await fetchWithTimeout(this.fetchFn, url, void 0, this.timeoutMs, this.base);
|
|
165
|
-
if (!res.ok)
|
|
189
|
+
if (!res.ok) {
|
|
190
|
+
const reason = refusedStatus(res.status) ? (await res.text().catch(() => "")).slice(0, 200) : "";
|
|
191
|
+
const err = new import_core3.NetworkError(`Blockbook request failed: ${res.status} ${res.statusText} (${url})${reason ? `: ${reason}` : ""}`, { provider: this.base });
|
|
192
|
+
throw refusedStatus(res.status) ? refusal(err) : err;
|
|
193
|
+
}
|
|
166
194
|
let body;
|
|
167
195
|
try {
|
|
168
196
|
body = await res.json();
|
|
@@ -226,7 +254,7 @@ var BlockbookClient = class _BlockbookClient {
|
|
|
226
254
|
}
|
|
227
255
|
async broadcast(rawTx) {
|
|
228
256
|
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");
|
|
257
|
+
if (data.error) throw refusal(new Error(typeof data.error === "string" ? data.error : data.error.message ?? "broadcast failed"));
|
|
230
258
|
if (!data.result) throw new import_core3.NetworkError("Blockbook returned no txid", { provider: this.base });
|
|
231
259
|
return data.result;
|
|
232
260
|
}
|
|
@@ -274,7 +302,11 @@ var EsploraClient = class {
|
|
|
274
302
|
async request(path, init) {
|
|
275
303
|
const url = `${this.base}${path}`;
|
|
276
304
|
const res = await fetchWithTimeout(this.fetchFn, url, init, this.timeoutMs, this.base);
|
|
277
|
-
if (!res.ok)
|
|
305
|
+
if (!res.ok) {
|
|
306
|
+
const reason = refusedStatus(res.status) ? (await res.text().catch(() => "")).slice(0, 200) : "";
|
|
307
|
+
const err = new import_core4.NetworkError(`Esplora request failed: ${res.status} ${res.statusText} (${url})${reason ? `: ${reason}` : ""}`, { provider: this.base });
|
|
308
|
+
throw refusedStatus(res.status) ? refusal(err) : err;
|
|
309
|
+
}
|
|
278
310
|
return res;
|
|
279
311
|
}
|
|
280
312
|
async json(path, check) {
|
|
@@ -379,6 +411,7 @@ function createClient(config) {
|
|
|
379
411
|
const attempts = list.length + (config.retries ?? 3);
|
|
380
412
|
const call = (method) => async (...args) => {
|
|
381
413
|
let last;
|
|
414
|
+
let answered = true;
|
|
382
415
|
for (let i = 0; i < attempts; i++) {
|
|
383
416
|
const c = list[i % list.length];
|
|
384
417
|
const fn = c[method];
|
|
@@ -387,9 +420,11 @@ function createClient(config) {
|
|
|
387
420
|
return await fn.apply(c, args);
|
|
388
421
|
} catch (e) {
|
|
389
422
|
last = e;
|
|
423
|
+
answered &&= isAnsweredRefusal(e);
|
|
390
424
|
}
|
|
391
425
|
}
|
|
392
|
-
|
|
426
|
+
const err = new import_core5.NetworkError(`All BTC clients failed for ${method}`, { cause: last });
|
|
427
|
+
throw answered && last !== void 0 ? refusal(err) : err;
|
|
393
428
|
};
|
|
394
429
|
const out = {};
|
|
395
430
|
for (const m of METHODS) out[m] = call(m);
|
|
@@ -610,11 +645,28 @@ function describeTransaction(tx, inputValues, ctx) {
|
|
|
610
645
|
});
|
|
611
646
|
const outputs = [];
|
|
612
647
|
const warnings = /* @__PURE__ */ new Set();
|
|
648
|
+
const scriptOf = (address) => {
|
|
649
|
+
try {
|
|
650
|
+
return import_base2.hex.encode(decodeAddress(address, ctx.network).script);
|
|
651
|
+
} catch {
|
|
652
|
+
return void 0;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
const ownScript = scriptOf(ctx.ownAddress);
|
|
656
|
+
const intendedScript = ctx.intended ? scriptOf(ctx.intended.to) : void 0;
|
|
657
|
+
let recipientSeen = false;
|
|
613
658
|
for (let i = 0; i < tx.outputsLength; i++) {
|
|
614
659
|
const out = tx.getOutput(i);
|
|
615
660
|
const value = out.amount ?? 0n;
|
|
616
661
|
const address = tx.getOutputAddress(i, ctx.network);
|
|
617
|
-
const
|
|
662
|
+
const script = out.script ? import_base2.hex.encode(out.script) : void 0;
|
|
663
|
+
const mine = script !== void 0 && script === ownScript;
|
|
664
|
+
let role;
|
|
665
|
+
if (ctx.intended === void 0) role = mine ? "change" : "recipient";
|
|
666
|
+
else if (!recipientSeen && script !== void 0 && script === intendedScript && value === ctx.intended.value) {
|
|
667
|
+
role = "recipient";
|
|
668
|
+
recipientSeen = true;
|
|
669
|
+
} else role = mine ? "change" : "unknown";
|
|
618
670
|
if (out.script && value < dustLimit(scriptKind(out.script))) warnings.add("dust-output");
|
|
619
671
|
outputs.push({ address, value, role });
|
|
620
672
|
}
|
|
@@ -901,7 +953,7 @@ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc
|
|
|
901
953
|
ctx(intended) {
|
|
902
954
|
return { network: this._network, ownAddress: this.address, intended, maxFeeRate: this.maxFeeRate };
|
|
903
955
|
}
|
|
904
|
-
async
|
|
956
|
+
async signPlan(plan, to, recipientValue, change, lockTime, rbf) {
|
|
905
957
|
const spend = spendInfo(this.publicKey, this._network, this.addressType);
|
|
906
958
|
const tx = new import_btc_signer4.Transaction({ lockTime });
|
|
907
959
|
const sequence = rbf ? RBF_SEQUENCE : FINAL_SEQUENCE;
|
|
@@ -919,7 +971,7 @@ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc
|
|
|
919
971
|
* signed vsize somehow needs more, the difference comes out of change (never the
|
|
920
972
|
* recipient) and the tx is re-signed once.
|
|
921
973
|
*/
|
|
922
|
-
async
|
|
974
|
+
async signSpend(spend) {
|
|
923
975
|
this.live();
|
|
924
976
|
const plan = await this.planSpend(spend);
|
|
925
977
|
await this.verifyPrevouts(plan.utxos);
|
|
@@ -927,7 +979,7 @@ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc
|
|
|
927
979
|
const rbf = spend.rbf ?? this._config.rbf ?? true;
|
|
928
980
|
let change = plan.change;
|
|
929
981
|
let fee = plan.fee;
|
|
930
|
-
let tx = await this.
|
|
982
|
+
let tx = await this.signPlan(plan, spend.to, plan.amount, change, lockTime, rbf);
|
|
931
983
|
const required = BigInt(tx.vsize) * plan.feeRate;
|
|
932
984
|
if (required > fee) {
|
|
933
985
|
const delta = required - fee;
|
|
@@ -940,7 +992,7 @@ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc
|
|
|
940
992
|
fee = plan.fee + plan.change;
|
|
941
993
|
change = 0n;
|
|
942
994
|
} else fee = required;
|
|
943
|
-
tx = await this.
|
|
995
|
+
tx = await this.signPlan(plan, spend.to, plan.amount, change, lockTime, rbf);
|
|
944
996
|
if (BigInt(tx.vsize) * plan.feeRate > fee) throw new Error("Fee shortfall after output rebalance.");
|
|
945
997
|
}
|
|
946
998
|
const ctx = this.ctx({ to: spend.to, value: plan.amount });
|
|
@@ -1011,7 +1063,7 @@ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc
|
|
|
1011
1063
|
/** "What you sign": the fully resolved spend (signed, NOT broadcast) or a caller's raw hex. */
|
|
1012
1064
|
async describeTransaction(tx) {
|
|
1013
1065
|
if (typeof tx === "string") return (await this.describeRaw(tx)).desc;
|
|
1014
|
-
return (await this.
|
|
1066
|
+
return (await this.signSpend(tx)).desc;
|
|
1015
1067
|
}
|
|
1016
1068
|
/** 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. */
|
|
1017
1069
|
async signTransaction(tx, options = {}) {
|
|
@@ -1021,7 +1073,7 @@ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc
|
|
|
1021
1073
|
this.checkMax(desc.fee, (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
|
|
1022
1074
|
return tx;
|
|
1023
1075
|
}
|
|
1024
|
-
const { tx: built, fee } = await this.
|
|
1076
|
+
const { tx: built, fee } = await this.signSpend(tx);
|
|
1025
1077
|
this.checkMax(fee, (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
|
|
1026
1078
|
return built.hex;
|
|
1027
1079
|
}
|
|
@@ -1039,24 +1091,36 @@ var WalletAccountBtc = class _WalletAccountBtc extends WalletAccountReadOnlyBtc
|
|
|
1039
1091
|
hash = parsed.id;
|
|
1040
1092
|
fee = desc.fee;
|
|
1041
1093
|
} else {
|
|
1042
|
-
const built = await this.
|
|
1094
|
+
const built = await this.signSpend(tx);
|
|
1043
1095
|
rawHex = built.tx.hex;
|
|
1044
1096
|
hash = built.tx.id;
|
|
1045
1097
|
fee = built.fee;
|
|
1046
1098
|
}
|
|
1047
1099
|
this.checkMax(fee, (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
|
|
1048
|
-
|
|
1049
|
-
return { hash: txid || hash, fee };
|
|
1100
|
+
return { hash: await this.broadcast(client, rawHex, hash), fee };
|
|
1050
1101
|
}
|
|
1051
1102
|
/** Native BTC only (`token` is rejected). Subject to `transferMaxFee`. */
|
|
1052
1103
|
async transfer(o, writeOptions = {}) {
|
|
1053
1104
|
if (o.token) throw new Error("Tokens are not supported on the bitcoin blockchain.");
|
|
1054
1105
|
this.live();
|
|
1055
1106
|
const client = this.requireClient("transfer");
|
|
1056
|
-
const built = await this.
|
|
1107
|
+
const built = await this.signSpend({ to: o.recipient, value: o.amount });
|
|
1057
1108
|
this.checkMax(built.fee, (0, import_core9.feeLimit)(this._config.transferMaxFee, writeOptions), "transfer");
|
|
1058
|
-
|
|
1059
|
-
|
|
1109
|
+
return { hash: await this.broadcast(client, built.tx.hex, built.tx.id), fee: built.fee };
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Send once through the client. Only a node's refusal means nothing went out; "already in the
|
|
1113
|
+
* mempool / block chain" means an earlier attempt delivered it; anything else may have landed and
|
|
1114
|
+
* is a {@link BroadcastUnknownError} with the txid computed before sending (round 3, M12).
|
|
1115
|
+
*/
|
|
1116
|
+
async broadcast(client, rawHex, txid) {
|
|
1117
|
+
try {
|
|
1118
|
+
return await client.broadcast(rawHex) || txid;
|
|
1119
|
+
} catch (e) {
|
|
1120
|
+
if (isAlreadyKnown(e)) return txid;
|
|
1121
|
+
if (isAnsweredRefusal(e)) throw e;
|
|
1122
|
+
throw new BroadcastUnknownError(txid, e);
|
|
1123
|
+
}
|
|
1060
1124
|
}
|
|
1061
1125
|
/** The same address as a {@link WalletAccountReadOnlyBtc}. */
|
|
1062
1126
|
toReadOnlyAccount() {
|