@anywalletkit/wallet-zcash 0.2.6 → 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.cjs CHANGED
@@ -22,6 +22,7 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  BLOCKCHAIR_URL: () => BLOCKCHAIR_URL,
24
24
  BlockchairClient: () => BlockchairClient,
25
+ BroadcastUnknownError: () => BroadcastUnknownError,
25
26
  CAIP2: () => CAIP2,
26
27
  CURRENT_BRANCH_ID: () => CURRENT_BRANCH_ID,
27
28
  DEFAULT_EXPIRY_DELTA: () => DEFAULT_EXPIRY_DELTA,
@@ -65,6 +66,7 @@ __export(src_exports, {
65
66
  estimateSize: () => estimateSize,
66
67
  hash160: () => hash160,
67
68
  headerDigest: () => headerDigest,
69
+ isAnsweredRefusal: () => isAnsweredRefusal,
68
70
  logicalActions: () => logicalActions,
69
71
  magicHash: () => magicHash,
70
72
  mapDashboardTx: () => mapDashboardTx,
@@ -164,77 +166,6 @@ function outputSize(script) {
164
166
  }
165
167
  var DUST_LIMIT = 546n;
166
168
 
167
- // src/fee.ts
168
- var import_core = require("@anywalletkit/core");
169
- var MARGINAL_FEE = 5000n;
170
- var GRACE_ACTIONS = 2;
171
- var P2PKH_STANDARD_INPUT_SIZE = 150;
172
- var P2PKH_STANDARD_OUTPUT_SIZE = 34;
173
- var P2PKH_INPUT_SIZE = 149;
174
- var MAX_UTXO_INPUTS = 200;
175
- function logicalActions(inputSizes2, outputSizes) {
176
- const inBytes = inputSizes2.reduce((s, n) => s + n, 0);
177
- const outBytes = outputSizes.reduce((s, n) => s + n, 0);
178
- return Math.max(Math.ceil(inBytes / P2PKH_STANDARD_INPUT_SIZE), Math.ceil(outBytes / P2PKH_STANDARD_OUTPUT_SIZE));
179
- }
180
- function conventionalFee(inputSizes2, outputSizes) {
181
- return MARGINAL_FEE * BigInt(Math.max(GRACE_ACTIONS, logicalActions(inputSizes2, outputSizes)));
182
- }
183
- function estimateSize(inputs, outputSizes) {
184
- const cs = (n) => n < 253 ? 1 : 3;
185
- return 20 + cs(inputs) + inputs * P2PKH_INPUT_SIZE + cs(outputSizes.length) + outputSizes.reduce((s, n) => s + n, 0) + 3;
186
- }
187
- var TooManyInputsError = class extends Error {
188
- maxInputs;
189
- constructor(maxInputs) {
190
- super(`The spend needs more than ${maxInputs} inputs; consolidate the UTXOs first or raise maxInputs.`);
191
- this.name = "TooManyInputsError";
192
- this.maxInputs = maxInputs;
193
- }
194
- };
195
- 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;
196
- function spendableUtxos(utxos) {
197
- return utxos.filter((u) => u.value > MARGINAL_FEE).sort(cmpUtxo);
198
- }
199
- function selectCoins(o) {
200
- const maxInputs = o.maxInputs ?? MAX_UTXO_INPUTS;
201
- const dust = o.changeDust ?? DUST_LIMIT;
202
- const candidates = spendableUtxos(o.utxos);
203
- const available = o.utxos.reduce((s, u) => s + u.value, 0n);
204
- const inputs = (n) => Array.from({ length: n }, () => P2PKH_INPUT_SIZE);
205
- const selected = [];
206
- let sum = 0n;
207
- for (const u of candidates) {
208
- if (selected.length >= maxInputs) throw new TooManyInputsError(maxInputs);
209
- selected.push(u);
210
- sum += u.value;
211
- const n = selected.length;
212
- const convNoChange = conventionalFee(inputs(n), [o.recipientOutputSize]);
213
- const feeNoChange = o.fee ?? convNoChange;
214
- if (sum < o.target + feeNoChange) continue;
215
- const convWithChange = conventionalFee(inputs(n), [o.recipientOutputSize, o.changeOutputSize]);
216
- const feeWithChange = o.fee ?? convWithChange;
217
- const change = sum - o.target - feeWithChange;
218
- if (change >= dust) {
219
- return { utxos: selected, fee: feeWithChange, conventionalFee: convWithChange, change, changeFolded: false, size: estimateSize(n, [o.recipientOutputSize, o.changeOutputSize]) };
220
- }
221
- return { utxos: selected, fee: sum - o.target, conventionalFee: convNoChange, change: 0n, changeFolded: sum - o.target > feeNoChange, size: estimateSize(n, [o.recipientOutputSize]) };
222
- }
223
- const need = o.target + (o.fee ?? conventionalFee(inputs(Math.max(selected.length, 1)), [o.recipientOutputSize]));
224
- throw new import_core.InsufficientFundsError(need, available, "ZEC");
225
- }
226
- function selectAll(o) {
227
- const maxInputs = o.maxInputs ?? MAX_UTXO_INPUTS;
228
- const utxos = spendableUtxos(o.utxos).slice(0, maxInputs);
229
- if (utxos.length === 0) return { utxos, fee: 0n, amount: 0n, size: 0 };
230
- const fee = o.fee ?? conventionalFee(Array.from({ length: utxos.length }, () => P2PKH_INPUT_SIZE), [o.outputSize]);
231
- const amount = utxos.reduce((s, u) => s + u.value, 0n) - fee;
232
- return { utxos, fee, amount: amount > 0n ? amount : 0n, size: estimateSize(utxos.length, [o.outputSize]) };
233
- }
234
-
235
- // src/tx.ts
236
- var import_blake2 = require("@noble/hashes/blake2.js");
237
-
238
169
  // src/encoding.ts
239
170
  var import_utils = require("@noble/hashes/utils.js");
240
171
  function compactSize(n) {
@@ -351,7 +282,76 @@ function reverseBytes(b) {
351
282
  return Uint8Array.from(b).reverse();
352
283
  }
353
284
 
285
+ // src/fee.ts
286
+ var import_core = require("@anywalletkit/core");
287
+ var MARGINAL_FEE = 5000n;
288
+ var GRACE_ACTIONS = 2;
289
+ var P2PKH_STANDARD_INPUT_SIZE = 150;
290
+ var P2PKH_STANDARD_OUTPUT_SIZE = 34;
291
+ var P2PKH_INPUT_SIZE = 149;
292
+ var MAX_UTXO_INPUTS = 200;
293
+ function logicalActions(inputSizes2, outputSizes) {
294
+ const inBytes = inputSizes2.reduce((s, n) => s + n, 0);
295
+ const outBytes = outputSizes.reduce((s, n) => s + n, 0);
296
+ return Math.max(Math.ceil(inBytes / P2PKH_STANDARD_INPUT_SIZE), Math.ceil(outBytes / P2PKH_STANDARD_OUTPUT_SIZE));
297
+ }
298
+ function conventionalFee(inputSizes2, outputSizes) {
299
+ return MARGINAL_FEE * BigInt(Math.max(GRACE_ACTIONS, logicalActions(inputSizes2, outputSizes)));
300
+ }
301
+ function estimateSize(inputs, outputSizes) {
302
+ const cs = (n) => n < 253 ? 1 : 3;
303
+ return 20 + cs(inputs) + inputs * P2PKH_INPUT_SIZE + cs(outputSizes.length) + outputSizes.reduce((s, n) => s + n, 0) + 3;
304
+ }
305
+ var TooManyInputsError = class extends Error {
306
+ maxInputs;
307
+ constructor(maxInputs) {
308
+ super(`The spend needs more than ${maxInputs} inputs; consolidate the UTXOs first or raise maxInputs.`);
309
+ this.name = "TooManyInputsError";
310
+ this.maxInputs = maxInputs;
311
+ }
312
+ };
313
+ 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;
314
+ function spendableUtxos(utxos) {
315
+ return utxos.filter((u) => u.value > MARGINAL_FEE).sort(cmpUtxo);
316
+ }
317
+ function selectCoins(o) {
318
+ const maxInputs = o.maxInputs ?? MAX_UTXO_INPUTS;
319
+ const dust = o.changeDust ?? DUST_LIMIT;
320
+ const candidates = spendableUtxos(o.utxos);
321
+ const available = o.utxos.reduce((s, u) => s + u.value, 0n);
322
+ const inputs = (n) => Array.from({ length: n }, () => P2PKH_INPUT_SIZE);
323
+ const selected = [];
324
+ let sum = 0n;
325
+ for (const u of candidates) {
326
+ if (selected.length >= maxInputs) throw new TooManyInputsError(maxInputs);
327
+ selected.push(u);
328
+ sum += u.value;
329
+ const n = selected.length;
330
+ const convNoChange = conventionalFee(inputs(n), [o.recipientOutputSize]);
331
+ const feeNoChange = o.fee ?? convNoChange;
332
+ if (sum < o.target + feeNoChange) continue;
333
+ const convWithChange = conventionalFee(inputs(n), [o.recipientOutputSize, o.changeOutputSize]);
334
+ const feeWithChange = o.fee ?? convWithChange;
335
+ const change = sum - o.target - feeWithChange;
336
+ if (change >= dust) {
337
+ return { utxos: selected, fee: feeWithChange, conventionalFee: convWithChange, change, changeFolded: false, size: estimateSize(n, [o.recipientOutputSize, o.changeOutputSize]) };
338
+ }
339
+ return { utxos: selected, fee: sum - o.target, conventionalFee: convNoChange, change: 0n, changeFolded: sum - o.target > feeNoChange, size: estimateSize(n, [o.recipientOutputSize]) };
340
+ }
341
+ const need = o.target + (o.fee ?? conventionalFee(inputs(Math.max(selected.length, 1)), [o.recipientOutputSize]));
342
+ throw new import_core.InsufficientFundsError(need, available, "ZEC");
343
+ }
344
+ function selectAll(o) {
345
+ const maxInputs = o.maxInputs ?? MAX_UTXO_INPUTS;
346
+ const utxos = spendableUtxos(o.utxos).slice(0, maxInputs);
347
+ if (utxos.length === 0) return { utxos, fee: 0n, amount: 0n, size: 0 };
348
+ const fee = o.fee ?? conventionalFee(Array.from({ length: utxos.length }, () => P2PKH_INPUT_SIZE), [o.outputSize]);
349
+ const amount = utxos.reduce((s, u) => s + u.value, 0n) - fee;
350
+ return { utxos, fee, amount: amount > 0n ? amount : 0n, size: estimateSize(utxos.length, [o.outputSize]) };
351
+ }
352
+
354
353
  // src/tx.ts
354
+ var import_blake2 = require("@noble/hashes/blake2.js");
355
355
  var TX_VERSION_V5 = 2147483653;
356
356
  var VERSION_GROUP_ID_V5 = 648488714;
357
357
  var SIGHASH_ALL = 1;
@@ -529,9 +529,26 @@ function describeTransaction(tx, spent, ctx) {
529
529
  const inputs = tx.inputs.map((i, k) => ({ txid: i.txid, vout: i.vout, value: spent[k].value, address: addressFromScript(spent[k].script, ctx.network) }));
530
530
  const outputs = [];
531
531
  const warnings = /* @__PURE__ */ new Set();
532
+ const scriptOf = (address) => {
533
+ try {
534
+ return (0, import_utils.bytesToHex)(decodeAddress(address, ctx.network).script);
535
+ } catch {
536
+ return void 0;
537
+ }
538
+ };
539
+ const ownScript = scriptOf(ctx.ownAddress);
540
+ const intendedScript = ctx.intended ? scriptOf(ctx.intended.to) : void 0;
541
+ let recipientSeen = false;
532
542
  for (const o of tx.outputs) {
533
543
  const address = addressFromScript(o.script, ctx.network);
534
- const role = address !== void 0 && address === ctx.ownAddress ? "change" : ctx.intended === void 0 ? "recipient" : address !== void 0 && address === ctx.intended.to && o.value === ctx.intended.value ? "recipient" : "unknown";
544
+ const script = (0, import_utils.bytesToHex)(o.script);
545
+ const mine = script === ownScript;
546
+ let role;
547
+ if (ctx.intended === void 0) role = mine ? "change" : "recipient";
548
+ else if (!recipientSeen && script === intendedScript && o.value === ctx.intended.value) {
549
+ role = "recipient";
550
+ recipientSeen = true;
551
+ } else role = mine ? "change" : "unknown";
535
552
  if (scriptKind(o.script) !== "unknown" && o.value < DUST_LIMIT) warnings.add("dust-output");
536
553
  outputs.push({ address, value: o.value, role });
537
554
  }
@@ -591,6 +608,19 @@ async function fetchWithTimeout(fetchFn, url, init, timeoutMs, provider) {
591
608
  throw new import_core2.NetworkError(`Request failed: ${url}`, { provider, cause: e });
592
609
  }
593
610
  }
611
+ var refusedStatus = (status) => status >= 400 && status < 500 && status !== 408;
612
+ function refusal(e) {
613
+ return Object.assign(e, { answered: true });
614
+ }
615
+ function isAnsweredRefusal(e) {
616
+ return e?.answered === true;
617
+ }
618
+ function isAlreadyKnown(e) {
619
+ for (let x = e, depth = 0; x instanceof Error && depth < 5; x = x.cause, depth++) {
620
+ if (/txn-already-(in-mempool|known)|already (known|in (the )?(mempool|block ?chain))/i.test(x.message)) return true;
621
+ }
622
+ return false;
623
+ }
594
624
  var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
595
625
  var isIntString = (v) => typeof v === "string" && /^-?\d+$/.test(v);
596
626
  var isInt = (v) => typeof v === "number" && Number.isInteger(v);
@@ -725,14 +755,17 @@ var BlockchairClient = class {
725
755
  const url = this.url("push/transaction");
726
756
  const res = await fetchWithTimeout(this.fetchFn, url, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body }, this.timeoutMs, this.base);
727
757
  let env = {};
758
+ let parsed = false;
728
759
  try {
729
760
  env = await res.json();
761
+ parsed = true;
730
762
  } catch {
731
763
  }
732
764
  const hash = env.data?.transaction_hash;
733
765
  if (hash) return hash;
734
- const reason = env.context?.error ?? `${res.status} ${res.statusText}`;
735
- throw new Error(`Broadcast rejected: ${reason}`);
766
+ const reason = env.context?.error;
767
+ if (parsed && reason && (res.ok || refusedStatus(res.status))) throw refusal(new Error(`Broadcast rejected: ${reason}`));
768
+ throw new import_core3.NetworkError(`Blockchair broadcast: no answer to rely on (${res.status} ${res.statusText})`, { provider: this.base });
736
769
  }
737
770
  async getBlockHeight() {
738
771
  const { data } = await this.request("stats", {}, void 0, (v) => isRecord(v) && isInt(v.best_block_height));
@@ -767,7 +800,11 @@ var ZcashBlockbookClient = class _ZcashBlockbookClient {
767
800
  async get(path, check) {
768
801
  const url = `${this.base}/api${path}`;
769
802
  const res = await fetchWithTimeout(this.fetchFn, url, { headers: this.headers }, this.timeoutMs, this.base);
770
- if (!res.ok) throw new import_core4.NetworkError(`Blockbook request failed: ${res.status} ${res.statusText} (${url})`, { provider: this.base });
803
+ if (!res.ok) {
804
+ const reason = refusedStatus(res.status) ? (await res.text().catch(() => "")).slice(0, 200) : "";
805
+ const err = new import_core4.NetworkError(`Blockbook request failed: ${res.status} ${res.statusText} (${url})${reason ? `: ${reason}` : ""}`, { provider: this.base });
806
+ throw refusedStatus(res.status) ? refusal(err) : err;
807
+ }
771
808
  let body;
772
809
  try {
773
810
  body = await res.json();
@@ -817,7 +854,7 @@ var ZcashBlockbookClient = class _ZcashBlockbookClient {
817
854
  }
818
855
  async broadcast(rawTx) {
819
856
  const data = await this.get(`/v2/sendtx/${rawTx}`, (v) => isRecord(v));
820
- if (data.error) throw new Error(`Broadcast rejected: ${typeof data.error === "string" ? data.error : data.error.message ?? "broadcast failed"}`);
857
+ if (data.error) throw refusal(new Error(`Broadcast rejected: ${typeof data.error === "string" ? data.error : data.error.message ?? "broadcast failed"}`));
821
858
  if (!data.result) throw new import_core4.NetworkError("Blockbook returned no txid", { provider: this.base });
822
859
  return data.result;
823
860
  }
@@ -853,12 +890,19 @@ function createClient(config) {
853
890
  const attempts = list.length + (config.retries ?? 3);
854
891
  const call = (method) => async (...args) => {
855
892
  let last;
893
+ let silent = false;
856
894
  for (let i = 0; i < attempts; i++) {
857
895
  const c = list[i % list.length];
858
896
  try {
859
897
  return await c[method].apply(c, args);
860
898
  } catch (e) {
861
- if (method === "broadcast" && !(e instanceof import_core5.NetworkError)) throw e;
899
+ if (method === "broadcast") {
900
+ if (isAnsweredRefusal(e)) {
901
+ if (!silent) throw e;
902
+ throw new import_core5.NetworkError("A Zcash client refused the broadcast after another never answered: it may be out", { cause: e });
903
+ }
904
+ silent = true;
905
+ }
862
906
  last = e;
863
907
  }
864
908
  }
@@ -869,6 +913,19 @@ function createClient(config) {
869
913
  return out;
870
914
  }
871
915
 
916
+ // src/errors.ts
917
+ var BroadcastUnknownError = class extends Error {
918
+ /** Always true: the transaction may be on its way. */
919
+ maybeSent = true;
920
+ /** The transaction id (hex). */
921
+ hash;
922
+ constructor(hash, cause) {
923
+ super(`Broadcast outcome unknown for ${hash}: ${cause instanceof Error ? cause.message.slice(0, 200) : String(cause).slice(0, 200)}`, { cause });
924
+ this.name = "BroadcastUnknownError";
925
+ this.hash = hash;
926
+ }
927
+ };
928
+
872
929
  // src/message.ts
873
930
  var import_sha22 = require("@noble/hashes/sha2.js");
874
931
  var import_secp256k1 = require("@noble/curves/secp256k1.js");
@@ -1078,13 +1135,27 @@ var WalletAccountZcash = class _WalletAccountZcash extends WalletAccountReadOnly
1078
1135
  ctx(intended) {
1079
1136
  return { network: this._network, ownAddress: this.address, intended, maxFee: this.maxFee };
1080
1137
  }
1081
- /** Lock time + expiry + branch id from the tip (or the config override). */
1138
+ /**
1139
+ * Lock time, expiry and branch id for a transaction that lands in the next block (round 3, L7):
1140
+ * expiry = tip + 1 + `expiryDelta`, the branch id in force at tip + 1 (mainnet table; off mainnet
1141
+ * `consensusBranchId` must be configured — testnet and regtest activate upgrades at other heights).
1142
+ * Without a tip nothing is signed when the expiry, anti-fee-sniping or the branch id need it: a
1143
+ * transaction signed with expiry 0 never expires.
1144
+ */
1082
1145
  async chainParams() {
1083
- const tip = await this.tipHeight();
1084
1146
  const delta = this._config.expiryDelta ?? DEFAULT_EXPIRY_DELTA;
1085
- const consensusBranchId = this._config.consensusBranchId ?? branchIdForHeight(tip);
1086
- const lockTime = tip !== void 0 && this._config.antiFeeSniping !== false ? tip : 0;
1087
- const expiryHeight = tip !== void 0 && delta > 0 ? tip + delta : 0;
1147
+ const configured = this._config.consensusBranchId;
1148
+ if (configured === void 0 && this._network !== "mainnet") {
1149
+ throw new UnsafeTransactionError("unknown-branch-id", `On ${this._network} the consensus branch id depends on that network's upgrade heights; set consensusBranchId in the config.`);
1150
+ }
1151
+ const antiSnipe = this._config.antiFeeSniping !== false;
1152
+ const needTip = delta > 0 || antiSnipe || configured === void 0;
1153
+ const tip = needTip ? await this.tipHeight() : void 0;
1154
+ if (needTip && tip === void 0) throw new import_core7.NetworkError("The Zcash chain tip is unknown, so the expiry height and the branch id cannot be set; nothing was signed.");
1155
+ const next = tip === void 0 ? void 0 : tip + 1;
1156
+ const consensusBranchId = configured ?? branchIdForHeight(next);
1157
+ const lockTime = tip !== void 0 && antiSnipe ? tip : 0;
1158
+ const expiryHeight = next !== void 0 && delta > 0 ? next + delta : 0;
1088
1159
  return { lockTime, expiryHeight, consensusBranchId };
1089
1160
  }
1090
1161
  /** Sign every input through the signer; the scriptSig is `<DER ‖ 0x01> <pubkey>`. */
@@ -1174,8 +1245,7 @@ var WalletAccountZcash = class _WalletAccountZcash extends WalletAccountReadOnly
1174
1245
  fee = built.fee;
1175
1246
  }
1176
1247
  this.checkMax(fee, (0, import_core7.feeLimit)(this._config.transactionMaxFee, options), "transaction");
1177
- const id = await client.broadcast(rawHex);
1178
- return { hash: id || hash, fee };
1248
+ return { hash: await this.broadcast(client, rawHex, hash), fee };
1179
1249
  }
1180
1250
  /** Native ZEC only (`token` is rejected). Subject to `transferMaxFee`. */
1181
1251
  async transfer(o, writeOptions = {}) {
@@ -1184,8 +1254,21 @@ var WalletAccountZcash = class _WalletAccountZcash extends WalletAccountReadOnly
1184
1254
  const client = this.requireClient("transfer");
1185
1255
  const built = await this.signSpend({ to: o.recipient, value: o.amount });
1186
1256
  this.checkMax(built.fee, (0, import_core7.feeLimit)(this._config.transferMaxFee, writeOptions), "transfer");
1187
- const id = await client.broadcast(built.hex);
1188
- return { hash: id || built.txid, fee: built.fee };
1257
+ return { hash: await this.broadcast(client, built.hex, built.txid), fee: built.fee };
1258
+ }
1259
+ /**
1260
+ * Send once through the client. Only a node's refusal means nothing went out; "already in the
1261
+ * mempool / block chain" means an earlier attempt delivered it; anything else may have landed and
1262
+ * is a {@link BroadcastUnknownError} with the txid computed before sending (round 3, M12).
1263
+ */
1264
+ async broadcast(client, rawHex, txid2) {
1265
+ try {
1266
+ return await client.broadcast(rawHex) || txid2;
1267
+ } catch (e) {
1268
+ if (isAlreadyKnown(e)) return txid2;
1269
+ if (isAnsweredRefusal(e)) throw e;
1270
+ throw new BroadcastUnknownError(txid2, e);
1271
+ }
1189
1272
  }
1190
1273
  /** The same address as a {@link WalletAccountReadOnlyZcash}. */
1191
1274
  toReadOnlyAccount() {