pivx-wallet 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/transparent-wallet.d.ts +1 -0
- package/dist/transparent-wallet.js +83 -1
- package/dist/wallet.d.ts +9 -0
- package/dist/wallet.js +95 -48
- package/package.json +1 -1
|
@@ -14,6 +14,14 @@ const isHex = (s) => /^(?:[0-9a-fA-F]{2})+$/.test(s);
|
|
|
14
14
|
const dustThreshold = (scriptLen) => Math.floor((30_000 * (8 + 1 + scriptLen + 148)) / 1000);
|
|
15
15
|
/** Coinbase/coinstake maturity in blocks (nCoinbaseMaturity): mainnet 100, testnet 15. */
|
|
16
16
|
const coinbaseMaturity = (network) => (network === 'mainnet' ? 100 : 15);
|
|
17
|
+
/**
|
|
18
|
+
* Depth of the rolling (height, hash) window kept for reorg recovery. On a
|
|
19
|
+
* detected same-height tip reorg, sync walks this window newest→oldest to find
|
|
20
|
+
* the true fork and resets there; a reorg deeper than the window cannot be
|
|
21
|
+
* located safely and fails loud instead of silently retaining orphaned UTXOs.
|
|
22
|
+
* Identical in the Rust SDK.
|
|
23
|
+
*/
|
|
24
|
+
const REORG_WINDOW = 100;
|
|
17
25
|
/**
|
|
18
26
|
* hash160 of a scriptPubKey we know how to spend, if it is one: a standard
|
|
19
27
|
* 25-byte P2PKH (76a914<20>88ac) or the 26-byte exchange form with an
|
|
@@ -43,6 +51,7 @@ export class TransparentWallet {
|
|
|
43
51
|
utxos = new Map(); // "txid:vout" → utxo
|
|
44
52
|
lastScanned = 0; // height of the last block passed to scanBlock
|
|
45
53
|
lastScannedHash = null; // hash of that block, for reorg detection
|
|
54
|
+
scannedHashes = []; // rolling window of recent (height, hash) for the reorg walk-back
|
|
46
55
|
pending = new Set(); // "txid:vout" reserved by buildSend until markSpent/release
|
|
47
56
|
/** One sync at a time (mirrors the shield wallet's busy guard). */
|
|
48
57
|
busy = false;
|
|
@@ -175,6 +184,14 @@ export class TransparentWallet {
|
|
|
175
184
|
}
|
|
176
185
|
}
|
|
177
186
|
this.lastScannedHash = typeof block.hash === 'string' ? block.hash : null;
|
|
187
|
+
// Record this block into the rolling reorg window (same usable-hash guard as
|
|
188
|
+
// lastScannedHash), keeping only the last REORG_WINDOW entries.
|
|
189
|
+
if (typeof block.hash === 'string') {
|
|
190
|
+
this.scannedHashes.push({ height, hash: block.hash });
|
|
191
|
+
if (this.scannedHashes.length > REORG_WINDOW) {
|
|
192
|
+
this.scannedHashes.splice(0, this.scannedHashes.length - REORG_WINDOW);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
178
195
|
}
|
|
179
196
|
/** Height of the last block passed to {@link scanBlock} (0 if none). */
|
|
180
197
|
lastScannedBlock() {
|
|
@@ -193,8 +210,13 @@ export class TransparentWallet {
|
|
|
193
210
|
this.pending.delete(k);
|
|
194
211
|
}
|
|
195
212
|
}
|
|
213
|
+
// Trim the reorg window to the retained span; if the reset height is itself a
|
|
214
|
+
// known window entry (a true fork), restore its hash so scan continuity is
|
|
215
|
+
// preserved — otherwise (a manual reset to an unknown height) leave it null.
|
|
216
|
+
this.scannedHashes = this.scannedHashes.filter((e) => e.height <= height);
|
|
217
|
+
const retained = this.scannedHashes.find((e) => e.height === height);
|
|
196
218
|
this.lastScanned = height;
|
|
197
|
-
this.lastScannedHash = null;
|
|
219
|
+
this.lastScannedHash = retained ? retained.hash : null;
|
|
198
220
|
}
|
|
199
221
|
/**
|
|
200
222
|
* Sync from the node into the wallet, from `max(fromHeight, lastScanned+1)`
|
|
@@ -218,6 +240,36 @@ export class TransparentWallet {
|
|
|
218
240
|
};
|
|
219
241
|
const concurrency = 8;
|
|
220
242
|
const tip = await client.getBlockCount();
|
|
243
|
+
// Stale-tip reorg detection: the forward scan only checks parent-hash
|
|
244
|
+
// continuity for blocks above lastScanned, so a same-height reorg (block N
|
|
245
|
+
// replaced while the tip stays N) leaves lastScanned == tip and is missed.
|
|
246
|
+
// Any reorg at/below lastScanned changes that block's hash, so re-verify
|
|
247
|
+
// the node's current hash for it. On a mismatch, walk the recorded window
|
|
248
|
+
// newest→oldest to find the true fork (the highest stored height whose
|
|
249
|
+
// hash the node still confirms) and reset there; the UTXO model self-heals
|
|
250
|
+
// (resetScan drops UTXOs above the fork and the re-scan re-credits the
|
|
251
|
+
// survivors). If the reorg is deeper than the window we cannot locate the
|
|
252
|
+
// fork safely, so fail loud rather than silently retain orphaned UTXOs.
|
|
253
|
+
// Honest chain = 1 getBlockHash (tip matches → skip the walk).
|
|
254
|
+
if (this.lastScanned > 0 && this.lastScannedHash !== null) {
|
|
255
|
+
const nodeTip = await client.getBlockHash(this.lastScanned);
|
|
256
|
+
if (nodeTip !== this.lastScannedHash) {
|
|
257
|
+
let fork;
|
|
258
|
+
for (let i = this.scannedHashes.length - 1; i >= 0; i--) {
|
|
259
|
+
const entry = this.scannedHashes[i];
|
|
260
|
+
if ((await client.getBlockHash(entry.height)) === entry.hash) {
|
|
261
|
+
fork = entry.height;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (fork !== undefined) {
|
|
266
|
+
this.resetScan(fork);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
throw new ScanDivergedError(this.lastScanned, this.lastScannedHash, nodeTip);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
221
273
|
const fetchBlock = async (h) => client.getBlock(await client.getBlockHash(h), 2);
|
|
222
274
|
// NaN/0/fractional → sane integer: 0 would loop forever and fractional
|
|
223
275
|
// heights would skip blocks (matches Rust batch.max(1)).
|
|
@@ -274,6 +326,13 @@ export class TransparentWallet {
|
|
|
274
326
|
* (broadcast definitively rejected) resolves them.
|
|
275
327
|
*/
|
|
276
328
|
buildSend(to, amount, feePerByte = 100) {
|
|
329
|
+
// A sync running in the event-loop gap (suspended on an RPC await) may be
|
|
330
|
+
// mid-reorg reset; selecting/reserving a UTXO now risks spending an output
|
|
331
|
+
// resetScan is about to drop. Refuse until the sync releases the guard.
|
|
332
|
+
// (markSpent/release stay unguarded: removal-only finalization must always
|
|
333
|
+
// complete, or a reservation leaks.)
|
|
334
|
+
if (this.busy)
|
|
335
|
+
throw new Error('wallet is busy: a sync is in progress');
|
|
277
336
|
if (!Number.isSafeInteger(amount) || amount <= 0)
|
|
278
337
|
throw new Error('amount must be a positive integer (satoshis)');
|
|
279
338
|
if (!Number.isInteger(feePerByte) || feePerByte <= 0)
|
|
@@ -364,6 +423,9 @@ export class TransparentWallet {
|
|
|
364
423
|
nextChange: this.nextChange,
|
|
365
424
|
lastScanned: this.lastScanned,
|
|
366
425
|
lastScannedHash: this.lastScannedHash,
|
|
426
|
+
// Rolling reorg window (ascending by height, newest last). Emitted here —
|
|
427
|
+
// after lastScannedHash, before utxos — for byte parity with the Rust SDK.
|
|
428
|
+
scannedHashes: this.scannedHashes.map((e) => ({ height: e.height, hash: e.hash })),
|
|
367
429
|
// Sorted by (txid, vout) so save() output is deterministic and
|
|
368
430
|
// byte-comparable with the Rust SDK's.
|
|
369
431
|
utxos: [...this.utxos.values()]
|
|
@@ -423,6 +485,10 @@ export class TransparentWallet {
|
|
|
423
485
|
if (!Array.isArray(s.utxos) || !Array.isArray(s.pending)) {
|
|
424
486
|
throw new Error('wallet state has invalid utxo or pending lists');
|
|
425
487
|
}
|
|
488
|
+
// Backward-compatible: older states have no window → treat as empty.
|
|
489
|
+
if (s.scannedHashes !== undefined && !Array.isArray(s.scannedHashes)) {
|
|
490
|
+
throw new Error('wallet state has an invalid scanned-hash window');
|
|
491
|
+
}
|
|
426
492
|
const w = TransparentWallet.create(seed, s.network, s.account, s.gap);
|
|
427
493
|
w.nextExternal = s.nextExternal;
|
|
428
494
|
w.nextChange = s.nextChange;
|
|
@@ -462,6 +528,22 @@ export class TransparentWallet {
|
|
|
462
528
|
}
|
|
463
529
|
w.pending.add(`${p.txid}:${p.vout}`);
|
|
464
530
|
}
|
|
531
|
+
for (const e of s.scannedHashes ?? []) {
|
|
532
|
+
// hash is string-checked (not hex-validated), matching lastScannedHash
|
|
533
|
+
// and the Rust SDK so a state loads in both or neither.
|
|
534
|
+
if (!isCount(e?.height) || typeof e.hash !== 'string') {
|
|
535
|
+
throw new Error('wallet state contains a malformed scanned-hash entry');
|
|
536
|
+
}
|
|
537
|
+
w.scannedHashes.push({ height: e.height, hash: e.hash });
|
|
538
|
+
}
|
|
539
|
+
// Honest save() output is ascending, unique, ≤ REORG_WINDOW entries, all
|
|
540
|
+
// heights ≤ lastScanned. The reorg walk-back trusts array order and entry
|
|
541
|
+
// heights, so reject anything else rather than let a hostile or corrupt
|
|
542
|
+
// state mislead it (this also keeps duplicates out of resetScan).
|
|
543
|
+
if (w.scannedHashes.length > REORG_WINDOW ||
|
|
544
|
+
w.scannedHashes.some((e, i) => e.height > w.lastScanned || (i > 0 && e.height <= w.scannedHashes[i - 1].height))) {
|
|
545
|
+
throw new Error('wallet state contains an invalid scanned-hash window');
|
|
546
|
+
}
|
|
465
547
|
return w;
|
|
466
548
|
}
|
|
467
549
|
}
|
package/dist/wallet.d.ts
CHANGED
|
@@ -104,6 +104,15 @@ export declare class PivxWallet {
|
|
|
104
104
|
* SECURITY.md.
|
|
105
105
|
*/
|
|
106
106
|
sync(client: PivxClient, opts?: SyncOptions): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* The node's finalsaplingroot at height h, or null below the SAPLING_ACTIVATION
|
|
109
|
+
* threshold — below real V5_0 the node reports a zero root our non-zero empty
|
|
110
|
+
* tree can't match, so returning null signals "skip the check". At/above the
|
|
111
|
+
* threshold the node must report one; treating an omitted root as "no root"
|
|
112
|
+
* would let a node suppress the check or force an all-the-way rewind by simply
|
|
113
|
+
* withholding the field.
|
|
114
|
+
*/
|
|
115
|
+
private nodeSaplingRoot;
|
|
107
116
|
/**
|
|
108
117
|
* Confirm the starting commitment tree against the node before scanning
|
|
109
118
|
* forward. A fresh wallet begins at a bundled checkpoint; if that
|
package/dist/wallet.js
CHANGED
|
@@ -2,9 +2,12 @@ import { RpcError } from 'pivx-rpc';
|
|
|
2
2
|
import { loadShield } from './shield-bindings.js';
|
|
3
3
|
/** PIVX BIP44 coin types. */
|
|
4
4
|
const COIN_TYPE = { mainnet: 119, testnet: 1 };
|
|
5
|
-
/**
|
|
6
|
-
* the
|
|
7
|
-
|
|
5
|
+
/** Threshold at/above which the sapling root check runs; below it we skip. These
|
|
6
|
+
* are the exact UPGRADE_V5_0 activation heights (inclusive) from PIVX consensus,
|
|
7
|
+
* so at/above them an honest node always reports a real, matchable
|
|
8
|
+
* finalsaplingroot, and below them (no shielded txs yet) there is nothing to
|
|
9
|
+
* verify. Mainnet V5_0 is 2_700_500; testnet is 201. */
|
|
10
|
+
const SAPLING_ACTIVATION = { mainnet: 2_700_500, testnet: 201 };
|
|
8
11
|
/** A note worth no more than its own input fee (sapling input 384 bytes ×
|
|
9
12
|
* 1000 sats/byte) never helps cover amount+fee, so it is never spent. */
|
|
10
13
|
const DUST_NOTE_SATS = 384_000;
|
|
@@ -332,6 +335,32 @@ export class PivxWallet {
|
|
|
332
335
|
}
|
|
333
336
|
return block;
|
|
334
337
|
};
|
|
338
|
+
// Stale-tip reorg detection: when lastProcessedBlock === tip the batch
|
|
339
|
+
// loop below never runs, so its per-batch root check never fires. A
|
|
340
|
+
// same-height reorg (block N replaced while the tip stays N) changes the
|
|
341
|
+
// shielded set's root but not the height. The tree can't be cheaply
|
|
342
|
+
// rewound, so re-verify the tip's finalsaplingroot against our local
|
|
343
|
+
// commitment root — the same localRoot-vs-nodeRoot check the batch loop
|
|
344
|
+
// does — and diverge on mismatch (caller recovers via reloadFromCheckpoint).
|
|
345
|
+
// Runs even at an exact checkpoint height: a fresh wallet still on its
|
|
346
|
+
// bundled checkpoint matches the node's finalsaplingroot there (that is
|
|
347
|
+
// what a checkpoint is, and ensureValidCheckpoint already confirmed it),
|
|
348
|
+
// so it is a clean no-op — while a wallet that scanned up to a checkpoint
|
|
349
|
+
// height and was then same-height reorged finally gets the check it was
|
|
350
|
+
// missing. Route the fetch through nodeSaplingRoot so it is skipped below
|
|
351
|
+
// the SAPLING_ACTIVATION threshold: below real V5_0 the node reports a
|
|
352
|
+
// zero root our non-zero empty tree can't match, so an honest wallet would
|
|
353
|
+
// false-diverge — the same activation exception ensureValidCheckpoint
|
|
354
|
+
// relies on. nodeSaplingRoot's verbosity-1 getblock returns
|
|
355
|
+
// finalsaplingroot without the full tx list the batch fetchBlock needs.
|
|
356
|
+
if (this.lastProcessedBlock === tip) {
|
|
357
|
+
const nodeRoot = await this.nodeSaplingRoot(client, tip);
|
|
358
|
+
if (nodeRoot !== null) {
|
|
359
|
+
const localRoot = reverseHex(this.shield.get_sapling_root(this.commitmentTree));
|
|
360
|
+
if (localRoot !== nodeRoot)
|
|
361
|
+
throw new ScanDivergedError(tip, localRoot, nodeRoot);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
335
364
|
while (this.lastProcessedBlock < tip) {
|
|
336
365
|
throwIfAborted(); // batch boundary: nothing applied yet, state consistent
|
|
337
366
|
const from = this.lastProcessedBlock + 1;
|
|
@@ -356,15 +385,25 @@ export class PivxWallet {
|
|
|
356
385
|
height: h,
|
|
357
386
|
txs: blocks[i].tx.map(({ hex, txid }) => ({ hex, txid })),
|
|
358
387
|
})));
|
|
359
|
-
|
|
360
|
-
//
|
|
361
|
-
// the node
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
388
|
+
// Verify the locally-built tree against the node's finalsaplingroot,
|
|
389
|
+
// except below the SAPLING_ACTIVATION threshold, where we skip: below
|
|
390
|
+
// real V5_0 the node reports a zero root our non-zero empty tree can't
|
|
391
|
+
// match, and no shielded txs exist below activation anyway, so there
|
|
392
|
+
// is nothing to verify. Both networks can scan into the skip window: a
|
|
393
|
+
// testnet wallet with a below-activation birth height resumes from the
|
|
394
|
+
// height-0 empty-tree checkpoint, and a mainnet wallet resumes from
|
|
395
|
+
// checkpoint 2_700_000 and scans the empty [2_700_001, 2_700_500) gap
|
|
396
|
+
// below its real activation. Same exception nodeSaplingRoot encodes.
|
|
397
|
+
if (to >= SAPLING_ACTIVATION[this.network]) {
|
|
398
|
+
const nodeRoot = blocks[blocks.length - 1].finalsaplingroot;
|
|
399
|
+
// A shielded chain always has a sapling root; a missing one past
|
|
400
|
+
// activation means the node is lying. Refuse to advance unverified.
|
|
401
|
+
if (!nodeRoot)
|
|
402
|
+
throw new Error(`node omitted finalsaplingroot at height ${to}`);
|
|
403
|
+
const localRoot = reverseHex(this.shield.get_sapling_root(this.commitmentTree));
|
|
404
|
+
if (localRoot !== nodeRoot)
|
|
405
|
+
throw new ScanDivergedError(to, localRoot, nodeRoot);
|
|
406
|
+
}
|
|
368
407
|
}
|
|
369
408
|
catch (err) {
|
|
370
409
|
this.commitmentTree = snapshot.tree;
|
|
@@ -381,6 +420,23 @@ export class PivxWallet {
|
|
|
381
420
|
this.busy = false;
|
|
382
421
|
}
|
|
383
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* The node's finalsaplingroot at height h, or null below the SAPLING_ACTIVATION
|
|
425
|
+
* threshold — below real V5_0 the node reports a zero root our non-zero empty
|
|
426
|
+
* tree can't match, so returning null signals "skip the check". At/above the
|
|
427
|
+
* threshold the node must report one; treating an omitted root as "no root"
|
|
428
|
+
* would let a node suppress the check or force an all-the-way rewind by simply
|
|
429
|
+
* withholding the field.
|
|
430
|
+
*/
|
|
431
|
+
async nodeSaplingRoot(client, h) {
|
|
432
|
+
if (h < SAPLING_ACTIVATION[this.network])
|
|
433
|
+
return null;
|
|
434
|
+
const block = (await client.getBlock(await client.getBlockHash(h), 1));
|
|
435
|
+
if (!block.finalsaplingroot) {
|
|
436
|
+
throw new Error(`node omitted finalsaplingroot at height ${h} (past sapling activation)`);
|
|
437
|
+
}
|
|
438
|
+
return block.finalsaplingroot;
|
|
439
|
+
}
|
|
384
440
|
/**
|
|
385
441
|
* Confirm the starting commitment tree against the node before scanning
|
|
386
442
|
* forward. A fresh wallet begins at a bundled checkpoint; if that
|
|
@@ -393,21 +449,8 @@ export class PivxWallet {
|
|
|
393
449
|
async ensureValidCheckpoint(client) {
|
|
394
450
|
if (this.startValidated)
|
|
395
451
|
return;
|
|
396
|
-
const activation = SAPLING_ACTIVATION[this.network];
|
|
397
|
-
// Sapling root at height h. Above activation the node must report one;
|
|
398
|
-
// treating an omitted root as "no root" would let a node suppress this
|
|
399
|
-
// check or force an all-the-way rewind by simply withholding the field.
|
|
400
|
-
const rootAt = async (h) => {
|
|
401
|
-
if (h < activation)
|
|
402
|
-
return null;
|
|
403
|
-
const block = (await client.getBlock(await client.getBlockHash(h), 1));
|
|
404
|
-
if (!block.finalsaplingroot) {
|
|
405
|
-
throw new Error(`node omitted finalsaplingroot at height ${h} (past sapling activation)`);
|
|
406
|
-
}
|
|
407
|
-
return block.finalsaplingroot;
|
|
408
|
-
};
|
|
409
452
|
const localRoot = () => reverseHex(this.shield.get_sapling_root(this.commitmentTree));
|
|
410
|
-
const node = await
|
|
453
|
+
const node = await this.nodeSaplingRoot(client, this.lastProcessedBlock);
|
|
411
454
|
if (node === null || localRoot() === node) {
|
|
412
455
|
this.startValidated = true;
|
|
413
456
|
return;
|
|
@@ -429,7 +472,7 @@ export class PivxWallet {
|
|
|
429
472
|
if (cpHeight >= lastCp)
|
|
430
473
|
break; // no older checkpoint available
|
|
431
474
|
lastCp = cpHeight;
|
|
432
|
-
const nodeRoot = await
|
|
475
|
+
const nodeRoot = await this.nodeSaplingRoot(client, cpHeight);
|
|
433
476
|
const cpRoot = reverseHex(this.shield.get_sapling_root(cpTree));
|
|
434
477
|
if (nodeRoot === null || cpRoot === nodeRoot) {
|
|
435
478
|
this.commitmentTree = cpTree;
|
|
@@ -511,31 +554,35 @@ export class PivxWallet {
|
|
|
511
554
|
}
|
|
512
555
|
}
|
|
513
556
|
}
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
const spendable = this.notes.filter((n) => !pending.has(n.nullifier) && n.note.value > DUST_NOTE_SATS);
|
|
519
|
-
// Refuse a send the inputs can't cover including the fee, unless the
|
|
520
|
-
// caller opts into sweep. Both branches mirror the Rust selection so the
|
|
521
|
-
// WASM can't silently pay the fee out of the recipient's amount — the
|
|
522
|
-
// WASM shares the same fee model but has no such guard.
|
|
523
|
-
const sufficient = useShield
|
|
524
|
-
? estimateShieldSelection(spendable, opts.amount, this.isShieldAddress(opts.to)).sufficient
|
|
525
|
-
: estimateTransparentSelection(opts.inputs, opts.amount, this.isShieldAddress(opts.to)).sufficient;
|
|
526
|
-
if (!sufficient && !opts.sweep) {
|
|
527
|
-
throw new Error('insufficient input value to cover amount + fee; lower the amount, ' +
|
|
528
|
-
'add inputs, or pass sweep:true to deduct the fee from the recipient');
|
|
529
|
-
}
|
|
530
|
-
// Prover is only needed to build; check it after the cheap validations
|
|
531
|
-
// so callers get input errors without loading ~50MB of parameters.
|
|
532
|
-
if (!(await this.shield.prover_is_loaded())) {
|
|
533
|
-
throw new Error('sapling prover not loaded: call loadProver() first');
|
|
534
|
-
}
|
|
557
|
+
// Acquire the single-writer guard BEFORE snapshotting spendable notes or
|
|
558
|
+
// awaiting anything. Otherwise two concurrent createTransaction calls could
|
|
559
|
+
// each snapshot the same notes before either set `busy`, then serialize on
|
|
560
|
+
// the guard and both build with the same inputs — a double-spend.
|
|
535
561
|
if (this.busy)
|
|
536
562
|
throw new Error('wallet is busy: another sync or spend is in progress');
|
|
537
563
|
this.busy = true;
|
|
538
564
|
try {
|
|
565
|
+
// Spendable notes, minus pending spends and dust. Dust notes (worth no
|
|
566
|
+
// more than their own input fee) can never help cover amount+fee and only
|
|
567
|
+
// let an attacker inflate the fee, so they are excluded from spending.
|
|
568
|
+
const pending = new Set([...this.pendingSpends.values()].flat());
|
|
569
|
+
const spendable = this.notes.filter((n) => !pending.has(n.nullifier) && n.note.value > DUST_NOTE_SATS);
|
|
570
|
+
// Refuse a send the inputs can't cover including the fee, unless the
|
|
571
|
+
// caller opts into sweep. Both branches mirror the Rust selection so the
|
|
572
|
+
// WASM can't silently pay the fee out of the recipient's amount — the
|
|
573
|
+
// WASM shares the same fee model but has no such guard.
|
|
574
|
+
const sufficient = useShield
|
|
575
|
+
? estimateShieldSelection(spendable, opts.amount, this.isShieldAddress(opts.to)).sufficient
|
|
576
|
+
: estimateTransparentSelection(opts.inputs, opts.amount, this.isShieldAddress(opts.to)).sufficient;
|
|
577
|
+
if (!sufficient && !opts.sweep) {
|
|
578
|
+
throw new Error('insufficient input value to cover amount + fee; lower the amount, ' +
|
|
579
|
+
'add inputs, or pass sweep:true to deduct the fee from the recipient');
|
|
580
|
+
}
|
|
581
|
+
// Prover is only needed to build; check it after the cheap validations
|
|
582
|
+
// so callers get input errors without loading ~50MB of parameters.
|
|
583
|
+
if (!(await this.shield.prover_is_loaded())) {
|
|
584
|
+
throw new Error('sapling prover not loaded: call loadProver() first');
|
|
585
|
+
}
|
|
539
586
|
const changeAddress = useShield ? this.getNewAddress() : opts.transparentChangeAddress;
|
|
540
587
|
const result = (await this.shield.create_transaction({
|
|
541
588
|
notes: useShield ? spendable : null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pivx-wallet",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Standalone PIVX wallet SDK: local key management, shielded (SHIELD/Sapling) scanning, balances, and transaction building. The node is only a chain-data source.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|