@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.js
ADDED
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BLOCKCHAIR_URL,
|
|
3
|
+
BlockchairClient,
|
|
4
|
+
CAIP2,
|
|
5
|
+
DEFAULT_TIMEOUT_MS,
|
|
6
|
+
HistoryUnavailableError,
|
|
7
|
+
Reader,
|
|
8
|
+
Writer,
|
|
9
|
+
blockchairTime,
|
|
10
|
+
bytesToHex,
|
|
11
|
+
compactSize,
|
|
12
|
+
createZcashHistory,
|
|
13
|
+
decodeCursor,
|
|
14
|
+
encodeCursor,
|
|
15
|
+
fetchWithTimeout,
|
|
16
|
+
hexToBytes,
|
|
17
|
+
isInt,
|
|
18
|
+
isIntString,
|
|
19
|
+
isRecord,
|
|
20
|
+
mapDashboardTx,
|
|
21
|
+
reverseBytes,
|
|
22
|
+
u32le,
|
|
23
|
+
utf8ToBytes,
|
|
24
|
+
zcashExplorerTxUrl
|
|
25
|
+
} from "./chunk-GJBPCXQK.js";
|
|
26
|
+
|
|
27
|
+
// src/address.ts
|
|
28
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
29
|
+
import { ripemd160 } from "@noble/hashes/legacy.js";
|
|
30
|
+
import { base58check } from "@scure/base";
|
|
31
|
+
var b58 = base58check(sha256);
|
|
32
|
+
var PREFIX = {
|
|
33
|
+
mainnet: { p2pkh: [28, 184], p2sh: [28, 189] },
|
|
34
|
+
testnet: { p2pkh: [29, 37], p2sh: [28, 186] }
|
|
35
|
+
};
|
|
36
|
+
function addressPrefix(network, kind) {
|
|
37
|
+
return PREFIX[network][kind];
|
|
38
|
+
}
|
|
39
|
+
function hash160(data) {
|
|
40
|
+
return ripemd160(sha256(data));
|
|
41
|
+
}
|
|
42
|
+
function scriptFor(kind, hash) {
|
|
43
|
+
if (hash.length !== 20) throw new Error("A transparent script needs a 20-byte hash");
|
|
44
|
+
if (kind === "p2pkh") return Uint8Array.of(118, 169, 20, ...hash, 136, 172);
|
|
45
|
+
return Uint8Array.of(169, 20, ...hash, 135);
|
|
46
|
+
}
|
|
47
|
+
function scriptKind(script) {
|
|
48
|
+
if (script.length === 25 && script[0] === 118 && script[1] === 169 && script[2] === 20 && script[23] === 136 && script[24] === 172) return "p2pkh";
|
|
49
|
+
if (script.length === 23 && script[0] === 169 && script[1] === 20 && script[22] === 135) return "p2sh";
|
|
50
|
+
return "unknown";
|
|
51
|
+
}
|
|
52
|
+
function encodeAddress(kind, hash, network) {
|
|
53
|
+
if (hash.length !== 20) throw new Error("A transparent address needs a 20-byte hash");
|
|
54
|
+
const [a, b] = PREFIX[network][kind];
|
|
55
|
+
return b58.encode(Uint8Array.of(a, b, ...hash));
|
|
56
|
+
}
|
|
57
|
+
function decodeAddress(address, network) {
|
|
58
|
+
const a = address.trim();
|
|
59
|
+
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.`);
|
|
60
|
+
if (/^zs1|^ztestsapling/i.test(a)) throw new Error("Sapling shielded address (zs1\u2026) \u2014 this wallet sends to transparent t-addresses only.");
|
|
61
|
+
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.");
|
|
62
|
+
let payload;
|
|
63
|
+
try {
|
|
64
|
+
payload = b58.decode(a);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
throw new Error(`Invalid Zcash address: ${a}`, { cause: e });
|
|
67
|
+
}
|
|
68
|
+
if (payload.length !== 22) throw new Error(`Invalid Zcash address: ${a}`);
|
|
69
|
+
const p0 = payload[0], p1 = payload[1];
|
|
70
|
+
for (const net of ["mainnet", "testnet"]) {
|
|
71
|
+
for (const kind of ["p2pkh", "p2sh"]) {
|
|
72
|
+
const [a0, a1] = PREFIX[net][kind];
|
|
73
|
+
if (p0 === a0 && p1 === a1) {
|
|
74
|
+
if (net !== network) throw new Error(`${a} is a Zcash ${net} address; this wallet is on ${network}.`);
|
|
75
|
+
const hash = payload.slice(2);
|
|
76
|
+
return { kind, hash, script: scriptFor(kind, hash) };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`Invalid Zcash address: ${a} (unknown version prefix)`);
|
|
81
|
+
}
|
|
82
|
+
function addressFromScript(script, network) {
|
|
83
|
+
const kind = scriptKind(script);
|
|
84
|
+
if (kind === "unknown") return void 0;
|
|
85
|
+
return encodeAddress(kind, kind === "p2pkh" ? script.slice(3, 23) : script.slice(2, 22), network);
|
|
86
|
+
}
|
|
87
|
+
function addressFromPublicKey(publicKey, network = "mainnet") {
|
|
88
|
+
if (publicKey.length !== 33) throw new Error("Expected a 33-byte compressed public key");
|
|
89
|
+
return encodeAddress("p2pkh", hash160(publicKey), network);
|
|
90
|
+
}
|
|
91
|
+
function outputSize(script) {
|
|
92
|
+
return 8 + (script.length < 253 ? 1 : 3) + script.length;
|
|
93
|
+
}
|
|
94
|
+
var DUST_LIMIT = 546n;
|
|
95
|
+
|
|
96
|
+
// src/fee.ts
|
|
97
|
+
import { InsufficientFundsError } from "@anywalletkit/core";
|
|
98
|
+
var MARGINAL_FEE = 5000n;
|
|
99
|
+
var GRACE_ACTIONS = 2;
|
|
100
|
+
var P2PKH_STANDARD_INPUT_SIZE = 150;
|
|
101
|
+
var P2PKH_STANDARD_OUTPUT_SIZE = 34;
|
|
102
|
+
var P2PKH_INPUT_SIZE = 149;
|
|
103
|
+
var MAX_UTXO_INPUTS = 200;
|
|
104
|
+
function logicalActions(inputSizes2, outputSizes) {
|
|
105
|
+
const inBytes = inputSizes2.reduce((s, n) => s + n, 0);
|
|
106
|
+
const outBytes = outputSizes.reduce((s, n) => s + n, 0);
|
|
107
|
+
return Math.max(Math.ceil(inBytes / P2PKH_STANDARD_INPUT_SIZE), Math.ceil(outBytes / P2PKH_STANDARD_OUTPUT_SIZE));
|
|
108
|
+
}
|
|
109
|
+
function conventionalFee(inputSizes2, outputSizes) {
|
|
110
|
+
return MARGINAL_FEE * BigInt(Math.max(GRACE_ACTIONS, logicalActions(inputSizes2, outputSizes)));
|
|
111
|
+
}
|
|
112
|
+
function estimateSize(inputs, outputSizes) {
|
|
113
|
+
const cs = (n) => n < 253 ? 1 : 3;
|
|
114
|
+
return 20 + cs(inputs) + inputs * P2PKH_INPUT_SIZE + cs(outputSizes.length) + outputSizes.reduce((s, n) => s + n, 0) + 3;
|
|
115
|
+
}
|
|
116
|
+
var TooManyInputsError = class extends Error {
|
|
117
|
+
maxInputs;
|
|
118
|
+
constructor(maxInputs) {
|
|
119
|
+
super(`The spend needs more than ${maxInputs} inputs; consolidate the UTXOs first or raise maxInputs.`);
|
|
120
|
+
this.name = "TooManyInputsError";
|
|
121
|
+
this.maxInputs = maxInputs;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
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;
|
|
125
|
+
function spendableUtxos(utxos) {
|
|
126
|
+
return utxos.filter((u) => u.value > MARGINAL_FEE).sort(cmpUtxo);
|
|
127
|
+
}
|
|
128
|
+
function selectCoins(o) {
|
|
129
|
+
const maxInputs = o.maxInputs ?? MAX_UTXO_INPUTS;
|
|
130
|
+
const dust = o.changeDust ?? DUST_LIMIT;
|
|
131
|
+
const candidates = spendableUtxos(o.utxos);
|
|
132
|
+
const available = o.utxos.reduce((s, u) => s + u.value, 0n);
|
|
133
|
+
const inputs = (n) => Array.from({ length: n }, () => P2PKH_INPUT_SIZE);
|
|
134
|
+
const selected = [];
|
|
135
|
+
let sum = 0n;
|
|
136
|
+
for (const u of candidates) {
|
|
137
|
+
if (selected.length >= maxInputs) throw new TooManyInputsError(maxInputs);
|
|
138
|
+
selected.push(u);
|
|
139
|
+
sum += u.value;
|
|
140
|
+
const n = selected.length;
|
|
141
|
+
const convNoChange = conventionalFee(inputs(n), [o.recipientOutputSize]);
|
|
142
|
+
const feeNoChange = o.fee ?? convNoChange;
|
|
143
|
+
if (sum < o.target + feeNoChange) continue;
|
|
144
|
+
const convWithChange = conventionalFee(inputs(n), [o.recipientOutputSize, o.changeOutputSize]);
|
|
145
|
+
const feeWithChange = o.fee ?? convWithChange;
|
|
146
|
+
const change = sum - o.target - feeWithChange;
|
|
147
|
+
if (change >= dust) {
|
|
148
|
+
return { utxos: selected, fee: feeWithChange, conventionalFee: convWithChange, change, changeFolded: false, size: estimateSize(n, [o.recipientOutputSize, o.changeOutputSize]) };
|
|
149
|
+
}
|
|
150
|
+
return { utxos: selected, fee: sum - o.target, conventionalFee: convNoChange, change: 0n, changeFolded: sum - o.target > feeNoChange, size: estimateSize(n, [o.recipientOutputSize]) };
|
|
151
|
+
}
|
|
152
|
+
const need = o.target + (o.fee ?? conventionalFee(inputs(Math.max(selected.length, 1)), [o.recipientOutputSize]));
|
|
153
|
+
throw new InsufficientFundsError(need, available, "ZEC");
|
|
154
|
+
}
|
|
155
|
+
function selectAll(o) {
|
|
156
|
+
const maxInputs = o.maxInputs ?? MAX_UTXO_INPUTS;
|
|
157
|
+
const utxos = spendableUtxos(o.utxos).slice(0, maxInputs);
|
|
158
|
+
if (utxos.length === 0) return { utxos, fee: 0n, amount: 0n, size: 0 };
|
|
159
|
+
const fee = o.fee ?? conventionalFee(Array.from({ length: utxos.length }, () => P2PKH_INPUT_SIZE), [o.outputSize]);
|
|
160
|
+
const amount = utxos.reduce((s, u) => s + u.value, 0n) - fee;
|
|
161
|
+
return { utxos, fee, amount: amount > 0n ? amount : 0n, size: estimateSize(utxos.length, [o.outputSize]) };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/tx.ts
|
|
165
|
+
import { blake2b } from "@noble/hashes/blake2.js";
|
|
166
|
+
var TX_VERSION_V5 = 2147483653;
|
|
167
|
+
var VERSION_GROUP_ID_V5 = 648488714;
|
|
168
|
+
var SIGHASH_ALL = 1;
|
|
169
|
+
var MAINNET_UPGRADES = Object.freeze([
|
|
170
|
+
{ name: "NU5", height: 1687104, branchId: 3268858036 },
|
|
171
|
+
{ name: "NU6", height: 2726400, branchId: 3370586197 },
|
|
172
|
+
{ name: "NU6.1", height: 3146400, branchId: 1307332080 },
|
|
173
|
+
{ name: "NU6.3", height: 3428143, branchId: 933566043 }
|
|
174
|
+
]);
|
|
175
|
+
var CURRENT_BRANCH_ID = MAINNET_UPGRADES[MAINNET_UPGRADES.length - 1].branchId;
|
|
176
|
+
function branchIdForHeight(height) {
|
|
177
|
+
if (height === void 0 || !Number.isFinite(height)) return CURRENT_BRANCH_ID;
|
|
178
|
+
let id = MAINNET_UPGRADES[0].branchId;
|
|
179
|
+
for (const u of MAINNET_UPGRADES) if (height >= u.height) id = u.branchId;
|
|
180
|
+
return id;
|
|
181
|
+
}
|
|
182
|
+
var FINAL_SEQUENCE = 4294967294;
|
|
183
|
+
var MAX_SEQUENCE = 4294967295;
|
|
184
|
+
var assertTxid = (txid2) => {
|
|
185
|
+
if (!/^[0-9a-fA-F]{64}$/.test(txid2)) throw new Error(`Invalid txid: ${txid2}`);
|
|
186
|
+
};
|
|
187
|
+
function writeTransparent(w, tx) {
|
|
188
|
+
w.compact(tx.inputs.length);
|
|
189
|
+
for (const i of tx.inputs) {
|
|
190
|
+
assertTxid(i.txid);
|
|
191
|
+
w.bytes(reverseBytes(hexToBytes(i.txid))).u32(i.vout).varBytes(i.scriptSig).u32(i.sequence);
|
|
192
|
+
}
|
|
193
|
+
w.compact(tx.outputs.length);
|
|
194
|
+
for (const o of tx.outputs) {
|
|
195
|
+
if (o.value < 0n) throw new Error("Negative output value");
|
|
196
|
+
w.i64(o.value).varBytes(o.script);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function serializeTx(tx) {
|
|
200
|
+
const w = new Writer();
|
|
201
|
+
w.u32(TX_VERSION_V5).u32(VERSION_GROUP_ID_V5).u32(tx.consensusBranchId >>> 0).u32(tx.lockTime).u32(tx.expiryHeight);
|
|
202
|
+
writeTransparent(w, tx);
|
|
203
|
+
w.compact(0);
|
|
204
|
+
w.compact(0);
|
|
205
|
+
w.compact(0);
|
|
206
|
+
return w.finish();
|
|
207
|
+
}
|
|
208
|
+
function parseTx(raw) {
|
|
209
|
+
const r = new Reader(typeof raw === "string" ? hexToBytes(raw) : raw);
|
|
210
|
+
const header = r.u32();
|
|
211
|
+
const overwintered = (header & 2147483648) !== 0;
|
|
212
|
+
const version = header & 2147483647;
|
|
213
|
+
if (!overwintered || version !== 5) throw new Error(`Only v5 Zcash transactions are supported here (got version ${version}${overwintered ? "" : ", not overwintered"}).`);
|
|
214
|
+
const vgid = r.u32();
|
|
215
|
+
if (vgid !== VERSION_GROUP_ID_V5) throw new Error(`Unexpected version group id 0x${vgid.toString(16)} for a v5 transaction.`);
|
|
216
|
+
const consensusBranchId = r.u32();
|
|
217
|
+
const lockTime = r.u32();
|
|
218
|
+
const expiryHeight = r.u32();
|
|
219
|
+
const nIn = r.compact();
|
|
220
|
+
const inputs = [];
|
|
221
|
+
for (let i = 0; i < nIn; i++) {
|
|
222
|
+
const txid2 = bytesToHex(reverseBytes(r.bytes(32)));
|
|
223
|
+
const vout = r.u32();
|
|
224
|
+
const scriptSig = r.varBytes();
|
|
225
|
+
const sequence = r.u32();
|
|
226
|
+
inputs.push({ txid: txid2, vout, scriptSig, sequence });
|
|
227
|
+
}
|
|
228
|
+
const nOut = r.compact();
|
|
229
|
+
const outputs = [];
|
|
230
|
+
for (let i = 0; i < nOut; i++) outputs.push({ value: r.i64(), script: r.varBytes() });
|
|
231
|
+
const nSpends = r.compact();
|
|
232
|
+
const nOutputs = r.compact();
|
|
233
|
+
if (nSpends !== 0 || nOutputs !== 0) throw new Error("This transaction carries a Sapling bundle; shielded transactions are not supported.");
|
|
234
|
+
const nActions = r.compact();
|
|
235
|
+
if (nActions !== 0) throw new Error("This transaction carries an Orchard bundle; shielded transactions are not supported.");
|
|
236
|
+
if (r.remaining !== 0) throw new Error(`Trailing bytes after the transaction (${r.remaining}).`);
|
|
237
|
+
return { consensusBranchId, lockTime, expiryHeight, inputs, outputs };
|
|
238
|
+
}
|
|
239
|
+
function personal(tag) {
|
|
240
|
+
const p = utf8ToBytes(tag);
|
|
241
|
+
if (p.length !== 16) throw new Error(`BLAKE2b personalization must be 16 bytes: ${tag}`);
|
|
242
|
+
return p;
|
|
243
|
+
}
|
|
244
|
+
function h(tag, data) {
|
|
245
|
+
return blake2b(data, { dkLen: 32, personalization: typeof tag === "string" ? personal(tag) : tag });
|
|
246
|
+
}
|
|
247
|
+
var EMPTY = new Uint8Array(0);
|
|
248
|
+
function txHashPersonalization(consensusBranchId) {
|
|
249
|
+
const p = new Uint8Array(16);
|
|
250
|
+
p.set(utf8ToBytes("ZcashTxHash_"), 0);
|
|
251
|
+
p.set(u32le(consensusBranchId >>> 0), 12);
|
|
252
|
+
return p;
|
|
253
|
+
}
|
|
254
|
+
function headerDigest(tx) {
|
|
255
|
+
const w = new Writer().u32(TX_VERSION_V5).u32(VERSION_GROUP_ID_V5).u32(tx.consensusBranchId >>> 0).u32(tx.lockTime).u32(tx.expiryHeight);
|
|
256
|
+
return h("ZTxIdHeadersHash", w.finish());
|
|
257
|
+
}
|
|
258
|
+
function prevoutsDigest(tx) {
|
|
259
|
+
const w = new Writer();
|
|
260
|
+
for (const i of tx.inputs) {
|
|
261
|
+
assertTxid(i.txid);
|
|
262
|
+
w.bytes(reverseBytes(hexToBytes(i.txid))).u32(i.vout);
|
|
263
|
+
}
|
|
264
|
+
return h("ZTxIdPrevoutHash", w.finish());
|
|
265
|
+
}
|
|
266
|
+
function sequenceDigest(tx) {
|
|
267
|
+
const w = new Writer();
|
|
268
|
+
for (const i of tx.inputs) w.u32(i.sequence);
|
|
269
|
+
return h("ZTxIdSequencHash", w.finish());
|
|
270
|
+
}
|
|
271
|
+
function outputsDigest(tx) {
|
|
272
|
+
const w = new Writer();
|
|
273
|
+
for (const o of tx.outputs) w.i64(o.value).varBytes(o.script);
|
|
274
|
+
return h("ZTxIdOutputsHash", w.finish());
|
|
275
|
+
}
|
|
276
|
+
function transparentDigest(tx) {
|
|
277
|
+
if (tx.inputs.length === 0 && tx.outputs.length === 0) return h("ZTxIdTranspaHash", EMPTY);
|
|
278
|
+
const w = new Writer().bytes(prevoutsDigest(tx)).bytes(sequenceDigest(tx)).bytes(outputsDigest(tx));
|
|
279
|
+
return h("ZTxIdTranspaHash", w.finish());
|
|
280
|
+
}
|
|
281
|
+
var saplingDigestEmpty = () => h("ZTxIdSaplingHash", EMPTY);
|
|
282
|
+
var orchardDigestEmpty = () => h("ZTxIdOrchardHash", EMPTY);
|
|
283
|
+
function txidDigest(tx) {
|
|
284
|
+
const w = new Writer().bytes(headerDigest(tx)).bytes(transparentDigest(tx)).bytes(saplingDigestEmpty()).bytes(orchardDigestEmpty());
|
|
285
|
+
return h(txHashPersonalization(tx.consensusBranchId), w.finish());
|
|
286
|
+
}
|
|
287
|
+
function txid(tx) {
|
|
288
|
+
return bytesToHex(reverseBytes(txidDigest(tx)));
|
|
289
|
+
}
|
|
290
|
+
function sighashAll(tx, index, spent) {
|
|
291
|
+
if (spent.length !== tx.inputs.length) throw new Error(`sighash: ${tx.inputs.length} inputs but ${spent.length} spent outputs`);
|
|
292
|
+
const input = tx.inputs[index];
|
|
293
|
+
const coin = spent[index];
|
|
294
|
+
if (!input || !coin) throw new Error(`sighash: input ${index} does not exist`);
|
|
295
|
+
const amounts = new Writer();
|
|
296
|
+
const scripts = new Writer();
|
|
297
|
+
for (const s of spent) {
|
|
298
|
+
amounts.i64(s.value);
|
|
299
|
+
scripts.varBytes(s.script);
|
|
300
|
+
}
|
|
301
|
+
const txin = new Writer().bytes(reverseBytes(hexToBytes(input.txid))).u32(input.vout).i64(coin.value).varBytes(coin.script).u32(input.sequence);
|
|
302
|
+
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()));
|
|
303
|
+
const transparentSig = h("ZTxIdTranspaHash", t.finish());
|
|
304
|
+
const w = new Writer().bytes(headerDigest(tx)).bytes(transparentSig).bytes(saplingDigestEmpty()).bytes(orchardDigestEmpty());
|
|
305
|
+
return h(txHashPersonalization(tx.consensusBranchId), w.finish());
|
|
306
|
+
}
|
|
307
|
+
function p2pkhScriptSig(derSignature, publicKey) {
|
|
308
|
+
const sig = new Uint8Array(derSignature.length + 1);
|
|
309
|
+
sig.set(derSignature, 0);
|
|
310
|
+
sig[derSignature.length] = SIGHASH_ALL;
|
|
311
|
+
return new Writer().bytes(compactSize(sig.length)).bytes(sig).bytes(compactSize(publicKey.length)).bytes(publicKey).finish();
|
|
312
|
+
}
|
|
313
|
+
function parseP2pkhScriptSig(scriptSig) {
|
|
314
|
+
try {
|
|
315
|
+
const r = new Reader(scriptSig);
|
|
316
|
+
const sig = r.varBytes();
|
|
317
|
+
const pub = r.varBytes();
|
|
318
|
+
if (r.remaining !== 0 || sig.length < 9 || pub.length !== 33 && pub.length !== 65) return void 0;
|
|
319
|
+
return { signature: sig.slice(0, -1), hashType: sig[sig.length - 1], publicKey: pub };
|
|
320
|
+
} catch {
|
|
321
|
+
return void 0;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/describe.ts
|
|
326
|
+
var DEFAULT_MAX_FEE = 1000000n;
|
|
327
|
+
var UnsafeTransactionError = class extends Error {
|
|
328
|
+
code;
|
|
329
|
+
constructor(code, message) {
|
|
330
|
+
super(message);
|
|
331
|
+
this.name = "UnsafeTransactionError";
|
|
332
|
+
this.code = code;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
function inputSizes(tx) {
|
|
336
|
+
return tx.inputs.map((i) => 32 + 4 + (i.scriptSig.length < 253 ? 1 : 3) + i.scriptSig.length + 4);
|
|
337
|
+
}
|
|
338
|
+
function describeTransaction(tx, spent, ctx) {
|
|
339
|
+
if (spent.length !== tx.inputs.length) throw new Error(`describeTransaction: ${tx.inputs.length} inputs but ${spent.length} spent outputs`);
|
|
340
|
+
const inputs = tx.inputs.map((i, k) => ({ txid: i.txid, vout: i.vout, value: spent[k].value, address: addressFromScript(spent[k].script, ctx.network) }));
|
|
341
|
+
const outputs = [];
|
|
342
|
+
const warnings = /* @__PURE__ */ new Set();
|
|
343
|
+
for (const o of tx.outputs) {
|
|
344
|
+
const address = addressFromScript(o.script, ctx.network);
|
|
345
|
+
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";
|
|
346
|
+
if (scriptKind(o.script) !== "unknown" && o.value < DUST_LIMIT) warnings.add("dust-output");
|
|
347
|
+
outputs.push({ address, value: o.value, role });
|
|
348
|
+
}
|
|
349
|
+
const inputSum = spent.reduce((s, c) => s + c.value, 0n);
|
|
350
|
+
const outputSum = tx.outputs.reduce((s, o) => s + o.value, 0n);
|
|
351
|
+
const fee = inputSum - outputSum;
|
|
352
|
+
const amount = outputs.filter((o) => o.role !== "change").reduce((s, o) => s + o.value, 0n);
|
|
353
|
+
const conv = conventionalFee(inputSizes(tx), tx.outputs.map((o) => outputSize(o.script)));
|
|
354
|
+
if (fee < conv) warnings.add("fee-below-conventional");
|
|
355
|
+
if (amount > 0n && fee * 10n > amount) warnings.add("fee-over-10pct-of-amount");
|
|
356
|
+
if (!outputs.some((o) => o.role === "change")) warnings.add("no-change");
|
|
357
|
+
if (tx.expiryHeight === 0) warnings.add("no-expiry");
|
|
358
|
+
const signed = tx.inputs.length > 0 && tx.inputs.every((i) => i.scriptSig.length > 0);
|
|
359
|
+
const desc = {
|
|
360
|
+
kind: "zcash-transfer",
|
|
361
|
+
version: 5,
|
|
362
|
+
consensusBranchId: tx.consensusBranchId,
|
|
363
|
+
inputs,
|
|
364
|
+
outputs,
|
|
365
|
+
amount,
|
|
366
|
+
fee,
|
|
367
|
+
conventionalFee: conv,
|
|
368
|
+
size: serializeTx(tx).length,
|
|
369
|
+
lockTime: tx.lockTime,
|
|
370
|
+
expiryHeight: tx.expiryHeight,
|
|
371
|
+
warnings: [...warnings]
|
|
372
|
+
};
|
|
373
|
+
if (signed) desc.txid = txid(tx);
|
|
374
|
+
return desc;
|
|
375
|
+
}
|
|
376
|
+
function assertSafe(desc, ctx) {
|
|
377
|
+
const maxFee = ctx.maxFee ?? DEFAULT_MAX_FEE;
|
|
378
|
+
if (desc.fee < 0n) throw new UnsafeTransactionError("outputs-exceed-inputs", "Outputs exceed inputs.");
|
|
379
|
+
if (desc.fee > maxFee) throw new UnsafeTransactionError("fee-too-high", `Fee ${desc.fee} zatoshis exceeds the ${maxFee} zatoshi ceiling (maxFee).`);
|
|
380
|
+
for (const o of desc.outputs) {
|
|
381
|
+
if (o.role === "change" && o.address !== ctx.ownAddress) throw new UnsafeTransactionError("change-not-ours", `Change output pays ${o.address ?? "<no address>"}, not ${ctx.ownAddress}.`);
|
|
382
|
+
}
|
|
383
|
+
if (ctx.intended) {
|
|
384
|
+
const recipients = desc.outputs.filter((o) => o.role === "recipient");
|
|
385
|
+
if (recipients.length !== 1) throw new UnsafeTransactionError("recipient-mismatch", `Expected one output of ${ctx.intended.value} zatoshis to ${ctx.intended.to}, found ${recipients.length}.`);
|
|
386
|
+
const unknown = desc.outputs.filter((o) => o.role === "unknown");
|
|
387
|
+
if (unknown.length) throw new UnsafeTransactionError("unexpected-output", `Transaction carries ${unknown.length} output(s) that are neither the recipient nor our change.`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/blockbook.ts
|
|
392
|
+
import { NetworkError } from "@anywalletkit/core";
|
|
393
|
+
var ZcashBlockbookClient = class _ZcashBlockbookClient {
|
|
394
|
+
base;
|
|
395
|
+
headers;
|
|
396
|
+
fetchFn;
|
|
397
|
+
timeoutMs;
|
|
398
|
+
constructor(config, fetchFn = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
399
|
+
this.base = config.url.replace(/\/+$/, "").replace(/\/api$/, "");
|
|
400
|
+
this.headers = { accept: "application/json", ...config.headers ?? {} };
|
|
401
|
+
this.fetchFn = fetchFn;
|
|
402
|
+
this.timeoutMs = timeoutMs;
|
|
403
|
+
}
|
|
404
|
+
async connect() {
|
|
405
|
+
}
|
|
406
|
+
async close() {
|
|
407
|
+
}
|
|
408
|
+
async get(path, check) {
|
|
409
|
+
const url = `${this.base}/api${path}`;
|
|
410
|
+
const res = await fetchWithTimeout(this.fetchFn, url, { headers: this.headers }, this.timeoutMs, this.base);
|
|
411
|
+
if (!res.ok) throw new NetworkError(`Blockbook request failed: ${res.status} ${res.statusText} (${url})`, { provider: this.base });
|
|
412
|
+
let body;
|
|
413
|
+
try {
|
|
414
|
+
body = await res.json();
|
|
415
|
+
} catch (e) {
|
|
416
|
+
throw new NetworkError(`Blockbook returned non-JSON for ${url}`, { provider: this.base, cause: e });
|
|
417
|
+
}
|
|
418
|
+
if (!check(body)) throw new NetworkError(`Blockbook returned an unexpected shape for ${url}`, { provider: this.base });
|
|
419
|
+
return body;
|
|
420
|
+
}
|
|
421
|
+
async getBalance(address) {
|
|
422
|
+
const data = await this.get(`/v2/address/${address}?details=basic`, (v) => isRecord(v) && isIntString(v.balance) && isIntString(v.unconfirmedBalance));
|
|
423
|
+
return { confirmed: BigInt(data.balance), unconfirmed: BigInt(data.unconfirmedBalance) };
|
|
424
|
+
}
|
|
425
|
+
async listUnspent(address) {
|
|
426
|
+
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)));
|
|
427
|
+
return data.map((u) => ({ txid: u.txid, vout: u.vout, value: BigInt(u.value), height: u.height && u.height > 0 ? u.height : -1 }));
|
|
428
|
+
}
|
|
429
|
+
static isTx(v) {
|
|
430
|
+
return isRecord(v) && typeof v.txid === "string" && isInt(v.blockHeight) && isInt(v.confirmations);
|
|
431
|
+
}
|
|
432
|
+
async getTransactionDetails(txid2) {
|
|
433
|
+
try {
|
|
434
|
+
return await this.get(`/v2/tx/${txid2}`, _ZcashBlockbookClient.isTx);
|
|
435
|
+
} catch (e) {
|
|
436
|
+
if (e instanceof NetworkError && /\b(400|404)\b/.test(e.message)) return void 0;
|
|
437
|
+
throw e;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async getTransaction(txid2) {
|
|
441
|
+
const tx = await this.get(`/v2/tx/${txid2}`, _ZcashBlockbookClient.isTx);
|
|
442
|
+
if (typeof tx.hex !== "string" || !tx.hex) throw new NetworkError(`Transaction ${txid2} has no hex data`, { provider: this.base });
|
|
443
|
+
return tx.hex;
|
|
444
|
+
}
|
|
445
|
+
async getOutput(txid2, vout) {
|
|
446
|
+
const tx = await this.getTransactionDetails(txid2);
|
|
447
|
+
const o = tx?.vout?.find((x) => x.n === vout);
|
|
448
|
+
if (!o || !isIntString(o.value) || typeof o.hex !== "string") return void 0;
|
|
449
|
+
return { value: BigInt(o.value), script: hexToBytes(o.hex) };
|
|
450
|
+
}
|
|
451
|
+
async getTransactionInfo(txid2) {
|
|
452
|
+
const tx = await this.getTransactionDetails(txid2);
|
|
453
|
+
if (!tx) return void 0;
|
|
454
|
+
const info = { txid: tx.txid, blockHeight: tx.blockHeight > 0 ? tx.blockHeight : -1, confirmations: tx.confirmations };
|
|
455
|
+
if (isIntString(tx.fees)) info.fee = BigInt(tx.fees);
|
|
456
|
+
if (typeof tx.blockTime === "number") info.blockTime = tx.blockTime;
|
|
457
|
+
return info;
|
|
458
|
+
}
|
|
459
|
+
async broadcast(rawTx) {
|
|
460
|
+
const data = await this.get(`/v2/sendtx/${rawTx}`, (v) => isRecord(v));
|
|
461
|
+
if (data.error) throw new Error(`Broadcast rejected: ${typeof data.error === "string" ? data.error : data.error.message ?? "broadcast failed"}`);
|
|
462
|
+
if (!data.result) throw new NetworkError("Blockbook returned no txid", { provider: this.base });
|
|
463
|
+
return data.result;
|
|
464
|
+
}
|
|
465
|
+
async getBlockHeight() {
|
|
466
|
+
const data = await this.get("/v2", (v) => isRecord(v));
|
|
467
|
+
const h2 = data.blockbook?.bestHeight;
|
|
468
|
+
if (typeof h2 !== "number" || h2 <= 0) throw new NetworkError("Blockbook did not report a block height", { provider: this.base });
|
|
469
|
+
return h2;
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
// src/client.ts
|
|
474
|
+
import { NetworkError as NetworkError2 } from "@anywalletkit/core";
|
|
475
|
+
var isClient = (c) => typeof c === "object" && c !== null && typeof c.listUnspent === "function";
|
|
476
|
+
function toClient(c, fetchFn, timeoutMs) {
|
|
477
|
+
if (isClient(c)) return c;
|
|
478
|
+
switch (c.type) {
|
|
479
|
+
case "blockchair":
|
|
480
|
+
return new BlockchairClient(c.clientConfig ?? {}, fetchFn, timeoutMs);
|
|
481
|
+
case "blockbook-http":
|
|
482
|
+
return new ZcashBlockbookClient(c.clientConfig, fetchFn, timeoutMs);
|
|
483
|
+
default:
|
|
484
|
+
throw new Error(`Unknown Zcash client type: ${String(c.type)}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
var METHODS = ["connect", "close", "getBalance", "listUnspent", "getTransaction", "getOutput", "getTransactionInfo", "broadcast", "getBlockHeight"];
|
|
488
|
+
function createClient(config) {
|
|
489
|
+
if (config.client === void 0) return void 0;
|
|
490
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
491
|
+
const list = (Array.isArray(config.client) ? config.client : [config.client]).map((c) => toClient(c, config.fetch, timeoutMs));
|
|
492
|
+
if (list.length === 0) return void 0;
|
|
493
|
+
if (list.length === 1) return list[0];
|
|
494
|
+
const attempts = list.length + (config.retries ?? 3);
|
|
495
|
+
const call = (method) => async (...args) => {
|
|
496
|
+
let last;
|
|
497
|
+
for (let i = 0; i < attempts; i++) {
|
|
498
|
+
const c = list[i % list.length];
|
|
499
|
+
try {
|
|
500
|
+
return await c[method].apply(c, args);
|
|
501
|
+
} catch (e) {
|
|
502
|
+
if (method === "broadcast" && !(e instanceof NetworkError2)) throw e;
|
|
503
|
+
last = e;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
throw new NetworkError2(`All Zcash clients failed for ${method}`, { cause: last });
|
|
507
|
+
};
|
|
508
|
+
const out = {};
|
|
509
|
+
for (const m of METHODS) out[m] = call(m);
|
|
510
|
+
return out;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/message.ts
|
|
514
|
+
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
515
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
516
|
+
import { base64 } from "@scure/base";
|
|
517
|
+
var MAGIC = "Zcash Signed Message:\n";
|
|
518
|
+
var PREFIX2 = utf8ToBytes(MAGIC);
|
|
519
|
+
function magicHash(message) {
|
|
520
|
+
const msg = typeof message === "string" ? utf8ToBytes(message) : message;
|
|
521
|
+
const p = compactSize(PREFIX2.length), l = compactSize(msg.length);
|
|
522
|
+
const buf = new Uint8Array(p.length + PREFIX2.length + l.length + msg.length);
|
|
523
|
+
let o = 0;
|
|
524
|
+
for (const part of [p, PREFIX2, l, msg]) {
|
|
525
|
+
buf.set(part, o);
|
|
526
|
+
o += part.length;
|
|
527
|
+
}
|
|
528
|
+
return sha2562(sha2562(buf));
|
|
529
|
+
}
|
|
530
|
+
function encodeMessageSignature(rs, recovery) {
|
|
531
|
+
if (rs.length !== 64 || recovery > 3) throw new Error("Expected a 64-byte r\u2016s signature and a recovery id 0..3.");
|
|
532
|
+
const out = new Uint8Array(65);
|
|
533
|
+
out[0] = 27 + 4 + recovery;
|
|
534
|
+
out.set(rs, 1);
|
|
535
|
+
return base64.encode(out);
|
|
536
|
+
}
|
|
537
|
+
async function signMessageWith(message, signer) {
|
|
538
|
+
const sig = await signer.sign(magicHash(message), { scheme: "ecdsa-recoverable", context: "zcash-message" });
|
|
539
|
+
return encodeMessageSignature(sig.subarray(0, 64), sig[64]);
|
|
540
|
+
}
|
|
541
|
+
function recoverMessagePublicKey(message, signature) {
|
|
542
|
+
let bytes;
|
|
543
|
+
try {
|
|
544
|
+
bytes = base64.decode(signature);
|
|
545
|
+
} catch {
|
|
546
|
+
return void 0;
|
|
547
|
+
}
|
|
548
|
+
if (bytes.length !== 65) return void 0;
|
|
549
|
+
const flag = bytes[0] - 27;
|
|
550
|
+
if (flag < 0 || flag > 15) return void 0;
|
|
551
|
+
const recovered = new Uint8Array(65);
|
|
552
|
+
recovered[0] = flag & 3;
|
|
553
|
+
recovered.set(bytes.subarray(1), 1);
|
|
554
|
+
try {
|
|
555
|
+
const pub = secp256k1.recoverPublicKey(recovered, magicHash(message), { prehash: false });
|
|
556
|
+
return pub.length === 33 ? pub : secp256k1.Point.fromBytes(pub).toBytes(true);
|
|
557
|
+
} catch {
|
|
558
|
+
return void 0;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/manager.ts
|
|
563
|
+
import { LocalSigner as LocalSigner2, SignerError as SignerError2, WalletManager, isSeedSigner } from "@anywalletkit/core";
|
|
564
|
+
|
|
565
|
+
// src/account.ts
|
|
566
|
+
import { InsufficientFundsError as InsufficientFundsError3, LocalSigner, SignerError, requireCurve } from "@anywalletkit/core";
|
|
567
|
+
import { secp256k1 as secp256k12 } from "@noble/curves/secp256k1.js";
|
|
568
|
+
|
|
569
|
+
// src/read-only-account.ts
|
|
570
|
+
import { InsufficientFundsError as InsufficientFundsError2, NetworkError as NetworkError3 } from "@anywalletkit/core";
|
|
571
|
+
var toBig = (v) => typeof v === "bigint" ? v : BigInt(typeof v === "string" ? v.trim() : Math.round(v));
|
|
572
|
+
var WalletAccountReadOnlyZcash = class {
|
|
573
|
+
/** The transparent address. */
|
|
574
|
+
address;
|
|
575
|
+
chain;
|
|
576
|
+
/** The address's scriptPubKey. */
|
|
577
|
+
scriptPubKey;
|
|
578
|
+
/** Script type of the address. */
|
|
579
|
+
scriptKind;
|
|
580
|
+
_config;
|
|
581
|
+
_client;
|
|
582
|
+
_network;
|
|
583
|
+
constructor(address, config = {}) {
|
|
584
|
+
this._config = config;
|
|
585
|
+
this._network = config.network ?? "mainnet";
|
|
586
|
+
const decoded = decodeAddress(address, this._network);
|
|
587
|
+
this.address = address.trim();
|
|
588
|
+
this.scriptPubKey = decoded.script;
|
|
589
|
+
this.scriptKind = decoded.kind;
|
|
590
|
+
this.chain = CAIP2[this._network];
|
|
591
|
+
this._client = createClient(config);
|
|
592
|
+
}
|
|
593
|
+
requireClient(what) {
|
|
594
|
+
if (!this._client) throw new NetworkError3(`The wallet must be connected to a client to ${what}.`);
|
|
595
|
+
return this._client;
|
|
596
|
+
}
|
|
597
|
+
/** Fee ceiling in zatoshis: `ZcashConfig.maxFee` or the default. */
|
|
598
|
+
get maxFee() {
|
|
599
|
+
return this._config.maxFee !== void 0 ? toBig(this._config.maxFee) : DEFAULT_MAX_FEE;
|
|
600
|
+
}
|
|
601
|
+
/** The transparent address. */
|
|
602
|
+
async getAddress() {
|
|
603
|
+
return this.address;
|
|
604
|
+
}
|
|
605
|
+
/** Confirmed plus incoming mempool value (the transports do not expose pending spends of our own outputs). */
|
|
606
|
+
async getBalance() {
|
|
607
|
+
const { confirmed, unconfirmed } = await this.requireClient("read balances").getBalance(this.address);
|
|
608
|
+
return confirmed + unconfirmed;
|
|
609
|
+
}
|
|
610
|
+
/** Always throws: Zcash has no tokens. */
|
|
611
|
+
async getTokenBalance(_token) {
|
|
612
|
+
throw new Error("The 'getTokenBalance' method is not supported on the zcash blockchain.");
|
|
613
|
+
}
|
|
614
|
+
/** Coin selection for a spend (ZIP-317 fee). Validates the recipient and the dust floor first. */
|
|
615
|
+
async planSpend(spend) {
|
|
616
|
+
if (this.scriptKind !== "p2pkh") throw new Error(`Cannot build spends from a ${this.scriptKind} address; only P2PKH (t1\u2026) accounts spend.`);
|
|
617
|
+
const amount = toBig(spend.value);
|
|
618
|
+
const recipient = decodeAddress(spend.to, this._network);
|
|
619
|
+
if (amount < DUST_LIMIT) throw new Error(`The amount must be at least the dust limit (${DUST_LIMIT} zatoshis).`);
|
|
620
|
+
const utxos = await this.requireClient("plan a spend").listUnspent(this.address);
|
|
621
|
+
if (utxos.length === 0) throw new InsufficientFundsError2(amount, 0n, "ZEC");
|
|
622
|
+
const selection = selectCoins({
|
|
623
|
+
utxos,
|
|
624
|
+
target: amount,
|
|
625
|
+
recipientOutputSize: outputSize(recipient.script),
|
|
626
|
+
changeOutputSize: outputSize(this.scriptPubKey),
|
|
627
|
+
maxInputs: this._config.maxInputs ?? MAX_UTXO_INPUTS,
|
|
628
|
+
...spend.fee !== void 0 ? { fee: toBig(spend.fee) } : {}
|
|
629
|
+
});
|
|
630
|
+
return { ...selection, amount, recipient };
|
|
631
|
+
}
|
|
632
|
+
/** ZIP-317 fee of a spend in zatoshis. */
|
|
633
|
+
async quoteSendTransaction(tx) {
|
|
634
|
+
const { fee } = await this.planSpend(tx);
|
|
635
|
+
return { fee };
|
|
636
|
+
}
|
|
637
|
+
/** ZIP-317 fee of a transfer in zatoshis. */
|
|
638
|
+
async quoteTransfer(o) {
|
|
639
|
+
if (o.token) throw new Error("Tokens are not supported on the zcash blockchain.");
|
|
640
|
+
return this.quoteSendTransaction({ to: o.recipient, value: o.amount });
|
|
641
|
+
}
|
|
642
|
+
/** Everything spendable in ONE transaction to a single output, no change. */
|
|
643
|
+
async getMaxSpendable(opts = {}) {
|
|
644
|
+
const utxos = await this.requireClient("plan a spend").listUnspent(this.address);
|
|
645
|
+
const outScript = opts.to ? decodeAddress(opts.to, this._network).script : this.scriptPubKey;
|
|
646
|
+
const sweep = selectAll({ utxos, outputSize: outputSize(outScript), maxInputs: this._config.maxInputs ?? MAX_UTXO_INPUTS, ...opts.fee !== void 0 ? { fee: toBig(opts.fee) } : {} });
|
|
647
|
+
const amount = sweep.amount >= DUST_LIMIT ? sweep.amount : 0n;
|
|
648
|
+
return { amount, fee: amount > 0n ? sweep.fee : 0n, changeValue: 0n, inputs: amount > 0n ? sweep.utxos.length : 0 };
|
|
649
|
+
}
|
|
650
|
+
/** Undefined until the transaction is mined. */
|
|
651
|
+
async getTransactionReceipt(hash) {
|
|
652
|
+
if (!/^[0-9a-fA-F]{64}$/.test(hash)) throw new Error("The 'getTransactionReceipt(hash)' method requires a valid transaction hash.");
|
|
653
|
+
const info = await this.requireClient("fetch transaction receipts").getTransactionInfo(hash);
|
|
654
|
+
if (!info || info.blockHeight <= 0) return void 0;
|
|
655
|
+
return { hash, status: 1, blockNumber: BigInt(info.blockHeight), confirmations: info.confirmations, ...info.fee !== void 0 ? { fee: info.fee } : {} };
|
|
656
|
+
}
|
|
657
|
+
/** Recover the key from a Zcash message signature and compare its P2PKH address with ours. */
|
|
658
|
+
async verify(message, signature) {
|
|
659
|
+
const pub = recoverMessagePublicKey(message, signature);
|
|
660
|
+
if (!pub) return false;
|
|
661
|
+
try {
|
|
662
|
+
return addressFromPublicKey(pub, this._network) === this.address;
|
|
663
|
+
} catch {
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/** Chain tip for lock time / expiry; undefined when the transport cannot tell. */
|
|
668
|
+
async tipHeight() {
|
|
669
|
+
if (!this._client) return void 0;
|
|
670
|
+
try {
|
|
671
|
+
return await this._client.getBlockHeight();
|
|
672
|
+
} catch {
|
|
673
|
+
return void 0;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
// src/account.ts
|
|
679
|
+
function addressFor(publicKey, network = "mainnet") {
|
|
680
|
+
return addressFromPublicKey(publicKey, network);
|
|
681
|
+
}
|
|
682
|
+
var DEFAULT_EXPIRY_DELTA = 40;
|
|
683
|
+
var WalletAccountZcash = class _WalletAccountZcash extends WalletAccountReadOnlyZcash {
|
|
684
|
+
/** UTXOs and the tip height come from the network before signing. */
|
|
685
|
+
needsNetwork = true;
|
|
686
|
+
/** Derivation path, or `external` for an external signer. */
|
|
687
|
+
path;
|
|
688
|
+
/** Compressed secp256k1 public key. */
|
|
689
|
+
publicKey;
|
|
690
|
+
/** The signer that holds the key. */
|
|
691
|
+
signer;
|
|
692
|
+
_disposed = false;
|
|
693
|
+
_readOnly;
|
|
694
|
+
/** `publicKey` is the signer's compressed secp256k1 key (33 bytes); it is needed synchronously for the address. */
|
|
695
|
+
constructor(signer, publicKey, path, config = {}) {
|
|
696
|
+
super(addressFromPublicKey(publicKey, config.network ?? "mainnet"), config);
|
|
697
|
+
this.signer = requireCurve(signer, "secp256k1");
|
|
698
|
+
if (!this.signer.schemes.includes("ecdsa")) throw new SignerError(`A Zcash account needs a signer with the 'ecdsa' scheme (has: ${this.signer.schemes.join(", ")}).`);
|
|
699
|
+
this.publicKey = publicKey;
|
|
700
|
+
this.path = path;
|
|
701
|
+
}
|
|
702
|
+
/** Back-compat: wrap a raw derived key in a {@link LocalSigner} (the account takes ownership; it cannot derive). */
|
|
703
|
+
static fromPrivateKey(privateKey, publicKey, path, config = {}) {
|
|
704
|
+
return new _WalletAccountZcash(LocalSigner.fromPrivateKey("secp256k1", privateKey, publicKey, path), publicKey, path, config);
|
|
705
|
+
}
|
|
706
|
+
/** Account index for the standard m/44'/133'/0'/0/i path. */
|
|
707
|
+
get index() {
|
|
708
|
+
const m = /^m\/44'\/(133|1)'\/0'\/0\/(\d+)$/.exec(this.path);
|
|
709
|
+
return m ? Number(m[2]) : void 0;
|
|
710
|
+
}
|
|
711
|
+
live() {
|
|
712
|
+
if (this._disposed) throw new SignerError("This account has been disposed.");
|
|
713
|
+
}
|
|
714
|
+
/** Zcash message signature (zcashd `signmessage` encoding), base64. */
|
|
715
|
+
async sign(message) {
|
|
716
|
+
this.live();
|
|
717
|
+
return signMessageWith(message, this.signer);
|
|
718
|
+
}
|
|
719
|
+
ctx(intended) {
|
|
720
|
+
return { network: this._network, ownAddress: this.address, intended, maxFee: this.maxFee };
|
|
721
|
+
}
|
|
722
|
+
/** Lock time + expiry + branch id from the tip (or the config override). */
|
|
723
|
+
async chainParams() {
|
|
724
|
+
const tip = await this.tipHeight();
|
|
725
|
+
const delta = this._config.expiryDelta ?? DEFAULT_EXPIRY_DELTA;
|
|
726
|
+
const consensusBranchId = this._config.consensusBranchId ?? branchIdForHeight(tip);
|
|
727
|
+
const lockTime = tip !== void 0 && this._config.antiFeeSniping !== false ? tip : 0;
|
|
728
|
+
const expiryHeight = tip !== void 0 && delta > 0 ? tip + delta : 0;
|
|
729
|
+
return { lockTime, expiryHeight, consensusBranchId };
|
|
730
|
+
}
|
|
731
|
+
/** Sign every input through the signer; the scriptSig is `<DER ‖ 0x01> <pubkey>`. */
|
|
732
|
+
async signInputs(tx, spent) {
|
|
733
|
+
for (let i = 0; i < tx.inputs.length; i++) {
|
|
734
|
+
const digest = sighashAll(tx, i, spent);
|
|
735
|
+
const rs = await this.signer.sign(digest, { scheme: "ecdsa", context: "zcash-sighash" });
|
|
736
|
+
const der = secp256k12.Signature.fromBytes(rs, "compact").toBytes("der");
|
|
737
|
+
tx.inputs[i].scriptSig = p2pkhScriptSig(der, this.publicKey);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async buildSignedTransaction(spend) {
|
|
741
|
+
this.live();
|
|
742
|
+
const plan = await this.planSpend(spend);
|
|
743
|
+
const params = await this.chainParams();
|
|
744
|
+
const tx = {
|
|
745
|
+
...params,
|
|
746
|
+
inputs: plan.utxos.map((u) => ({ txid: u.txid, vout: u.vout, scriptSig: new Uint8Array(0), sequence: FINAL_SEQUENCE })),
|
|
747
|
+
outputs: [{ value: plan.amount, script: plan.recipient.script }, ...plan.change > 0n ? [{ value: plan.change, script: this.scriptPubKey }] : []]
|
|
748
|
+
};
|
|
749
|
+
const spent = plan.utxos.map((u) => ({ value: u.value, script: this.scriptPubKey }));
|
|
750
|
+
const have = plan.utxos.reduce((s, u) => s + u.value, 0n);
|
|
751
|
+
if (have < plan.amount + plan.fee) throw new InsufficientFundsError3(plan.amount + plan.fee, have, "ZEC");
|
|
752
|
+
await this.signInputs(tx, spent);
|
|
753
|
+
const ctx = this.ctx({ to: spend.to, value: plan.amount });
|
|
754
|
+
const desc = describeTransaction(tx, spent, ctx);
|
|
755
|
+
assertSafe(desc, ctx);
|
|
756
|
+
const bytes = serializeTx(tx);
|
|
757
|
+
return { tx, hex: bytesToHex(bytes), txid: txid(tx), fee: plan.fee, plan, desc };
|
|
758
|
+
}
|
|
759
|
+
checkMax(fee, limit, op) {
|
|
760
|
+
if (limit !== void 0 && fee > BigInt(limit)) throw new Error(`Exceeded maximum fee cost for ${op} operation.`);
|
|
761
|
+
}
|
|
762
|
+
/** The outputs a raw transaction spends, fetched through the client (the wire format carries no values). */
|
|
763
|
+
async spentOutputs(tx) {
|
|
764
|
+
const client = this.requireClient("inspect a raw transaction");
|
|
765
|
+
const out = [];
|
|
766
|
+
for (const i of tx.inputs) {
|
|
767
|
+
const o = await client.getOutput(i.txid, i.vout);
|
|
768
|
+
if (!o) throw new Error(`Prevout ${i.txid}:${i.vout} not found.`);
|
|
769
|
+
out.push(o);
|
|
770
|
+
}
|
|
771
|
+
return out;
|
|
772
|
+
}
|
|
773
|
+
/** Describe a raw signed tx (needs the network for prevouts) and run the safety checks. */
|
|
774
|
+
async describeRaw(rawHex) {
|
|
775
|
+
const tx = parseTx(rawHex);
|
|
776
|
+
const ctx = this.ctx();
|
|
777
|
+
const desc = describeTransaction(tx, await this.spentOutputs(tx), ctx);
|
|
778
|
+
assertSafe(desc, ctx);
|
|
779
|
+
return { tx, desc };
|
|
780
|
+
}
|
|
781
|
+
/** "What you sign": the fully resolved spend (signed, NOT broadcast) or a caller's raw hex. */
|
|
782
|
+
async describeTransaction(tx) {
|
|
783
|
+
if (typeof tx === "string") return (await this.describeRaw(tx)).desc;
|
|
784
|
+
return (await this.buildSignedTransaction(tx)).desc;
|
|
785
|
+
}
|
|
786
|
+
/** Sign without broadcasting. Returns the raw transaction hex. Raw hex input is returned unchanged. */
|
|
787
|
+
async signTransaction(tx) {
|
|
788
|
+
this.live();
|
|
789
|
+
if (typeof tx === "string") return tx;
|
|
790
|
+
const built = await this.buildSignedTransaction(tx);
|
|
791
|
+
this.checkMax(built.fee, this._config.transactionMaxFee, "transaction");
|
|
792
|
+
return built.hex;
|
|
793
|
+
}
|
|
794
|
+
async quoteSendTransaction(tx) {
|
|
795
|
+
if (typeof tx === "string") return { fee: (await this.describeRaw(tx)).desc.fee };
|
|
796
|
+
return super.quoteSendTransaction(tx);
|
|
797
|
+
}
|
|
798
|
+
async sendTransaction(tx) {
|
|
799
|
+
this.live();
|
|
800
|
+
const client = this.requireClient("send transactions");
|
|
801
|
+
let rawHex, hash, fee;
|
|
802
|
+
if (typeof tx === "string") {
|
|
803
|
+
const { tx: parsed, desc } = await this.describeRaw(tx);
|
|
804
|
+
rawHex = tx;
|
|
805
|
+
hash = txid(parsed);
|
|
806
|
+
fee = desc.fee;
|
|
807
|
+
} else {
|
|
808
|
+
const built = await this.buildSignedTransaction(tx);
|
|
809
|
+
rawHex = built.hex;
|
|
810
|
+
hash = built.txid;
|
|
811
|
+
fee = built.fee;
|
|
812
|
+
}
|
|
813
|
+
this.checkMax(fee, this._config.transactionMaxFee, "transaction");
|
|
814
|
+
const id = await client.broadcast(rawHex);
|
|
815
|
+
return { hash: id || hash, fee };
|
|
816
|
+
}
|
|
817
|
+
/** Native ZEC only (`token` is rejected). Subject to `transferMaxFee`. */
|
|
818
|
+
async transfer(o) {
|
|
819
|
+
if (o.token) throw new Error("Tokens are not supported on the zcash blockchain.");
|
|
820
|
+
this.live();
|
|
821
|
+
const client = this.requireClient("transfer");
|
|
822
|
+
const built = await this.buildSignedTransaction({ to: o.recipient, value: o.amount });
|
|
823
|
+
this.checkMax(built.fee, this._config.transferMaxFee, "transfer");
|
|
824
|
+
const id = await client.broadcast(built.hex);
|
|
825
|
+
return { hash: id || built.txid, fee: built.fee };
|
|
826
|
+
}
|
|
827
|
+
/** The same address as a {@link WalletAccountReadOnlyZcash}. */
|
|
828
|
+
toReadOnlyAccount() {
|
|
829
|
+
this._readOnly ??= new WalletAccountReadOnlyZcash(this.address, this._config);
|
|
830
|
+
return this._readOnly;
|
|
831
|
+
}
|
|
832
|
+
dispose() {
|
|
833
|
+
this._disposed = true;
|
|
834
|
+
this.signer.dispose();
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
// src/manager.ts
|
|
839
|
+
var coinType = (network) => network === "mainnet" ? 133 : 1;
|
|
840
|
+
var WalletManagerZcash = class extends WalletManager {
|
|
841
|
+
constructor(seedOrSigner, config = {}) {
|
|
842
|
+
super(seedOrSigner, config);
|
|
843
|
+
}
|
|
844
|
+
/** `m/44'/133'` on mainnet (coin type 1 on testnet); account `i` is at `basePath/0'/0/i`. */
|
|
845
|
+
get basePath() {
|
|
846
|
+
return `m/44'/${coinType(this._config.network ?? "mainnet")}'`;
|
|
847
|
+
}
|
|
848
|
+
async getAccount(index = 0) {
|
|
849
|
+
return this.getAccountByPath(`${this.basePath}/0'/0/${index}`);
|
|
850
|
+
}
|
|
851
|
+
async getAccountByPath(path) {
|
|
852
|
+
const full = path.startsWith("m/") ? path : `${this.basePath}/${path}`;
|
|
853
|
+
const cached = this._accounts.get(full);
|
|
854
|
+
if (cached) return cached;
|
|
855
|
+
const external = this.externalSigner();
|
|
856
|
+
if (external) return this.fromSigner(external, full);
|
|
857
|
+
const signer = LocalSigner2.leafFromSeed(this.seedBytes(), "secp256k1", full);
|
|
858
|
+
const account = new WalletAccountZcash(signer, signer.publicKey, full, this._config);
|
|
859
|
+
this._accounts.set(full, account);
|
|
860
|
+
return account;
|
|
861
|
+
}
|
|
862
|
+
async fromSigner(signer, path) {
|
|
863
|
+
this.requireSigner("secp256k1");
|
|
864
|
+
let leaf = signer;
|
|
865
|
+
let key = "external";
|
|
866
|
+
if (isSeedSigner(signer)) {
|
|
867
|
+
if (path === signer.path) key = path;
|
|
868
|
+
else if (path.startsWith(`${signer.path}/`)) {
|
|
869
|
+
leaf = await (signer.deriveLeaf ?? signer.derive).call(signer, path.slice(signer.path.length + 1));
|
|
870
|
+
key = path;
|
|
871
|
+
} else throw new SignerError2(`The injected signer sits at ${signer.path}; it cannot provide ${path}.`);
|
|
872
|
+
} else if (path !== `${this.basePath}/0'/0/0` && path !== "external") {
|
|
873
|
+
throw new SignerError2(`An external signer has one account; ${path} is not derivable from it.`);
|
|
874
|
+
}
|
|
875
|
+
const cached = this._accounts.get(key);
|
|
876
|
+
if (cached) return cached;
|
|
877
|
+
const account = new WalletAccountZcash(leaf, await leaf.getPublicKey(), key, this._config);
|
|
878
|
+
this._accounts.set(key, account);
|
|
879
|
+
return account;
|
|
880
|
+
}
|
|
881
|
+
/** A read-only account for any transparent address, with this manager's config. */
|
|
882
|
+
getReadOnlyAccount(address) {
|
|
883
|
+
return new WalletAccountReadOnlyZcash(address, this._config);
|
|
884
|
+
}
|
|
885
|
+
/** ZIP-317 has no fee market: every tier is the 5000-zatoshi marginal fee per logical action. */
|
|
886
|
+
async getFeeRates() {
|
|
887
|
+
return { slow: MARGINAL_FEE, normal: MARGINAL_FEE, fast: MARGINAL_FEE };
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
export {
|
|
891
|
+
BLOCKCHAIR_URL,
|
|
892
|
+
BlockchairClient,
|
|
893
|
+
CAIP2,
|
|
894
|
+
CURRENT_BRANCH_ID,
|
|
895
|
+
DEFAULT_EXPIRY_DELTA,
|
|
896
|
+
DEFAULT_MAX_FEE,
|
|
897
|
+
DUST_LIMIT,
|
|
898
|
+
FINAL_SEQUENCE,
|
|
899
|
+
GRACE_ACTIONS,
|
|
900
|
+
HistoryUnavailableError,
|
|
901
|
+
MAINNET_UPGRADES,
|
|
902
|
+
MARGINAL_FEE,
|
|
903
|
+
MAX_SEQUENCE,
|
|
904
|
+
MAX_UTXO_INPUTS,
|
|
905
|
+
P2PKH_INPUT_SIZE,
|
|
906
|
+
SIGHASH_ALL,
|
|
907
|
+
TX_VERSION_V5,
|
|
908
|
+
TooManyInputsError,
|
|
909
|
+
UnsafeTransactionError,
|
|
910
|
+
VERSION_GROUP_ID_V5,
|
|
911
|
+
WalletAccountReadOnlyZcash,
|
|
912
|
+
WalletAccountZcash,
|
|
913
|
+
WalletManagerZcash,
|
|
914
|
+
ZcashBlockbookClient,
|
|
915
|
+
addressFor,
|
|
916
|
+
addressFromPublicKey,
|
|
917
|
+
addressFromScript,
|
|
918
|
+
addressPrefix,
|
|
919
|
+
assertSafe,
|
|
920
|
+
blockchairTime,
|
|
921
|
+
branchIdForHeight,
|
|
922
|
+
coinType,
|
|
923
|
+
conventionalFee,
|
|
924
|
+
createClient,
|
|
925
|
+
createZcashHistory,
|
|
926
|
+
decodeAddress,
|
|
927
|
+
decodeCursor as decodeHistoryCursor,
|
|
928
|
+
WalletManagerZcash as default,
|
|
929
|
+
describeTransaction,
|
|
930
|
+
encodeAddress,
|
|
931
|
+
encodeCursor as encodeHistoryCursor,
|
|
932
|
+
encodeMessageSignature,
|
|
933
|
+
estimateSize,
|
|
934
|
+
hash160,
|
|
935
|
+
headerDigest,
|
|
936
|
+
logicalActions,
|
|
937
|
+
magicHash,
|
|
938
|
+
mapDashboardTx,
|
|
939
|
+
outputSize,
|
|
940
|
+
outputsDigest,
|
|
941
|
+
p2pkhScriptSig,
|
|
942
|
+
parseP2pkhScriptSig,
|
|
943
|
+
parseTx,
|
|
944
|
+
prevoutsDigest,
|
|
945
|
+
recoverMessagePublicKey,
|
|
946
|
+
scriptFor,
|
|
947
|
+
scriptKind,
|
|
948
|
+
selectAll,
|
|
949
|
+
selectCoins,
|
|
950
|
+
sequenceDigest,
|
|
951
|
+
serializeTx,
|
|
952
|
+
sighashAll,
|
|
953
|
+
signMessageWith,
|
|
954
|
+
spendableUtxos,
|
|
955
|
+
transparentDigest,
|
|
956
|
+
txHashPersonalization,
|
|
957
|
+
txid,
|
|
958
|
+
txidDigest,
|
|
959
|
+
zcashExplorerTxUrl
|
|
960
|
+
};
|
|
961
|
+
//# sourceMappingURL=index.js.map
|