@anywalletkit/wallet-zcash 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/LICENSE +202 -0
- package/README.md +82 -2
- package/dist/chunk-GJBPCXQK.js +421 -0
- package/dist/chunk-GJBPCXQK.js.map +1 -0
- package/dist/history-DQNeEwXE.d.cts +304 -0
- package/dist/history-DQNeEwXE.d.ts +304 -0
- package/dist/history.cjs +312 -0
- package/dist/history.cjs.map +1 -0
- package/dist/history.d.cts +2 -0
- package/dist/history.d.ts +2 -0
- package/dist/history.js +17 -0
- package/dist/history.js.map +1 -0
- package/dist/index.cjs +1355 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +419 -0
- package/dist/index.d.ts +419 -0
- package/dist/index.js +961 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -3
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1355 @@
|
|
|
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
|
+
BLOCKCHAIR_URL: () => BLOCKCHAIR_URL,
|
|
24
|
+
BlockchairClient: () => BlockchairClient,
|
|
25
|
+
CAIP2: () => CAIP2,
|
|
26
|
+
CURRENT_BRANCH_ID: () => CURRENT_BRANCH_ID,
|
|
27
|
+
DEFAULT_EXPIRY_DELTA: () => DEFAULT_EXPIRY_DELTA,
|
|
28
|
+
DEFAULT_MAX_FEE: () => DEFAULT_MAX_FEE,
|
|
29
|
+
DUST_LIMIT: () => DUST_LIMIT,
|
|
30
|
+
FINAL_SEQUENCE: () => FINAL_SEQUENCE,
|
|
31
|
+
GRACE_ACTIONS: () => GRACE_ACTIONS,
|
|
32
|
+
HistoryUnavailableError: () => HistoryUnavailableError,
|
|
33
|
+
MAINNET_UPGRADES: () => MAINNET_UPGRADES,
|
|
34
|
+
MARGINAL_FEE: () => MARGINAL_FEE,
|
|
35
|
+
MAX_SEQUENCE: () => MAX_SEQUENCE,
|
|
36
|
+
MAX_UTXO_INPUTS: () => MAX_UTXO_INPUTS,
|
|
37
|
+
P2PKH_INPUT_SIZE: () => P2PKH_INPUT_SIZE,
|
|
38
|
+
SIGHASH_ALL: () => SIGHASH_ALL,
|
|
39
|
+
TX_VERSION_V5: () => TX_VERSION_V5,
|
|
40
|
+
TooManyInputsError: () => TooManyInputsError,
|
|
41
|
+
UnsafeTransactionError: () => UnsafeTransactionError,
|
|
42
|
+
VERSION_GROUP_ID_V5: () => VERSION_GROUP_ID_V5,
|
|
43
|
+
WalletAccountReadOnlyZcash: () => WalletAccountReadOnlyZcash,
|
|
44
|
+
WalletAccountZcash: () => WalletAccountZcash,
|
|
45
|
+
WalletManagerZcash: () => WalletManagerZcash,
|
|
46
|
+
ZcashBlockbookClient: () => ZcashBlockbookClient,
|
|
47
|
+
addressFor: () => addressFor,
|
|
48
|
+
addressFromPublicKey: () => addressFromPublicKey,
|
|
49
|
+
addressFromScript: () => addressFromScript,
|
|
50
|
+
addressPrefix: () => addressPrefix,
|
|
51
|
+
assertSafe: () => assertSafe,
|
|
52
|
+
blockchairTime: () => blockchairTime,
|
|
53
|
+
branchIdForHeight: () => branchIdForHeight,
|
|
54
|
+
coinType: () => coinType,
|
|
55
|
+
conventionalFee: () => conventionalFee,
|
|
56
|
+
createClient: () => createClient,
|
|
57
|
+
createZcashHistory: () => createZcashHistory,
|
|
58
|
+
decodeAddress: () => decodeAddress,
|
|
59
|
+
decodeHistoryCursor: () => decodeCursor,
|
|
60
|
+
default: () => WalletManagerZcash,
|
|
61
|
+
describeTransaction: () => describeTransaction,
|
|
62
|
+
encodeAddress: () => encodeAddress,
|
|
63
|
+
encodeHistoryCursor: () => encodeCursor,
|
|
64
|
+
encodeMessageSignature: () => encodeMessageSignature,
|
|
65
|
+
estimateSize: () => estimateSize,
|
|
66
|
+
hash160: () => hash160,
|
|
67
|
+
headerDigest: () => headerDigest,
|
|
68
|
+
logicalActions: () => logicalActions,
|
|
69
|
+
magicHash: () => magicHash,
|
|
70
|
+
mapDashboardTx: () => mapDashboardTx,
|
|
71
|
+
outputSize: () => outputSize,
|
|
72
|
+
outputsDigest: () => outputsDigest,
|
|
73
|
+
p2pkhScriptSig: () => p2pkhScriptSig,
|
|
74
|
+
parseP2pkhScriptSig: () => parseP2pkhScriptSig,
|
|
75
|
+
parseTx: () => parseTx,
|
|
76
|
+
prevoutsDigest: () => prevoutsDigest,
|
|
77
|
+
recoverMessagePublicKey: () => recoverMessagePublicKey,
|
|
78
|
+
scriptFor: () => scriptFor,
|
|
79
|
+
scriptKind: () => scriptKind,
|
|
80
|
+
selectAll: () => selectAll,
|
|
81
|
+
selectCoins: () => selectCoins,
|
|
82
|
+
sequenceDigest: () => sequenceDigest,
|
|
83
|
+
serializeTx: () => serializeTx,
|
|
84
|
+
sighashAll: () => sighashAll,
|
|
85
|
+
signMessageWith: () => signMessageWith,
|
|
86
|
+
spendableUtxos: () => spendableUtxos,
|
|
87
|
+
transparentDigest: () => transparentDigest,
|
|
88
|
+
txHashPersonalization: () => txHashPersonalization,
|
|
89
|
+
txid: () => txid,
|
|
90
|
+
txidDigest: () => txidDigest,
|
|
91
|
+
zcashExplorerTxUrl: () => zcashExplorerTxUrl
|
|
92
|
+
});
|
|
93
|
+
module.exports = __toCommonJS(src_exports);
|
|
94
|
+
|
|
95
|
+
// src/types.ts
|
|
96
|
+
var CAIP2 = { mainnet: "zcash:mainnet", testnet: "zcash:testnet" };
|
|
97
|
+
|
|
98
|
+
// src/address.ts
|
|
99
|
+
var import_sha2 = require("@noble/hashes/sha2.js");
|
|
100
|
+
var import_legacy = require("@noble/hashes/legacy.js");
|
|
101
|
+
var import_base = require("@scure/base");
|
|
102
|
+
var b58 = (0, import_base.base58check)(import_sha2.sha256);
|
|
103
|
+
var PREFIX = {
|
|
104
|
+
mainnet: { p2pkh: [28, 184], p2sh: [28, 189] },
|
|
105
|
+
testnet: { p2pkh: [29, 37], p2sh: [28, 186] }
|
|
106
|
+
};
|
|
107
|
+
function addressPrefix(network, kind) {
|
|
108
|
+
return PREFIX[network][kind];
|
|
109
|
+
}
|
|
110
|
+
function hash160(data) {
|
|
111
|
+
return (0, import_legacy.ripemd160)((0, import_sha2.sha256)(data));
|
|
112
|
+
}
|
|
113
|
+
function scriptFor(kind, hash) {
|
|
114
|
+
if (hash.length !== 20) throw new Error("A transparent script needs a 20-byte hash");
|
|
115
|
+
if (kind === "p2pkh") return Uint8Array.of(118, 169, 20, ...hash, 136, 172);
|
|
116
|
+
return Uint8Array.of(169, 20, ...hash, 135);
|
|
117
|
+
}
|
|
118
|
+
function scriptKind(script) {
|
|
119
|
+
if (script.length === 25 && script[0] === 118 && script[1] === 169 && script[2] === 20 && script[23] === 136 && script[24] === 172) return "p2pkh";
|
|
120
|
+
if (script.length === 23 && script[0] === 169 && script[1] === 20 && script[22] === 135) return "p2sh";
|
|
121
|
+
return "unknown";
|
|
122
|
+
}
|
|
123
|
+
function encodeAddress(kind, hash, network) {
|
|
124
|
+
if (hash.length !== 20) throw new Error("A transparent address needs a 20-byte hash");
|
|
125
|
+
const [a, b] = PREFIX[network][kind];
|
|
126
|
+
return b58.encode(Uint8Array.of(a, b, ...hash));
|
|
127
|
+
}
|
|
128
|
+
function decodeAddress(address, network) {
|
|
129
|
+
const a = address.trim();
|
|
130
|
+
if (/^u1|^utest/i.test(a)) throw new Error(`Unified address (${a.slice(0, 6)}\u2026) \u2014 shielded receivers are not supported; use a transparent t-address.`);
|
|
131
|
+
if (/^zs1|^ztestsapling/i.test(a)) throw new Error("Sapling shielded address (zs1\u2026) \u2014 this wallet sends to transparent t-addresses only.");
|
|
132
|
+
if (/^zc/i.test(a) && a.length > 90) throw new Error("Sprout address (zc\u2026) \u2014 the Sprout pool is deprecated; use a transparent t-address.");
|
|
133
|
+
let payload;
|
|
134
|
+
try {
|
|
135
|
+
payload = b58.decode(a);
|
|
136
|
+
} catch (e) {
|
|
137
|
+
throw new Error(`Invalid Zcash address: ${a}`, { cause: e });
|
|
138
|
+
}
|
|
139
|
+
if (payload.length !== 22) throw new Error(`Invalid Zcash address: ${a}`);
|
|
140
|
+
const p0 = payload[0], p1 = payload[1];
|
|
141
|
+
for (const net of ["mainnet", "testnet"]) {
|
|
142
|
+
for (const kind of ["p2pkh", "p2sh"]) {
|
|
143
|
+
const [a0, a1] = PREFIX[net][kind];
|
|
144
|
+
if (p0 === a0 && p1 === a1) {
|
|
145
|
+
if (net !== network) throw new Error(`${a} is a Zcash ${net} address; this wallet is on ${network}.`);
|
|
146
|
+
const hash = payload.slice(2);
|
|
147
|
+
return { kind, hash, script: scriptFor(kind, hash) };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`Invalid Zcash address: ${a} (unknown version prefix)`);
|
|
152
|
+
}
|
|
153
|
+
function addressFromScript(script, network) {
|
|
154
|
+
const kind = scriptKind(script);
|
|
155
|
+
if (kind === "unknown") return void 0;
|
|
156
|
+
return encodeAddress(kind, kind === "p2pkh" ? script.slice(3, 23) : script.slice(2, 22), network);
|
|
157
|
+
}
|
|
158
|
+
function addressFromPublicKey(publicKey, network = "mainnet") {
|
|
159
|
+
if (publicKey.length !== 33) throw new Error("Expected a 33-byte compressed public key");
|
|
160
|
+
return encodeAddress("p2pkh", hash160(publicKey), network);
|
|
161
|
+
}
|
|
162
|
+
function outputSize(script) {
|
|
163
|
+
return 8 + (script.length < 253 ? 1 : 3) + script.length;
|
|
164
|
+
}
|
|
165
|
+
var DUST_LIMIT = 546n;
|
|
166
|
+
|
|
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
|
+
// src/encoding.ts
|
|
239
|
+
var import_utils = require("@noble/hashes/utils.js");
|
|
240
|
+
function compactSize(n) {
|
|
241
|
+
if (!Number.isInteger(n) || n < 0) throw new Error(`CompactSize: ${n} is not a non-negative integer`);
|
|
242
|
+
if (n < 253) return Uint8Array.of(n);
|
|
243
|
+
if (n <= 65535) return Uint8Array.of(253, n & 255, n >>> 8);
|
|
244
|
+
if (n <= 4294967295) return Uint8Array.of(254, n & 255, n >>> 8 & 255, n >>> 16 & 255, n >>> 24);
|
|
245
|
+
throw new Error("CompactSize: value too large");
|
|
246
|
+
}
|
|
247
|
+
function u32le(n) {
|
|
248
|
+
if (!Number.isInteger(n) || n < 0 || n > 4294967295) throw new Error(`u32: ${n} out of range`);
|
|
249
|
+
return Uint8Array.of(n & 255, n >>> 8 & 255, n >>> 16 & 255, n >>> 24);
|
|
250
|
+
}
|
|
251
|
+
function i64le(v) {
|
|
252
|
+
if (v < -(1n << 63n) || v >= 1n << 63n) throw new Error(`i64: ${v} out of range`);
|
|
253
|
+
let x = v < 0n ? v + (1n << 64n) : v;
|
|
254
|
+
const out = new Uint8Array(8);
|
|
255
|
+
for (let i = 0; i < 8; i++) {
|
|
256
|
+
out[i] = Number(x & 0xffn);
|
|
257
|
+
x >>= 8n;
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
var Writer = class {
|
|
262
|
+
parts = [];
|
|
263
|
+
u8(n) {
|
|
264
|
+
this.parts.push(Uint8Array.of(n & 255));
|
|
265
|
+
return this;
|
|
266
|
+
}
|
|
267
|
+
u32(n) {
|
|
268
|
+
this.parts.push(u32le(n));
|
|
269
|
+
return this;
|
|
270
|
+
}
|
|
271
|
+
i64(v) {
|
|
272
|
+
this.parts.push(i64le(v));
|
|
273
|
+
return this;
|
|
274
|
+
}
|
|
275
|
+
bytes(b) {
|
|
276
|
+
this.parts.push(b);
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
compact(n) {
|
|
280
|
+
this.parts.push(compactSize(n));
|
|
281
|
+
return this;
|
|
282
|
+
}
|
|
283
|
+
/** CompactSize length prefix + bytes (the "field encoding" of a script). */
|
|
284
|
+
varBytes(b) {
|
|
285
|
+
return this.compact(b.length).bytes(b);
|
|
286
|
+
}
|
|
287
|
+
finish() {
|
|
288
|
+
const len = this.parts.reduce((s, p) => s + p.length, 0);
|
|
289
|
+
const out = new Uint8Array(len);
|
|
290
|
+
let o = 0;
|
|
291
|
+
for (const p of this.parts) {
|
|
292
|
+
out.set(p, o);
|
|
293
|
+
o += p.length;
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
var Reader = class {
|
|
299
|
+
constructor(buf) {
|
|
300
|
+
this.buf = buf;
|
|
301
|
+
}
|
|
302
|
+
buf;
|
|
303
|
+
offset = 0;
|
|
304
|
+
get remaining() {
|
|
305
|
+
return this.buf.length - this.offset;
|
|
306
|
+
}
|
|
307
|
+
need(n) {
|
|
308
|
+
if (this.offset + n > this.buf.length) throw new Error("Transaction truncated");
|
|
309
|
+
}
|
|
310
|
+
u8() {
|
|
311
|
+
this.need(1);
|
|
312
|
+
return this.buf[this.offset++];
|
|
313
|
+
}
|
|
314
|
+
u32() {
|
|
315
|
+
this.need(4);
|
|
316
|
+
const b = this.buf;
|
|
317
|
+
const o = this.offset;
|
|
318
|
+
this.offset += 4;
|
|
319
|
+
return (b[o] | b[o + 1] << 8 | b[o + 2] << 16) + b[o + 3] * 16777216;
|
|
320
|
+
}
|
|
321
|
+
i64() {
|
|
322
|
+
this.need(8);
|
|
323
|
+
let v = 0n;
|
|
324
|
+
for (let i = 7; i >= 0; i--) v = v << 8n | BigInt(this.buf[this.offset + i]);
|
|
325
|
+
this.offset += 8;
|
|
326
|
+
return v >= 1n << 63n ? v - (1n << 64n) : v;
|
|
327
|
+
}
|
|
328
|
+
bytes(n) {
|
|
329
|
+
this.need(n);
|
|
330
|
+
const out = this.buf.slice(this.offset, this.offset + n);
|
|
331
|
+
this.offset += n;
|
|
332
|
+
return out;
|
|
333
|
+
}
|
|
334
|
+
compact() {
|
|
335
|
+
const first = this.u8();
|
|
336
|
+
if (first < 253) return first;
|
|
337
|
+
if (first === 253) {
|
|
338
|
+
this.need(2);
|
|
339
|
+
const v = this.buf[this.offset] | this.buf[this.offset + 1] << 8;
|
|
340
|
+
this.offset += 2;
|
|
341
|
+
return v;
|
|
342
|
+
}
|
|
343
|
+
if (first === 254) return this.u32();
|
|
344
|
+
throw new Error("CompactSize: 64-bit lengths are not supported");
|
|
345
|
+
}
|
|
346
|
+
varBytes() {
|
|
347
|
+
return this.bytes(this.compact());
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
function reverseBytes(b) {
|
|
351
|
+
return Uint8Array.from(b).reverse();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/tx.ts
|
|
355
|
+
var TX_VERSION_V5 = 2147483653;
|
|
356
|
+
var VERSION_GROUP_ID_V5 = 648488714;
|
|
357
|
+
var SIGHASH_ALL = 1;
|
|
358
|
+
var MAINNET_UPGRADES = Object.freeze([
|
|
359
|
+
{ name: "NU5", height: 1687104, branchId: 3268858036 },
|
|
360
|
+
{ name: "NU6", height: 2726400, branchId: 3370586197 },
|
|
361
|
+
{ name: "NU6.1", height: 3146400, branchId: 1307332080 },
|
|
362
|
+
{ name: "NU6.3", height: 3428143, branchId: 933566043 }
|
|
363
|
+
]);
|
|
364
|
+
var CURRENT_BRANCH_ID = MAINNET_UPGRADES[MAINNET_UPGRADES.length - 1].branchId;
|
|
365
|
+
function branchIdForHeight(height) {
|
|
366
|
+
if (height === void 0 || !Number.isFinite(height)) return CURRENT_BRANCH_ID;
|
|
367
|
+
let id = MAINNET_UPGRADES[0].branchId;
|
|
368
|
+
for (const u of MAINNET_UPGRADES) if (height >= u.height) id = u.branchId;
|
|
369
|
+
return id;
|
|
370
|
+
}
|
|
371
|
+
var FINAL_SEQUENCE = 4294967294;
|
|
372
|
+
var MAX_SEQUENCE = 4294967295;
|
|
373
|
+
var assertTxid = (txid2) => {
|
|
374
|
+
if (!/^[0-9a-fA-F]{64}$/.test(txid2)) throw new Error(`Invalid txid: ${txid2}`);
|
|
375
|
+
};
|
|
376
|
+
function writeTransparent(w, tx) {
|
|
377
|
+
w.compact(tx.inputs.length);
|
|
378
|
+
for (const i of tx.inputs) {
|
|
379
|
+
assertTxid(i.txid);
|
|
380
|
+
w.bytes(reverseBytes((0, import_utils.hexToBytes)(i.txid))).u32(i.vout).varBytes(i.scriptSig).u32(i.sequence);
|
|
381
|
+
}
|
|
382
|
+
w.compact(tx.outputs.length);
|
|
383
|
+
for (const o of tx.outputs) {
|
|
384
|
+
if (o.value < 0n) throw new Error("Negative output value");
|
|
385
|
+
w.i64(o.value).varBytes(o.script);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function serializeTx(tx) {
|
|
389
|
+
const w = new Writer();
|
|
390
|
+
w.u32(TX_VERSION_V5).u32(VERSION_GROUP_ID_V5).u32(tx.consensusBranchId >>> 0).u32(tx.lockTime).u32(tx.expiryHeight);
|
|
391
|
+
writeTransparent(w, tx);
|
|
392
|
+
w.compact(0);
|
|
393
|
+
w.compact(0);
|
|
394
|
+
w.compact(0);
|
|
395
|
+
return w.finish();
|
|
396
|
+
}
|
|
397
|
+
function parseTx(raw) {
|
|
398
|
+
const r = new Reader(typeof raw === "string" ? (0, import_utils.hexToBytes)(raw) : raw);
|
|
399
|
+
const header = r.u32();
|
|
400
|
+
const overwintered = (header & 2147483648) !== 0;
|
|
401
|
+
const version = header & 2147483647;
|
|
402
|
+
if (!overwintered || version !== 5) throw new Error(`Only v5 Zcash transactions are supported here (got version ${version}${overwintered ? "" : ", not overwintered"}).`);
|
|
403
|
+
const vgid = r.u32();
|
|
404
|
+
if (vgid !== VERSION_GROUP_ID_V5) throw new Error(`Unexpected version group id 0x${vgid.toString(16)} for a v5 transaction.`);
|
|
405
|
+
const consensusBranchId = r.u32();
|
|
406
|
+
const lockTime = r.u32();
|
|
407
|
+
const expiryHeight = r.u32();
|
|
408
|
+
const nIn = r.compact();
|
|
409
|
+
const inputs = [];
|
|
410
|
+
for (let i = 0; i < nIn; i++) {
|
|
411
|
+
const txid2 = (0, import_utils.bytesToHex)(reverseBytes(r.bytes(32)));
|
|
412
|
+
const vout = r.u32();
|
|
413
|
+
const scriptSig = r.varBytes();
|
|
414
|
+
const sequence = r.u32();
|
|
415
|
+
inputs.push({ txid: txid2, vout, scriptSig, sequence });
|
|
416
|
+
}
|
|
417
|
+
const nOut = r.compact();
|
|
418
|
+
const outputs = [];
|
|
419
|
+
for (let i = 0; i < nOut; i++) outputs.push({ value: r.i64(), script: r.varBytes() });
|
|
420
|
+
const nSpends = r.compact();
|
|
421
|
+
const nOutputs = r.compact();
|
|
422
|
+
if (nSpends !== 0 || nOutputs !== 0) throw new Error("This transaction carries a Sapling bundle; shielded transactions are not supported.");
|
|
423
|
+
const nActions = r.compact();
|
|
424
|
+
if (nActions !== 0) throw new Error("This transaction carries an Orchard bundle; shielded transactions are not supported.");
|
|
425
|
+
if (r.remaining !== 0) throw new Error(`Trailing bytes after the transaction (${r.remaining}).`);
|
|
426
|
+
return { consensusBranchId, lockTime, expiryHeight, inputs, outputs };
|
|
427
|
+
}
|
|
428
|
+
function personal(tag) {
|
|
429
|
+
const p = (0, import_utils.utf8ToBytes)(tag);
|
|
430
|
+
if (p.length !== 16) throw new Error(`BLAKE2b personalization must be 16 bytes: ${tag}`);
|
|
431
|
+
return p;
|
|
432
|
+
}
|
|
433
|
+
function h(tag, data) {
|
|
434
|
+
return (0, import_blake2.blake2b)(data, { dkLen: 32, personalization: typeof tag === "string" ? personal(tag) : tag });
|
|
435
|
+
}
|
|
436
|
+
var EMPTY = new Uint8Array(0);
|
|
437
|
+
function txHashPersonalization(consensusBranchId) {
|
|
438
|
+
const p = new Uint8Array(16);
|
|
439
|
+
p.set((0, import_utils.utf8ToBytes)("ZcashTxHash_"), 0);
|
|
440
|
+
p.set(u32le(consensusBranchId >>> 0), 12);
|
|
441
|
+
return p;
|
|
442
|
+
}
|
|
443
|
+
function headerDigest(tx) {
|
|
444
|
+
const w = new Writer().u32(TX_VERSION_V5).u32(VERSION_GROUP_ID_V5).u32(tx.consensusBranchId >>> 0).u32(tx.lockTime).u32(tx.expiryHeight);
|
|
445
|
+
return h("ZTxIdHeadersHash", w.finish());
|
|
446
|
+
}
|
|
447
|
+
function prevoutsDigest(tx) {
|
|
448
|
+
const w = new Writer();
|
|
449
|
+
for (const i of tx.inputs) {
|
|
450
|
+
assertTxid(i.txid);
|
|
451
|
+
w.bytes(reverseBytes((0, import_utils.hexToBytes)(i.txid))).u32(i.vout);
|
|
452
|
+
}
|
|
453
|
+
return h("ZTxIdPrevoutHash", w.finish());
|
|
454
|
+
}
|
|
455
|
+
function sequenceDigest(tx) {
|
|
456
|
+
const w = new Writer();
|
|
457
|
+
for (const i of tx.inputs) w.u32(i.sequence);
|
|
458
|
+
return h("ZTxIdSequencHash", w.finish());
|
|
459
|
+
}
|
|
460
|
+
function outputsDigest(tx) {
|
|
461
|
+
const w = new Writer();
|
|
462
|
+
for (const o of tx.outputs) w.i64(o.value).varBytes(o.script);
|
|
463
|
+
return h("ZTxIdOutputsHash", w.finish());
|
|
464
|
+
}
|
|
465
|
+
function transparentDigest(tx) {
|
|
466
|
+
if (tx.inputs.length === 0 && tx.outputs.length === 0) return h("ZTxIdTranspaHash", EMPTY);
|
|
467
|
+
const w = new Writer().bytes(prevoutsDigest(tx)).bytes(sequenceDigest(tx)).bytes(outputsDigest(tx));
|
|
468
|
+
return h("ZTxIdTranspaHash", w.finish());
|
|
469
|
+
}
|
|
470
|
+
var saplingDigestEmpty = () => h("ZTxIdSaplingHash", EMPTY);
|
|
471
|
+
var orchardDigestEmpty = () => h("ZTxIdOrchardHash", EMPTY);
|
|
472
|
+
function txidDigest(tx) {
|
|
473
|
+
const w = new Writer().bytes(headerDigest(tx)).bytes(transparentDigest(tx)).bytes(saplingDigestEmpty()).bytes(orchardDigestEmpty());
|
|
474
|
+
return h(txHashPersonalization(tx.consensusBranchId), w.finish());
|
|
475
|
+
}
|
|
476
|
+
function txid(tx) {
|
|
477
|
+
return (0, import_utils.bytesToHex)(reverseBytes(txidDigest(tx)));
|
|
478
|
+
}
|
|
479
|
+
function sighashAll(tx, index, spent) {
|
|
480
|
+
if (spent.length !== tx.inputs.length) throw new Error(`sighash: ${tx.inputs.length} inputs but ${spent.length} spent outputs`);
|
|
481
|
+
const input = tx.inputs[index];
|
|
482
|
+
const coin = spent[index];
|
|
483
|
+
if (!input || !coin) throw new Error(`sighash: input ${index} does not exist`);
|
|
484
|
+
const amounts = new Writer();
|
|
485
|
+
const scripts = new Writer();
|
|
486
|
+
for (const s of spent) {
|
|
487
|
+
amounts.i64(s.value);
|
|
488
|
+
scripts.varBytes(s.script);
|
|
489
|
+
}
|
|
490
|
+
const txin = new Writer().bytes(reverseBytes((0, import_utils.hexToBytes)(input.txid))).u32(input.vout).i64(coin.value).varBytes(coin.script).u32(input.sequence);
|
|
491
|
+
const t = new Writer().u8(SIGHASH_ALL).bytes(prevoutsDigest(tx)).bytes(h("ZTxTrAmountsHash", amounts.finish())).bytes(h("ZTxTrScriptsHash", scripts.finish())).bytes(sequenceDigest(tx)).bytes(outputsDigest(tx)).bytes(h("Zcash___TxInHash", txin.finish()));
|
|
492
|
+
const transparentSig = h("ZTxIdTranspaHash", t.finish());
|
|
493
|
+
const w = new Writer().bytes(headerDigest(tx)).bytes(transparentSig).bytes(saplingDigestEmpty()).bytes(orchardDigestEmpty());
|
|
494
|
+
return h(txHashPersonalization(tx.consensusBranchId), w.finish());
|
|
495
|
+
}
|
|
496
|
+
function p2pkhScriptSig(derSignature, publicKey) {
|
|
497
|
+
const sig = new Uint8Array(derSignature.length + 1);
|
|
498
|
+
sig.set(derSignature, 0);
|
|
499
|
+
sig[derSignature.length] = SIGHASH_ALL;
|
|
500
|
+
return new Writer().bytes(compactSize(sig.length)).bytes(sig).bytes(compactSize(publicKey.length)).bytes(publicKey).finish();
|
|
501
|
+
}
|
|
502
|
+
function parseP2pkhScriptSig(scriptSig) {
|
|
503
|
+
try {
|
|
504
|
+
const r = new Reader(scriptSig);
|
|
505
|
+
const sig = r.varBytes();
|
|
506
|
+
const pub = r.varBytes();
|
|
507
|
+
if (r.remaining !== 0 || sig.length < 9 || pub.length !== 33 && pub.length !== 65) return void 0;
|
|
508
|
+
return { signature: sig.slice(0, -1), hashType: sig[sig.length - 1], publicKey: pub };
|
|
509
|
+
} catch {
|
|
510
|
+
return void 0;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/describe.ts
|
|
515
|
+
var DEFAULT_MAX_FEE = 1000000n;
|
|
516
|
+
var UnsafeTransactionError = class extends Error {
|
|
517
|
+
code;
|
|
518
|
+
constructor(code, message) {
|
|
519
|
+
super(message);
|
|
520
|
+
this.name = "UnsafeTransactionError";
|
|
521
|
+
this.code = code;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
function inputSizes(tx) {
|
|
525
|
+
return tx.inputs.map((i) => 32 + 4 + (i.scriptSig.length < 253 ? 1 : 3) + i.scriptSig.length + 4);
|
|
526
|
+
}
|
|
527
|
+
function describeTransaction(tx, spent, ctx) {
|
|
528
|
+
if (spent.length !== tx.inputs.length) throw new Error(`describeTransaction: ${tx.inputs.length} inputs but ${spent.length} spent outputs`);
|
|
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
|
+
const outputs = [];
|
|
531
|
+
const warnings = /* @__PURE__ */ new Set();
|
|
532
|
+
for (const o of tx.outputs) {
|
|
533
|
+
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";
|
|
535
|
+
if (scriptKind(o.script) !== "unknown" && o.value < DUST_LIMIT) warnings.add("dust-output");
|
|
536
|
+
outputs.push({ address, value: o.value, role });
|
|
537
|
+
}
|
|
538
|
+
const inputSum = spent.reduce((s, c) => s + c.value, 0n);
|
|
539
|
+
const outputSum = tx.outputs.reduce((s, o) => s + o.value, 0n);
|
|
540
|
+
const fee = inputSum - outputSum;
|
|
541
|
+
const amount = outputs.filter((o) => o.role !== "change").reduce((s, o) => s + o.value, 0n);
|
|
542
|
+
const conv = conventionalFee(inputSizes(tx), tx.outputs.map((o) => outputSize(o.script)));
|
|
543
|
+
if (fee < conv) warnings.add("fee-below-conventional");
|
|
544
|
+
if (amount > 0n && fee * 10n > amount) warnings.add("fee-over-10pct-of-amount");
|
|
545
|
+
if (!outputs.some((o) => o.role === "change")) warnings.add("no-change");
|
|
546
|
+
if (tx.expiryHeight === 0) warnings.add("no-expiry");
|
|
547
|
+
const signed = tx.inputs.length > 0 && tx.inputs.every((i) => i.scriptSig.length > 0);
|
|
548
|
+
const desc = {
|
|
549
|
+
kind: "zcash-transfer",
|
|
550
|
+
version: 5,
|
|
551
|
+
consensusBranchId: tx.consensusBranchId,
|
|
552
|
+
inputs,
|
|
553
|
+
outputs,
|
|
554
|
+
amount,
|
|
555
|
+
fee,
|
|
556
|
+
conventionalFee: conv,
|
|
557
|
+
size: serializeTx(tx).length,
|
|
558
|
+
lockTime: tx.lockTime,
|
|
559
|
+
expiryHeight: tx.expiryHeight,
|
|
560
|
+
warnings: [...warnings]
|
|
561
|
+
};
|
|
562
|
+
if (signed) desc.txid = txid(tx);
|
|
563
|
+
return desc;
|
|
564
|
+
}
|
|
565
|
+
function assertSafe(desc, ctx) {
|
|
566
|
+
const maxFee = ctx.maxFee ?? DEFAULT_MAX_FEE;
|
|
567
|
+
if (desc.fee < 0n) throw new UnsafeTransactionError("outputs-exceed-inputs", "Outputs exceed inputs.");
|
|
568
|
+
if (desc.fee > maxFee) throw new UnsafeTransactionError("fee-too-high", `Fee ${desc.fee} zatoshis exceeds the ${maxFee} zatoshi ceiling (maxFee).`);
|
|
569
|
+
for (const o of desc.outputs) {
|
|
570
|
+
if (o.role === "change" && o.address !== ctx.ownAddress) throw new UnsafeTransactionError("change-not-ours", `Change output pays ${o.address ?? "<no address>"}, not ${ctx.ownAddress}.`);
|
|
571
|
+
}
|
|
572
|
+
if (ctx.intended) {
|
|
573
|
+
const recipients = desc.outputs.filter((o) => o.role === "recipient");
|
|
574
|
+
if (recipients.length !== 1) throw new UnsafeTransactionError("recipient-mismatch", `Expected one output of ${ctx.intended.value} zatoshis to ${ctx.intended.to}, found ${recipients.length}.`);
|
|
575
|
+
const unknown = desc.outputs.filter((o) => o.role === "unknown");
|
|
576
|
+
if (unknown.length) throw new UnsafeTransactionError("unexpected-output", `Transaction carries ${unknown.length} output(s) that are neither the recipient nor our change.`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/blockchair.ts
|
|
581
|
+
var import_core3 = require("@anywalletkit/core");
|
|
582
|
+
|
|
583
|
+
// src/http.ts
|
|
584
|
+
var import_core2 = require("@anywalletkit/core");
|
|
585
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
586
|
+
async function fetchWithTimeout(fetchFn, url, init, timeoutMs, provider) {
|
|
587
|
+
const signal = typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(timeoutMs) : void 0;
|
|
588
|
+
try {
|
|
589
|
+
return await fetchFn(url, { ...init, ...signal ? { signal } : {} });
|
|
590
|
+
} catch (e) {
|
|
591
|
+
throw new import_core2.NetworkError(`Request failed: ${url}`, { provider, cause: e });
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
595
|
+
var isIntString = (v) => typeof v === "string" && /^-?\d+$/.test(v);
|
|
596
|
+
var isInt = (v) => typeof v === "number" && Number.isInteger(v);
|
|
597
|
+
|
|
598
|
+
// src/blockchair.ts
|
|
599
|
+
var BLOCKCHAIR_URL = "https://api.blockchair.com/zcash";
|
|
600
|
+
var UTXO_LIMIT = 1e3;
|
|
601
|
+
var isUtxo = (v) => isRecord(v) && isInt(v.block_id) && typeof v.transaction_hash === "string" && isInt(v.index) && typeof v.value === "number";
|
|
602
|
+
var isOutput = (v) => isRecord(v) && isInt(v.index) && typeof v.value === "number" && typeof v.script_hex === "string";
|
|
603
|
+
var isDashboard = (v) => isRecord(v) && isRecord(v.transaction) && isInt(v.transaction.block_id) && typeof v.transaction.hash === "string" && Array.isArray(v.inputs) && v.inputs.every(isOutput) && Array.isArray(v.outputs) && v.outputs.every(isOutput);
|
|
604
|
+
function blockchairTime(s) {
|
|
605
|
+
if (!s) return void 0;
|
|
606
|
+
const t = Date.parse(`${s.replace(" ", "T")}Z`);
|
|
607
|
+
return Number.isFinite(t) ? Math.floor(t / 1e3) : void 0;
|
|
608
|
+
}
|
|
609
|
+
var BlockchairClient = class {
|
|
610
|
+
base;
|
|
611
|
+
key;
|
|
612
|
+
fetchFn;
|
|
613
|
+
timeoutMs;
|
|
614
|
+
constructor(config = {}, fetchFn = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
615
|
+
this.base = (config.url ?? BLOCKCHAIR_URL).replace(/\/+$/, "");
|
|
616
|
+
this.key = config.key;
|
|
617
|
+
this.fetchFn = fetchFn;
|
|
618
|
+
this.timeoutMs = timeoutMs;
|
|
619
|
+
}
|
|
620
|
+
async connect() {
|
|
621
|
+
}
|
|
622
|
+
async close() {
|
|
623
|
+
}
|
|
624
|
+
url(path, query = {}) {
|
|
625
|
+
const q = new URLSearchParams(query);
|
|
626
|
+
if (this.key) q.set("key", this.key);
|
|
627
|
+
const s = q.toString();
|
|
628
|
+
return `${this.base}/${path}${s ? `?${s}` : ""}`;
|
|
629
|
+
}
|
|
630
|
+
async request(path, query, init, check) {
|
|
631
|
+
const url = this.url(path, query);
|
|
632
|
+
const res = await fetchWithTimeout(this.fetchFn, url, init, this.timeoutMs, this.base);
|
|
633
|
+
let body;
|
|
634
|
+
try {
|
|
635
|
+
body = await res.json();
|
|
636
|
+
} catch (e) {
|
|
637
|
+
if (res.status === 402 || res.status === 429 || res.status === 430) throw new import_core3.NetworkError(`Blockchair rate limit (${res.status})`, { provider: this.base });
|
|
638
|
+
throw new import_core3.NetworkError(`Blockchair returned non-JSON (${res.status}) for ${url}`, { provider: this.base, cause: e });
|
|
639
|
+
}
|
|
640
|
+
const env = body;
|
|
641
|
+
const code = env.context?.code ?? res.status;
|
|
642
|
+
if (code === 402 || code === 429 || code === 430) throw new import_core3.NetworkError(`Blockchair rate limit (${code})`, { provider: this.base });
|
|
643
|
+
if (code === 404) throw new NotFound();
|
|
644
|
+
if (code !== 200 || !res.ok) throw new import_core3.NetworkError(`Blockchair request failed: ${code} ${env.context?.error ?? res.statusText} (${url})`, { provider: this.base });
|
|
645
|
+
if (!check(env.data)) throw new import_core3.NetworkError(`Blockchair returned an unexpected shape for ${url}`, { provider: this.base });
|
|
646
|
+
return { data: env.data, state: env.context?.state };
|
|
647
|
+
}
|
|
648
|
+
async dashboard(address) {
|
|
649
|
+
const { data, state } = await this.request(`dashboards/address/${address}`, { limit: `0,${UTXO_LIMIT}` }, void 0, (v) => isRecord(v) && Object.values(v).every((a) => isRecord(a) && isRecord(a.address) && typeof a.address.balance === "number" && Array.isArray(a.utxo) && a.utxo.every(isUtxo)));
|
|
650
|
+
const entry = data[address] ?? Object.values(data)[0];
|
|
651
|
+
if (!entry) throw new import_core3.NetworkError(`Blockchair knows no address ${address}`, { provider: this.base });
|
|
652
|
+
return { address: entry.address, utxo: entry.utxo, transactions: entry.transactions ?? [], state };
|
|
653
|
+
}
|
|
654
|
+
/** Transaction hashes of an address, newest first, paged by offset (what the activity history walks). */
|
|
655
|
+
async listTransactions(address, limit, offset) {
|
|
656
|
+
const { data } = await this.request(`dashboards/address/${address}`, { limit: `${limit},0`, offset: `${offset},0` }, void 0, (v) => isRecord(v) && Object.values(v).every((a) => isRecord(a) && isRecord(a.address) && (a.transactions === void 0 || Array.isArray(a.transactions))));
|
|
657
|
+
const entry = data[address] ?? Object.values(data)[0];
|
|
658
|
+
if (!entry) throw new import_core3.NetworkError(`Blockchair knows no address ${address}`, { provider: this.base });
|
|
659
|
+
return { hashes: (entry.transactions ?? []).filter((h2) => typeof h2 === "string"), total: entry.address.transaction_count };
|
|
660
|
+
}
|
|
661
|
+
/** Confirmed = the UTXOs mined so far; unconfirmed = incoming mempool outputs. (Beyond 1000 UTXOs the dashboard balance is used.) */
|
|
662
|
+
async getBalance(address) {
|
|
663
|
+
const d = await this.dashboard(address);
|
|
664
|
+
let confirmed = 0n, unconfirmed = 0n;
|
|
665
|
+
for (const u of d.utxo) {
|
|
666
|
+
if (u.block_id > 0) confirmed += BigInt(u.value);
|
|
667
|
+
else unconfirmed += BigInt(u.value);
|
|
668
|
+
}
|
|
669
|
+
if ((d.address.unspent_output_count ?? d.utxo.length) > d.utxo.length) confirmed = BigInt(d.address.balance) - unconfirmed;
|
|
670
|
+
return { confirmed, unconfirmed };
|
|
671
|
+
}
|
|
672
|
+
async listUnspent(address) {
|
|
673
|
+
const d = await this.dashboard(address);
|
|
674
|
+
return d.utxo.map((u) => ({ txid: u.transaction_hash, vout: u.index, value: BigInt(u.value), height: u.block_id > 0 ? u.block_id : -1 }));
|
|
675
|
+
}
|
|
676
|
+
async getTransaction(txid2) {
|
|
677
|
+
const { data } = await this.request(`raw/transaction/${txid2}`, {}, void 0, (v) => isRecord(v) && Object.values(v).every((t) => isRecord(t) && typeof t.raw_transaction === "string"));
|
|
678
|
+
const hex = (data[txid2] ?? Object.values(data)[0])?.raw_transaction;
|
|
679
|
+
if (!hex) throw new import_core3.NetworkError(`Transaction ${txid2} has no raw data`, { provider: this.base });
|
|
680
|
+
return hex;
|
|
681
|
+
}
|
|
682
|
+
/** Inputs / outputs with addresses, values and scripts — what the activity history and prevout lookups read. */
|
|
683
|
+
async getTransactionDashboard(txid2) {
|
|
684
|
+
try {
|
|
685
|
+
const { data, state } = await this.request(`dashboards/transaction/${txid2}`, {}, void 0, (v) => isRecord(v) && Object.values(v).every((t2) => t2 === null || isDashboard(t2)));
|
|
686
|
+
const t = data[txid2] ?? Object.values(data)[0];
|
|
687
|
+
if (!t) return void 0;
|
|
688
|
+
return { ...t, transaction: { ...t.transaction, ...state !== void 0 ? { confirmations: t.transaction.block_id > 0 ? state - t.transaction.block_id + 1 : 0 } : {} } };
|
|
689
|
+
} catch (e) {
|
|
690
|
+
if (e instanceof NotFound) return void 0;
|
|
691
|
+
throw e;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/** Several transactions in one call (Blockchair takes up to 10 comma-separated hashes). */
|
|
695
|
+
async getTransactionDashboards(txids) {
|
|
696
|
+
const out = /* @__PURE__ */ new Map();
|
|
697
|
+
for (let i = 0; i < txids.length; i += 10) {
|
|
698
|
+
const slice = txids.slice(i, i + 10);
|
|
699
|
+
const { data, state } = await this.request(`dashboards/transactions/${slice.join(",")}`, {}, void 0, (v) => isRecord(v) && Object.values(v).every((t) => t === null || isDashboard(t)));
|
|
700
|
+
for (const [hash, t] of Object.entries(data)) {
|
|
701
|
+
if (!t) continue;
|
|
702
|
+
const confirmations = state !== void 0 && t.transaction.block_id > 0 ? state - t.transaction.block_id + 1 : 0;
|
|
703
|
+
out.set(hash, { ...t, transaction: { ...t.transaction, confirmations } });
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return out;
|
|
707
|
+
}
|
|
708
|
+
async getOutput(txid2, vout) {
|
|
709
|
+
const d = await this.getTransactionDashboard(txid2);
|
|
710
|
+
const o = d?.outputs.find((x) => x.index === vout);
|
|
711
|
+
return o ? { value: BigInt(o.value), script: (0, import_utils.hexToBytes)(o.script_hex) } : void 0;
|
|
712
|
+
}
|
|
713
|
+
async getTransactionInfo(txid2) {
|
|
714
|
+
const d = await this.getTransactionDashboard(txid2);
|
|
715
|
+
if (!d) return void 0;
|
|
716
|
+
const t = d.transaction;
|
|
717
|
+
const info = { txid: t.hash, blockHeight: t.block_id > 0 ? t.block_id : -1, confirmations: t.confirmations ?? 0 };
|
|
718
|
+
if (typeof t.fee === "number") info.fee = BigInt(t.fee);
|
|
719
|
+
const time = blockchairTime(t.time);
|
|
720
|
+
if (time !== void 0) info.blockTime = time;
|
|
721
|
+
return info;
|
|
722
|
+
}
|
|
723
|
+
async broadcast(rawTx) {
|
|
724
|
+
const body = `data=${encodeURIComponent(rawTx)}`;
|
|
725
|
+
const url = this.url("push/transaction");
|
|
726
|
+
const res = await fetchWithTimeout(this.fetchFn, url, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body }, this.timeoutMs, this.base);
|
|
727
|
+
let env = {};
|
|
728
|
+
try {
|
|
729
|
+
env = await res.json();
|
|
730
|
+
} catch {
|
|
731
|
+
}
|
|
732
|
+
const hash = env.data?.transaction_hash;
|
|
733
|
+
if (hash) return hash;
|
|
734
|
+
const reason = env.context?.error ?? `${res.status} ${res.statusText}`;
|
|
735
|
+
throw new Error(`Broadcast rejected: ${reason}`);
|
|
736
|
+
}
|
|
737
|
+
async getBlockHeight() {
|
|
738
|
+
const { data } = await this.request("stats", {}, void 0, (v) => isRecord(v) && isInt(v.best_block_height));
|
|
739
|
+
if (data.best_block_height <= 0) throw new import_core3.NetworkError("Blockchair reported no block height", { provider: this.base });
|
|
740
|
+
return data.best_block_height;
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
var NotFound = class extends Error {
|
|
744
|
+
constructor() {
|
|
745
|
+
super("not found");
|
|
746
|
+
this.name = "NotFound";
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
// src/blockbook.ts
|
|
751
|
+
var import_core4 = require("@anywalletkit/core");
|
|
752
|
+
var ZcashBlockbookClient = class _ZcashBlockbookClient {
|
|
753
|
+
base;
|
|
754
|
+
headers;
|
|
755
|
+
fetchFn;
|
|
756
|
+
timeoutMs;
|
|
757
|
+
constructor(config, fetchFn = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
758
|
+
this.base = config.url.replace(/\/+$/, "").replace(/\/api$/, "");
|
|
759
|
+
this.headers = { accept: "application/json", ...config.headers ?? {} };
|
|
760
|
+
this.fetchFn = fetchFn;
|
|
761
|
+
this.timeoutMs = timeoutMs;
|
|
762
|
+
}
|
|
763
|
+
async connect() {
|
|
764
|
+
}
|
|
765
|
+
async close() {
|
|
766
|
+
}
|
|
767
|
+
async get(path, check) {
|
|
768
|
+
const url = `${this.base}/api${path}`;
|
|
769
|
+
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 });
|
|
771
|
+
let body;
|
|
772
|
+
try {
|
|
773
|
+
body = await res.json();
|
|
774
|
+
} catch (e) {
|
|
775
|
+
throw new import_core4.NetworkError(`Blockbook returned non-JSON for ${url}`, { provider: this.base, cause: e });
|
|
776
|
+
}
|
|
777
|
+
if (!check(body)) throw new import_core4.NetworkError(`Blockbook returned an unexpected shape for ${url}`, { provider: this.base });
|
|
778
|
+
return body;
|
|
779
|
+
}
|
|
780
|
+
async getBalance(address) {
|
|
781
|
+
const data = await this.get(`/v2/address/${address}?details=basic`, (v) => isRecord(v) && isIntString(v.balance) && isIntString(v.unconfirmedBalance));
|
|
782
|
+
return { confirmed: BigInt(data.balance), unconfirmed: BigInt(data.unconfirmedBalance) };
|
|
783
|
+
}
|
|
784
|
+
async listUnspent(address) {
|
|
785
|
+
const data = await this.get(`/v2/utxo/${address}`, (v) => Array.isArray(v) && v.every((u) => isRecord(u) && typeof u.txid === "string" && isInt(u.vout) && isIntString(u.value)));
|
|
786
|
+
return data.map((u) => ({ txid: u.txid, vout: u.vout, value: BigInt(u.value), height: u.height && u.height > 0 ? u.height : -1 }));
|
|
787
|
+
}
|
|
788
|
+
static isTx(v) {
|
|
789
|
+
return isRecord(v) && typeof v.txid === "string" && isInt(v.blockHeight) && isInt(v.confirmations);
|
|
790
|
+
}
|
|
791
|
+
async getTransactionDetails(txid2) {
|
|
792
|
+
try {
|
|
793
|
+
return await this.get(`/v2/tx/${txid2}`, _ZcashBlockbookClient.isTx);
|
|
794
|
+
} catch (e) {
|
|
795
|
+
if (e instanceof import_core4.NetworkError && /\b(400|404)\b/.test(e.message)) return void 0;
|
|
796
|
+
throw e;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async getTransaction(txid2) {
|
|
800
|
+
const tx = await this.get(`/v2/tx/${txid2}`, _ZcashBlockbookClient.isTx);
|
|
801
|
+
if (typeof tx.hex !== "string" || !tx.hex) throw new import_core4.NetworkError(`Transaction ${txid2} has no hex data`, { provider: this.base });
|
|
802
|
+
return tx.hex;
|
|
803
|
+
}
|
|
804
|
+
async getOutput(txid2, vout) {
|
|
805
|
+
const tx = await this.getTransactionDetails(txid2);
|
|
806
|
+
const o = tx?.vout?.find((x) => x.n === vout);
|
|
807
|
+
if (!o || !isIntString(o.value) || typeof o.hex !== "string") return void 0;
|
|
808
|
+
return { value: BigInt(o.value), script: (0, import_utils.hexToBytes)(o.hex) };
|
|
809
|
+
}
|
|
810
|
+
async getTransactionInfo(txid2) {
|
|
811
|
+
const tx = await this.getTransactionDetails(txid2);
|
|
812
|
+
if (!tx) return void 0;
|
|
813
|
+
const info = { txid: tx.txid, blockHeight: tx.blockHeight > 0 ? tx.blockHeight : -1, confirmations: tx.confirmations };
|
|
814
|
+
if (isIntString(tx.fees)) info.fee = BigInt(tx.fees);
|
|
815
|
+
if (typeof tx.blockTime === "number") info.blockTime = tx.blockTime;
|
|
816
|
+
return info;
|
|
817
|
+
}
|
|
818
|
+
async broadcast(rawTx) {
|
|
819
|
+
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"}`);
|
|
821
|
+
if (!data.result) throw new import_core4.NetworkError("Blockbook returned no txid", { provider: this.base });
|
|
822
|
+
return data.result;
|
|
823
|
+
}
|
|
824
|
+
async getBlockHeight() {
|
|
825
|
+
const data = await this.get("/v2", (v) => isRecord(v));
|
|
826
|
+
const h2 = data.blockbook?.bestHeight;
|
|
827
|
+
if (typeof h2 !== "number" || h2 <= 0) throw new import_core4.NetworkError("Blockbook did not report a block height", { provider: this.base });
|
|
828
|
+
return h2;
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
// src/client.ts
|
|
833
|
+
var import_core5 = require("@anywalletkit/core");
|
|
834
|
+
var isClient = (c) => typeof c === "object" && c !== null && typeof c.listUnspent === "function";
|
|
835
|
+
function toClient(c, fetchFn, timeoutMs) {
|
|
836
|
+
if (isClient(c)) return c;
|
|
837
|
+
switch (c.type) {
|
|
838
|
+
case "blockchair":
|
|
839
|
+
return new BlockchairClient(c.clientConfig ?? {}, fetchFn, timeoutMs);
|
|
840
|
+
case "blockbook-http":
|
|
841
|
+
return new ZcashBlockbookClient(c.clientConfig, fetchFn, timeoutMs);
|
|
842
|
+
default:
|
|
843
|
+
throw new Error(`Unknown Zcash client type: ${String(c.type)}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
var METHODS = ["connect", "close", "getBalance", "listUnspent", "getTransaction", "getOutput", "getTransactionInfo", "broadcast", "getBlockHeight"];
|
|
847
|
+
function createClient(config) {
|
|
848
|
+
if (config.client === void 0) return void 0;
|
|
849
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
850
|
+
const list = (Array.isArray(config.client) ? config.client : [config.client]).map((c) => toClient(c, config.fetch, timeoutMs));
|
|
851
|
+
if (list.length === 0) return void 0;
|
|
852
|
+
if (list.length === 1) return list[0];
|
|
853
|
+
const attempts = list.length + (config.retries ?? 3);
|
|
854
|
+
const call = (method) => async (...args) => {
|
|
855
|
+
let last;
|
|
856
|
+
for (let i = 0; i < attempts; i++) {
|
|
857
|
+
const c = list[i % list.length];
|
|
858
|
+
try {
|
|
859
|
+
return await c[method].apply(c, args);
|
|
860
|
+
} catch (e) {
|
|
861
|
+
if (method === "broadcast" && !(e instanceof import_core5.NetworkError)) throw e;
|
|
862
|
+
last = e;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
throw new import_core5.NetworkError(`All Zcash clients failed for ${method}`, { cause: last });
|
|
866
|
+
};
|
|
867
|
+
const out = {};
|
|
868
|
+
for (const m of METHODS) out[m] = call(m);
|
|
869
|
+
return out;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/message.ts
|
|
873
|
+
var import_sha22 = require("@noble/hashes/sha2.js");
|
|
874
|
+
var import_secp256k1 = require("@noble/curves/secp256k1.js");
|
|
875
|
+
var import_base2 = require("@scure/base");
|
|
876
|
+
var MAGIC = "Zcash Signed Message:\n";
|
|
877
|
+
var PREFIX2 = (0, import_utils.utf8ToBytes)(MAGIC);
|
|
878
|
+
function magicHash(message) {
|
|
879
|
+
const msg = typeof message === "string" ? (0, import_utils.utf8ToBytes)(message) : message;
|
|
880
|
+
const p = compactSize(PREFIX2.length), l = compactSize(msg.length);
|
|
881
|
+
const buf = new Uint8Array(p.length + PREFIX2.length + l.length + msg.length);
|
|
882
|
+
let o = 0;
|
|
883
|
+
for (const part of [p, PREFIX2, l, msg]) {
|
|
884
|
+
buf.set(part, o);
|
|
885
|
+
o += part.length;
|
|
886
|
+
}
|
|
887
|
+
return (0, import_sha22.sha256)((0, import_sha22.sha256)(buf));
|
|
888
|
+
}
|
|
889
|
+
function encodeMessageSignature(rs, recovery) {
|
|
890
|
+
if (rs.length !== 64 || recovery > 3) throw new Error("Expected a 64-byte r\u2016s signature and a recovery id 0..3.");
|
|
891
|
+
const out = new Uint8Array(65);
|
|
892
|
+
out[0] = 27 + 4 + recovery;
|
|
893
|
+
out.set(rs, 1);
|
|
894
|
+
return import_base2.base64.encode(out);
|
|
895
|
+
}
|
|
896
|
+
async function signMessageWith(message, signer) {
|
|
897
|
+
const sig = await signer.sign(magicHash(message), { scheme: "ecdsa-recoverable", context: "zcash-message" });
|
|
898
|
+
return encodeMessageSignature(sig.subarray(0, 64), sig[64]);
|
|
899
|
+
}
|
|
900
|
+
function recoverMessagePublicKey(message, signature) {
|
|
901
|
+
let bytes;
|
|
902
|
+
try {
|
|
903
|
+
bytes = import_base2.base64.decode(signature);
|
|
904
|
+
} catch {
|
|
905
|
+
return void 0;
|
|
906
|
+
}
|
|
907
|
+
if (bytes.length !== 65) return void 0;
|
|
908
|
+
const flag = bytes[0] - 27;
|
|
909
|
+
if (flag < 0 || flag > 15) return void 0;
|
|
910
|
+
const recovered = new Uint8Array(65);
|
|
911
|
+
recovered[0] = flag & 3;
|
|
912
|
+
recovered.set(bytes.subarray(1), 1);
|
|
913
|
+
try {
|
|
914
|
+
const pub = import_secp256k1.secp256k1.recoverPublicKey(recovered, magicHash(message), { prehash: false });
|
|
915
|
+
return pub.length === 33 ? pub : import_secp256k1.secp256k1.Point.fromBytes(pub).toBytes(true);
|
|
916
|
+
} catch {
|
|
917
|
+
return void 0;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/manager.ts
|
|
922
|
+
var import_core8 = require("@anywalletkit/core");
|
|
923
|
+
|
|
924
|
+
// src/account.ts
|
|
925
|
+
var import_core7 = require("@anywalletkit/core");
|
|
926
|
+
var import_secp256k12 = require("@noble/curves/secp256k1.js");
|
|
927
|
+
|
|
928
|
+
// src/read-only-account.ts
|
|
929
|
+
var import_core6 = require("@anywalletkit/core");
|
|
930
|
+
var toBig = (v) => typeof v === "bigint" ? v : BigInt(typeof v === "string" ? v.trim() : Math.round(v));
|
|
931
|
+
var WalletAccountReadOnlyZcash = class {
|
|
932
|
+
/** The transparent address. */
|
|
933
|
+
address;
|
|
934
|
+
chain;
|
|
935
|
+
/** The address's scriptPubKey. */
|
|
936
|
+
scriptPubKey;
|
|
937
|
+
/** Script type of the address. */
|
|
938
|
+
scriptKind;
|
|
939
|
+
_config;
|
|
940
|
+
_client;
|
|
941
|
+
_network;
|
|
942
|
+
constructor(address, config = {}) {
|
|
943
|
+
this._config = config;
|
|
944
|
+
this._network = config.network ?? "mainnet";
|
|
945
|
+
const decoded = decodeAddress(address, this._network);
|
|
946
|
+
this.address = address.trim();
|
|
947
|
+
this.scriptPubKey = decoded.script;
|
|
948
|
+
this.scriptKind = decoded.kind;
|
|
949
|
+
this.chain = CAIP2[this._network];
|
|
950
|
+
this._client = createClient(config);
|
|
951
|
+
}
|
|
952
|
+
requireClient(what) {
|
|
953
|
+
if (!this._client) throw new import_core6.NetworkError(`The wallet must be connected to a client to ${what}.`);
|
|
954
|
+
return this._client;
|
|
955
|
+
}
|
|
956
|
+
/** Fee ceiling in zatoshis: `ZcashConfig.maxFee` or the default. */
|
|
957
|
+
get maxFee() {
|
|
958
|
+
return this._config.maxFee !== void 0 ? toBig(this._config.maxFee) : DEFAULT_MAX_FEE;
|
|
959
|
+
}
|
|
960
|
+
/** The transparent address. */
|
|
961
|
+
async getAddress() {
|
|
962
|
+
return this.address;
|
|
963
|
+
}
|
|
964
|
+
/** Confirmed plus incoming mempool value (the transports do not expose pending spends of our own outputs). */
|
|
965
|
+
async getBalance() {
|
|
966
|
+
const { confirmed, unconfirmed } = await this.requireClient("read balances").getBalance(this.address);
|
|
967
|
+
return confirmed + unconfirmed;
|
|
968
|
+
}
|
|
969
|
+
/** Always throws: Zcash has no tokens. */
|
|
970
|
+
async getTokenBalance(_token) {
|
|
971
|
+
throw new Error("The 'getTokenBalance' method is not supported on the zcash blockchain.");
|
|
972
|
+
}
|
|
973
|
+
/** Coin selection for a spend (ZIP-317 fee). Validates the recipient and the dust floor first. */
|
|
974
|
+
async planSpend(spend) {
|
|
975
|
+
if (this.scriptKind !== "p2pkh") throw new Error(`Cannot build spends from a ${this.scriptKind} address; only P2PKH (t1\u2026) accounts spend.`);
|
|
976
|
+
const amount = toBig(spend.value);
|
|
977
|
+
const recipient = decodeAddress(spend.to, this._network);
|
|
978
|
+
if (amount < DUST_LIMIT) throw new Error(`The amount must be at least the dust limit (${DUST_LIMIT} zatoshis).`);
|
|
979
|
+
const utxos = await this.requireClient("plan a spend").listUnspent(this.address);
|
|
980
|
+
if (utxos.length === 0) throw new import_core6.InsufficientFundsError(amount, 0n, "ZEC");
|
|
981
|
+
const selection = selectCoins({
|
|
982
|
+
utxos,
|
|
983
|
+
target: amount,
|
|
984
|
+
recipientOutputSize: outputSize(recipient.script),
|
|
985
|
+
changeOutputSize: outputSize(this.scriptPubKey),
|
|
986
|
+
maxInputs: this._config.maxInputs ?? MAX_UTXO_INPUTS,
|
|
987
|
+
...spend.fee !== void 0 ? { fee: toBig(spend.fee) } : {}
|
|
988
|
+
});
|
|
989
|
+
return { ...selection, amount, recipient };
|
|
990
|
+
}
|
|
991
|
+
/** ZIP-317 fee of a spend in zatoshis. */
|
|
992
|
+
async quoteSendTransaction(tx) {
|
|
993
|
+
const { fee } = await this.planSpend(tx);
|
|
994
|
+
return { fee };
|
|
995
|
+
}
|
|
996
|
+
/** ZIP-317 fee of a transfer in zatoshis. */
|
|
997
|
+
async quoteTransfer(o) {
|
|
998
|
+
if (o.token) throw new Error("Tokens are not supported on the zcash blockchain.");
|
|
999
|
+
return this.quoteSendTransaction({ to: o.recipient, value: o.amount });
|
|
1000
|
+
}
|
|
1001
|
+
/** Everything spendable in ONE transaction to a single output, no change. */
|
|
1002
|
+
async getMaxSpendable(opts = {}) {
|
|
1003
|
+
const utxos = await this.requireClient("plan a spend").listUnspent(this.address);
|
|
1004
|
+
const outScript = opts.to ? decodeAddress(opts.to, this._network).script : this.scriptPubKey;
|
|
1005
|
+
const sweep = selectAll({ utxos, outputSize: outputSize(outScript), maxInputs: this._config.maxInputs ?? MAX_UTXO_INPUTS, ...opts.fee !== void 0 ? { fee: toBig(opts.fee) } : {} });
|
|
1006
|
+
const amount = sweep.amount >= DUST_LIMIT ? sweep.amount : 0n;
|
|
1007
|
+
return { amount, fee: amount > 0n ? sweep.fee : 0n, changeValue: 0n, inputs: amount > 0n ? sweep.utxos.length : 0 };
|
|
1008
|
+
}
|
|
1009
|
+
/** Undefined until the transaction is mined. */
|
|
1010
|
+
async getTransactionReceipt(hash) {
|
|
1011
|
+
if (!/^[0-9a-fA-F]{64}$/.test(hash)) throw new Error("The 'getTransactionReceipt(hash)' method requires a valid transaction hash.");
|
|
1012
|
+
const info = await this.requireClient("fetch transaction receipts").getTransactionInfo(hash);
|
|
1013
|
+
if (!info || info.blockHeight <= 0) return void 0;
|
|
1014
|
+
return { hash, status: 1, blockNumber: BigInt(info.blockHeight), confirmations: info.confirmations, ...info.fee !== void 0 ? { fee: info.fee } : {} };
|
|
1015
|
+
}
|
|
1016
|
+
/** Recover the key from a Zcash message signature and compare its P2PKH address with ours. */
|
|
1017
|
+
async verify(message, signature) {
|
|
1018
|
+
const pub = recoverMessagePublicKey(message, signature);
|
|
1019
|
+
if (!pub) return false;
|
|
1020
|
+
try {
|
|
1021
|
+
return addressFromPublicKey(pub, this._network) === this.address;
|
|
1022
|
+
} catch {
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
/** Chain tip for lock time / expiry; undefined when the transport cannot tell. */
|
|
1027
|
+
async tipHeight() {
|
|
1028
|
+
if (!this._client) return void 0;
|
|
1029
|
+
try {
|
|
1030
|
+
return await this._client.getBlockHeight();
|
|
1031
|
+
} catch {
|
|
1032
|
+
return void 0;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
// src/account.ts
|
|
1038
|
+
function addressFor(publicKey, network = "mainnet") {
|
|
1039
|
+
return addressFromPublicKey(publicKey, network);
|
|
1040
|
+
}
|
|
1041
|
+
var DEFAULT_EXPIRY_DELTA = 40;
|
|
1042
|
+
var WalletAccountZcash = class _WalletAccountZcash extends WalletAccountReadOnlyZcash {
|
|
1043
|
+
/** UTXOs and the tip height come from the network before signing. */
|
|
1044
|
+
needsNetwork = true;
|
|
1045
|
+
/** Derivation path, or `external` for an external signer. */
|
|
1046
|
+
path;
|
|
1047
|
+
/** Compressed secp256k1 public key. */
|
|
1048
|
+
publicKey;
|
|
1049
|
+
/** The signer that holds the key. */
|
|
1050
|
+
signer;
|
|
1051
|
+
_disposed = false;
|
|
1052
|
+
_readOnly;
|
|
1053
|
+
/** `publicKey` is the signer's compressed secp256k1 key (33 bytes); it is needed synchronously for the address. */
|
|
1054
|
+
constructor(signer, publicKey, path, config = {}) {
|
|
1055
|
+
super(addressFromPublicKey(publicKey, config.network ?? "mainnet"), config);
|
|
1056
|
+
this.signer = (0, import_core7.requireCurve)(signer, "secp256k1");
|
|
1057
|
+
if (!this.signer.schemes.includes("ecdsa")) throw new import_core7.SignerError(`A Zcash account needs a signer with the 'ecdsa' scheme (has: ${this.signer.schemes.join(", ")}).`);
|
|
1058
|
+
this.publicKey = publicKey;
|
|
1059
|
+
this.path = path;
|
|
1060
|
+
}
|
|
1061
|
+
/** Back-compat: wrap a raw derived key in a {@link LocalSigner} (the account takes ownership; it cannot derive). */
|
|
1062
|
+
static fromPrivateKey(privateKey, publicKey, path, config = {}) {
|
|
1063
|
+
return new _WalletAccountZcash(import_core7.LocalSigner.fromPrivateKey("secp256k1", privateKey, publicKey, path), publicKey, path, config);
|
|
1064
|
+
}
|
|
1065
|
+
/** Account index for the standard m/44'/133'/0'/0/i path. */
|
|
1066
|
+
get index() {
|
|
1067
|
+
const m = /^m\/44'\/(133|1)'\/0'\/0\/(\d+)$/.exec(this.path);
|
|
1068
|
+
return m ? Number(m[2]) : void 0;
|
|
1069
|
+
}
|
|
1070
|
+
live() {
|
|
1071
|
+
if (this._disposed) throw new import_core7.SignerError("This account has been disposed.");
|
|
1072
|
+
}
|
|
1073
|
+
/** Zcash message signature (zcashd `signmessage` encoding), base64. */
|
|
1074
|
+
async sign(message) {
|
|
1075
|
+
this.live();
|
|
1076
|
+
return signMessageWith(message, this.signer);
|
|
1077
|
+
}
|
|
1078
|
+
ctx(intended) {
|
|
1079
|
+
return { network: this._network, ownAddress: this.address, intended, maxFee: this.maxFee };
|
|
1080
|
+
}
|
|
1081
|
+
/** Lock time + expiry + branch id from the tip (or the config override). */
|
|
1082
|
+
async chainParams() {
|
|
1083
|
+
const tip = await this.tipHeight();
|
|
1084
|
+
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;
|
|
1088
|
+
return { lockTime, expiryHeight, consensusBranchId };
|
|
1089
|
+
}
|
|
1090
|
+
/** Sign every input through the signer; the scriptSig is `<DER ‖ 0x01> <pubkey>`. */
|
|
1091
|
+
async signInputs(tx, spent) {
|
|
1092
|
+
for (let i = 0; i < tx.inputs.length; i++) {
|
|
1093
|
+
const digest = sighashAll(tx, i, spent);
|
|
1094
|
+
const rs = await this.signer.sign(digest, { scheme: "ecdsa", context: "zcash-sighash" });
|
|
1095
|
+
const der = import_secp256k12.secp256k1.Signature.fromBytes(rs, "compact").toBytes("der");
|
|
1096
|
+
tx.inputs[i].scriptSig = p2pkhScriptSig(der, this.publicKey);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
async buildSignedTransaction(spend) {
|
|
1100
|
+
this.live();
|
|
1101
|
+
const plan = await this.planSpend(spend);
|
|
1102
|
+
const params = await this.chainParams();
|
|
1103
|
+
const tx = {
|
|
1104
|
+
...params,
|
|
1105
|
+
inputs: plan.utxos.map((u) => ({ txid: u.txid, vout: u.vout, scriptSig: new Uint8Array(0), sequence: FINAL_SEQUENCE })),
|
|
1106
|
+
outputs: [{ value: plan.amount, script: plan.recipient.script }, ...plan.change > 0n ? [{ value: plan.change, script: this.scriptPubKey }] : []]
|
|
1107
|
+
};
|
|
1108
|
+
const spent = plan.utxos.map((u) => ({ value: u.value, script: this.scriptPubKey }));
|
|
1109
|
+
const have = plan.utxos.reduce((s, u) => s + u.value, 0n);
|
|
1110
|
+
if (have < plan.amount + plan.fee) throw new import_core7.InsufficientFundsError(plan.amount + plan.fee, have, "ZEC");
|
|
1111
|
+
await this.signInputs(tx, spent);
|
|
1112
|
+
const ctx = this.ctx({ to: spend.to, value: plan.amount });
|
|
1113
|
+
const desc = describeTransaction(tx, spent, ctx);
|
|
1114
|
+
assertSafe(desc, ctx);
|
|
1115
|
+
const bytes = serializeTx(tx);
|
|
1116
|
+
return { tx, hex: (0, import_utils.bytesToHex)(bytes), txid: txid(tx), fee: plan.fee, plan, desc };
|
|
1117
|
+
}
|
|
1118
|
+
checkMax(fee, limit, op) {
|
|
1119
|
+
if (limit !== void 0 && fee > BigInt(limit)) throw new Error(`Exceeded maximum fee cost for ${op} operation.`);
|
|
1120
|
+
}
|
|
1121
|
+
/** The outputs a raw transaction spends, fetched through the client (the wire format carries no values). */
|
|
1122
|
+
async spentOutputs(tx) {
|
|
1123
|
+
const client = this.requireClient("inspect a raw transaction");
|
|
1124
|
+
const out = [];
|
|
1125
|
+
for (const i of tx.inputs) {
|
|
1126
|
+
const o = await client.getOutput(i.txid, i.vout);
|
|
1127
|
+
if (!o) throw new Error(`Prevout ${i.txid}:${i.vout} not found.`);
|
|
1128
|
+
out.push(o);
|
|
1129
|
+
}
|
|
1130
|
+
return out;
|
|
1131
|
+
}
|
|
1132
|
+
/** Describe a raw signed tx (needs the network for prevouts) and run the safety checks. */
|
|
1133
|
+
async describeRaw(rawHex) {
|
|
1134
|
+
const tx = parseTx(rawHex);
|
|
1135
|
+
const ctx = this.ctx();
|
|
1136
|
+
const desc = describeTransaction(tx, await this.spentOutputs(tx), ctx);
|
|
1137
|
+
assertSafe(desc, ctx);
|
|
1138
|
+
return { tx, desc };
|
|
1139
|
+
}
|
|
1140
|
+
/** "What you sign": the fully resolved spend (signed, NOT broadcast) or a caller's raw hex. */
|
|
1141
|
+
async describeTransaction(tx) {
|
|
1142
|
+
if (typeof tx === "string") return (await this.describeRaw(tx)).desc;
|
|
1143
|
+
return (await this.buildSignedTransaction(tx)).desc;
|
|
1144
|
+
}
|
|
1145
|
+
/** Sign without broadcasting. Returns the raw transaction hex. Raw hex input is returned unchanged. */
|
|
1146
|
+
async signTransaction(tx) {
|
|
1147
|
+
this.live();
|
|
1148
|
+
if (typeof tx === "string") return tx;
|
|
1149
|
+
const built = await this.buildSignedTransaction(tx);
|
|
1150
|
+
this.checkMax(built.fee, this._config.transactionMaxFee, "transaction");
|
|
1151
|
+
return built.hex;
|
|
1152
|
+
}
|
|
1153
|
+
async quoteSendTransaction(tx) {
|
|
1154
|
+
if (typeof tx === "string") return { fee: (await this.describeRaw(tx)).desc.fee };
|
|
1155
|
+
return super.quoteSendTransaction(tx);
|
|
1156
|
+
}
|
|
1157
|
+
async sendTransaction(tx) {
|
|
1158
|
+
this.live();
|
|
1159
|
+
const client = this.requireClient("send transactions");
|
|
1160
|
+
let rawHex, hash, fee;
|
|
1161
|
+
if (typeof tx === "string") {
|
|
1162
|
+
const { tx: parsed, desc } = await this.describeRaw(tx);
|
|
1163
|
+
rawHex = tx;
|
|
1164
|
+
hash = txid(parsed);
|
|
1165
|
+
fee = desc.fee;
|
|
1166
|
+
} else {
|
|
1167
|
+
const built = await this.buildSignedTransaction(tx);
|
|
1168
|
+
rawHex = built.hex;
|
|
1169
|
+
hash = built.txid;
|
|
1170
|
+
fee = built.fee;
|
|
1171
|
+
}
|
|
1172
|
+
this.checkMax(fee, this._config.transactionMaxFee, "transaction");
|
|
1173
|
+
const id = await client.broadcast(rawHex);
|
|
1174
|
+
return { hash: id || hash, fee };
|
|
1175
|
+
}
|
|
1176
|
+
/** Native ZEC only (`token` is rejected). Subject to `transferMaxFee`. */
|
|
1177
|
+
async transfer(o) {
|
|
1178
|
+
if (o.token) throw new Error("Tokens are not supported on the zcash blockchain.");
|
|
1179
|
+
this.live();
|
|
1180
|
+
const client = this.requireClient("transfer");
|
|
1181
|
+
const built = await this.buildSignedTransaction({ to: o.recipient, value: o.amount });
|
|
1182
|
+
this.checkMax(built.fee, this._config.transferMaxFee, "transfer");
|
|
1183
|
+
const id = await client.broadcast(built.hex);
|
|
1184
|
+
return { hash: id || built.txid, fee: built.fee };
|
|
1185
|
+
}
|
|
1186
|
+
/** The same address as a {@link WalletAccountReadOnlyZcash}. */
|
|
1187
|
+
toReadOnlyAccount() {
|
|
1188
|
+
this._readOnly ??= new WalletAccountReadOnlyZcash(this.address, this._config);
|
|
1189
|
+
return this._readOnly;
|
|
1190
|
+
}
|
|
1191
|
+
dispose() {
|
|
1192
|
+
this._disposed = true;
|
|
1193
|
+
this.signer.dispose();
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
// src/manager.ts
|
|
1198
|
+
var coinType = (network) => network === "mainnet" ? 133 : 1;
|
|
1199
|
+
var WalletManagerZcash = class extends import_core8.WalletManager {
|
|
1200
|
+
constructor(seedOrSigner, config = {}) {
|
|
1201
|
+
super(seedOrSigner, config);
|
|
1202
|
+
}
|
|
1203
|
+
/** `m/44'/133'` on mainnet (coin type 1 on testnet); account `i` is at `basePath/0'/0/i`. */
|
|
1204
|
+
get basePath() {
|
|
1205
|
+
return `m/44'/${coinType(this._config.network ?? "mainnet")}'`;
|
|
1206
|
+
}
|
|
1207
|
+
async getAccount(index = 0) {
|
|
1208
|
+
return this.getAccountByPath(`${this.basePath}/0'/0/${index}`);
|
|
1209
|
+
}
|
|
1210
|
+
async getAccountByPath(path) {
|
|
1211
|
+
const full = path.startsWith("m/") ? path : `${this.basePath}/${path}`;
|
|
1212
|
+
const cached = this._accounts.get(full);
|
|
1213
|
+
if (cached) return cached;
|
|
1214
|
+
const external = this.externalSigner();
|
|
1215
|
+
if (external) return this.fromSigner(external, full);
|
|
1216
|
+
const signer = import_core8.LocalSigner.leafFromSeed(this.seedBytes(), "secp256k1", full);
|
|
1217
|
+
const account = new WalletAccountZcash(signer, signer.publicKey, full, this._config);
|
|
1218
|
+
this._accounts.set(full, account);
|
|
1219
|
+
return account;
|
|
1220
|
+
}
|
|
1221
|
+
async fromSigner(signer, path) {
|
|
1222
|
+
this.requireSigner("secp256k1");
|
|
1223
|
+
let leaf = signer;
|
|
1224
|
+
let key = "external";
|
|
1225
|
+
if ((0, import_core8.isSeedSigner)(signer)) {
|
|
1226
|
+
if (path === signer.path) key = path;
|
|
1227
|
+
else if (path.startsWith(`${signer.path}/`)) {
|
|
1228
|
+
leaf = await (signer.deriveLeaf ?? signer.derive).call(signer, path.slice(signer.path.length + 1));
|
|
1229
|
+
key = path;
|
|
1230
|
+
} else throw new import_core8.SignerError(`The injected signer sits at ${signer.path}; it cannot provide ${path}.`);
|
|
1231
|
+
} else if (path !== `${this.basePath}/0'/0/0` && path !== "external") {
|
|
1232
|
+
throw new import_core8.SignerError(`An external signer has one account; ${path} is not derivable from it.`);
|
|
1233
|
+
}
|
|
1234
|
+
const cached = this._accounts.get(key);
|
|
1235
|
+
if (cached) return cached;
|
|
1236
|
+
const account = new WalletAccountZcash(leaf, await leaf.getPublicKey(), key, this._config);
|
|
1237
|
+
this._accounts.set(key, account);
|
|
1238
|
+
return account;
|
|
1239
|
+
}
|
|
1240
|
+
/** A read-only account for any transparent address, with this manager's config. */
|
|
1241
|
+
getReadOnlyAccount(address) {
|
|
1242
|
+
return new WalletAccountReadOnlyZcash(address, this._config);
|
|
1243
|
+
}
|
|
1244
|
+
/** ZIP-317 has no fee market: every tier is the 5000-zatoshi marginal fee per logical action. */
|
|
1245
|
+
async getFeeRates() {
|
|
1246
|
+
return { slow: MARGINAL_FEE, normal: MARGINAL_FEE, fast: MARGINAL_FEE };
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
// src/history.ts
|
|
1251
|
+
var HistoryUnavailableError = class extends Error {
|
|
1252
|
+
chain;
|
|
1253
|
+
reason;
|
|
1254
|
+
constructor(chain, reason) {
|
|
1255
|
+
super(`History unavailable for ${chain}: ${reason}`);
|
|
1256
|
+
this.name = "HistoryUnavailableError";
|
|
1257
|
+
this.chain = chain;
|
|
1258
|
+
this.reason = reason;
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
var DEFAULT_LIMIT = 25;
|
|
1262
|
+
var MAX_PAGE_SIZE = 100;
|
|
1263
|
+
function encodeCursor(c) {
|
|
1264
|
+
return `${c.v}:${c.o}:${c.s}`;
|
|
1265
|
+
}
|
|
1266
|
+
function decodeCursor(s) {
|
|
1267
|
+
const m = /^1:(\d+):(\d+)$/.exec(s);
|
|
1268
|
+
if (!m) throw new Error("@anywalletkit/wallet-zcash: malformed history cursor");
|
|
1269
|
+
const o = Number(m[1]), size = Number(m[2]);
|
|
1270
|
+
if (size < 1) throw new Error("@anywalletkit/wallet-zcash: malformed history cursor");
|
|
1271
|
+
return { v: 1, o, s: size };
|
|
1272
|
+
}
|
|
1273
|
+
function mapDashboardTx(chain, address, d) {
|
|
1274
|
+
let ownIn = 0n, ownOut = 0n, sentToOthers = 0n;
|
|
1275
|
+
for (const i of d.inputs) if (i.recipient === address) ownIn += BigInt(i.value);
|
|
1276
|
+
for (const o of d.outputs) {
|
|
1277
|
+
if (o.recipient === address) ownOut += BigInt(o.value);
|
|
1278
|
+
else sentToOthers += BigInt(o.value);
|
|
1279
|
+
}
|
|
1280
|
+
const firstInput = d.inputs.find((i) => i.recipient)?.recipient;
|
|
1281
|
+
const firstOther = d.outputs.find((o) => o.recipient && o.recipient !== address)?.recipient;
|
|
1282
|
+
const transfers = [];
|
|
1283
|
+
let from, to, nativeValue;
|
|
1284
|
+
if (ownIn > 0n) {
|
|
1285
|
+
from = address;
|
|
1286
|
+
to = firstOther ?? address;
|
|
1287
|
+
nativeValue = firstOther ? sentToOthers : ownOut;
|
|
1288
|
+
if (nativeValue > 0n) transfers.push({ token: null, from: address, to, amount: nativeValue });
|
|
1289
|
+
} else {
|
|
1290
|
+
from = firstInput ?? (d.inputs.length === 0 ? "shielded" : "coinbase");
|
|
1291
|
+
to = address;
|
|
1292
|
+
nativeValue = ownOut;
|
|
1293
|
+
if (nativeValue > 0n) transfers.push({ token: null, from, to: address, amount: nativeValue });
|
|
1294
|
+
}
|
|
1295
|
+
const t = d.transaction;
|
|
1296
|
+
const confirmed = t.block_id > 0;
|
|
1297
|
+
const out = {
|
|
1298
|
+
chain,
|
|
1299
|
+
hash: t.hash,
|
|
1300
|
+
time: blockchairTime(t.time) ?? 0,
|
|
1301
|
+
status: confirmed ? "confirmed" : "pending",
|
|
1302
|
+
address,
|
|
1303
|
+
from,
|
|
1304
|
+
to,
|
|
1305
|
+
nativeValue,
|
|
1306
|
+
transfers,
|
|
1307
|
+
raw: { tx: d, ownIn, ownOut }
|
|
1308
|
+
};
|
|
1309
|
+
if (typeof t.fee === "number") out.fee = BigInt(t.fee);
|
|
1310
|
+
if (confirmed) out.block = BigInt(t.block_id);
|
|
1311
|
+
return out;
|
|
1312
|
+
}
|
|
1313
|
+
function zcashExplorerTxUrl(hash) {
|
|
1314
|
+
return `https://blockchair.com/zcash/transaction/${hash}`;
|
|
1315
|
+
}
|
|
1316
|
+
function createZcashHistory(options = {}) {
|
|
1317
|
+
const chain = CAIP2.mainnet;
|
|
1318
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
1319
|
+
if (typeof fetchFn !== "function") throw new Error("@anywalletkit/wallet-zcash: no global fetch; pass `fetch` in the options");
|
|
1320
|
+
const client = new BlockchairClient({ ...options.blockchairUrl ? { url: options.blockchairUrl } : {}, ...options.key ? { key: options.key } : {} }, fetchFn, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
1321
|
+
const wrap = async (run) => {
|
|
1322
|
+
try {
|
|
1323
|
+
return await run();
|
|
1324
|
+
} catch (e) {
|
|
1325
|
+
throw new HistoryUnavailableError(chain, e instanceof Error ? e.message : String(e));
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
return {
|
|
1329
|
+
chain,
|
|
1330
|
+
async listActivity(address, query = {}) {
|
|
1331
|
+
const cursor = query.cursor ? decodeCursor(query.cursor) : void 0;
|
|
1332
|
+
const pageSize = cursor?.s ?? Math.min(MAX_PAGE_SIZE, Math.max(1, Math.floor(options.pageSize ?? query.limit ?? DEFAULT_LIMIT)));
|
|
1333
|
+
const offset = cursor?.o ?? 0;
|
|
1334
|
+
const { hashes, total } = await wrap(() => client.listTransactions(address, pageSize, offset));
|
|
1335
|
+
const dashboards = hashes.length ? await wrap(() => client.getTransactionDashboards(hashes)) : /* @__PURE__ */ new Map();
|
|
1336
|
+
let items = hashes.map((h2) => dashboards.get(h2)).filter((d) => !!d).map((d) => mapDashboardTx(chain, address, d));
|
|
1337
|
+
items.sort((a, b) => (b.block === void 0 ? 1 : a.block === void 0 ? -1 : Number(b.block - a.block)) || b.time - a.time);
|
|
1338
|
+
let more = total !== void 0 ? offset + hashes.length < total : hashes.length === pageSize;
|
|
1339
|
+
if (query.since !== void 0) {
|
|
1340
|
+
const since = query.since;
|
|
1341
|
+
if (items.some((t) => t.status === "confirmed" && t.time < since)) more = false;
|
|
1342
|
+
items = items.filter((t) => t.status !== "confirmed" || t.time >= since);
|
|
1343
|
+
}
|
|
1344
|
+
const page = { items, offset, ...total !== void 0 ? { total } : {} };
|
|
1345
|
+
if (more) page.cursor = encodeCursor({ v: 1, o: offset + hashes.length, s: pageSize });
|
|
1346
|
+
return page;
|
|
1347
|
+
},
|
|
1348
|
+
async getTransaction(hash, address) {
|
|
1349
|
+
const d = await wrap(() => client.getTransactionDashboard(hash));
|
|
1350
|
+
return d ? mapDashboardTx(chain, address, d) : void 0;
|
|
1351
|
+
},
|
|
1352
|
+
explorerTxUrl: zcashExplorerTxUrl
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
//# sourceMappingURL=index.cjs.map
|