pivx-wallet 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -105,10 +105,18 @@ export declare class TransparentWallet {
105
105
  * Like the shield wallet's sync this is a chain-data pull, not chain
106
106
  * authentication: point it at a node you trust. See SECURITY.md.
107
107
  */
108
- sync(client: PivxClient, { fromHeight, batchSize, onProgress }?: {
108
+ sync(client: PivxClient, { fromHeight, batchSize, onProgress, signal }?: {
109
109
  fromHeight?: number;
110
110
  batchSize?: number;
111
111
  onProgress?: (height: number, tip: number) => void;
112
+ /**
113
+ * Abort the sync. Checked at every batch and concurrency-chunk
114
+ * boundary, before the next round of RPCs is issued; when set, sync
115
+ * throws `signal.reason` (an `AbortError` DOMException by default).
116
+ * Fully scanned blocks are kept and the busy guard is released, so a
117
+ * follow-up sync resumes where this one stopped.
118
+ */
119
+ signal?: AbortSignal;
112
120
  }): Promise<void>;
113
121
  /**
114
122
  * Total spendable transparent balance in satoshis. Outputs reserved by
@@ -207,11 +207,15 @@ export class TransparentWallet {
207
207
  * Like the shield wallet's sync this is a chain-data pull, not chain
208
208
  * authentication: point it at a node you trust. See SECURITY.md.
209
209
  */
210
- async sync(client, { fromHeight = 0, batchSize = 100, onProgress } = {}) {
210
+ async sync(client, { fromHeight = 0, batchSize = 100, onProgress, signal } = {}) {
211
211
  if (this.busy)
212
212
  throw new Error('wallet is busy: another sync is in progress');
213
213
  this.busy = true;
214
214
  try {
215
+ const throwIfAborted = () => {
216
+ if (signal?.aborted)
217
+ throw signal.reason ?? new DOMException('sync aborted', 'AbortError');
218
+ };
215
219
  const concurrency = 8;
216
220
  const tip = await client.getBlockCount();
217
221
  const fetchBlock = async (h) => client.getBlock(await client.getBlockHash(h), 2);
@@ -220,9 +224,11 @@ export class TransparentWallet {
220
224
  const batch = Math.max(1, Math.floor(batchSize) || 1);
221
225
  let from = Math.max(fromHeight, this.lastScanned + 1);
222
226
  while (from <= tip) {
227
+ throwIfAborted(); // batch boundary: before issuing the next round of RPCs
223
228
  const to = Math.min(from + batch - 1, tip);
224
229
  const heights = Array.from({ length: to - from + 1 }, (_, i) => from + i);
225
230
  for (let i = 0; i < heights.length; i += concurrency) {
231
+ throwIfAborted(); // chunk boundary: previous chunk fully scanned
226
232
  const blocks = await Promise.all(heights.slice(i, i + concurrency).map(fetchBlock));
227
233
  for (const b of blocks) {
228
234
  // getblock verbosity 2 always carries these; a block without them
package/dist/types.d.ts CHANGED
@@ -90,6 +90,14 @@ export interface SyncOptions {
90
90
  */
91
91
  rpcConcurrency?: number;
92
92
  onProgress?: (height: number, tip: number) => void;
93
+ /**
94
+ * Abort the sync. Checked at every batch and concurrency-chunk boundary,
95
+ * before the next round of RPCs is issued; when set, sync throws
96
+ * `signal.reason` (an `AbortError` DOMException by default). State stays
97
+ * consistent: only fully applied, root-verified batches are kept, and the
98
+ * busy guard is released so a follow-up sync can resume where it stopped.
99
+ */
100
+ signal?: AbortSignal;
93
101
  }
94
102
  /** Serialized wallet state (spending key deliberately excluded). */
95
103
  export interface WalletState {
package/dist/wallet.d.ts CHANGED
@@ -60,6 +60,15 @@ export declare class PivxWallet {
60
60
  recipient: string;
61
61
  value: number;
62
62
  } | undefined;
63
+ /**
64
+ * Remove every nullifier-map entry that is no longer referenced by a
65
+ * currently tracked unspent note or by a pending spend, and return the
66
+ * number removed. Explicit and opt-in: the map is what powers
67
+ * {@link getNoteFromNullifier}, so callers using nullifier → note
68
+ * attribution should call this only after reconciling the spends they
69
+ * care about. Deterministic; the save/load format is unchanged.
70
+ */
71
+ pruneNullifiers(): number;
63
72
  /**
64
73
  * Scan blocks (strictly ascending heights, all above the last synced
65
74
  * block). Returns the raw hexes of transactions relevant to this wallet.
package/dist/wallet.js CHANGED
@@ -202,6 +202,28 @@ export class PivxWallet {
202
202
  getNoteFromNullifier(nullifier) {
203
203
  return this.nullifierMap.get(nullifier);
204
204
  }
205
+ /**
206
+ * Remove every nullifier-map entry that is no longer referenced by a
207
+ * currently tracked unspent note or by a pending spend, and return the
208
+ * number removed. Explicit and opt-in: the map is what powers
209
+ * {@link getNoteFromNullifier}, so callers using nullifier → note
210
+ * attribution should call this only after reconciling the spends they
211
+ * care about. Deterministic; the save/load format is unchanged.
212
+ */
213
+ pruneNullifiers() {
214
+ const live = new Set(this.notes.map((n) => n.nullifier));
215
+ for (const nulls of this.pendingSpends.values())
216
+ for (const n of nulls)
217
+ live.add(n);
218
+ let removed = 0;
219
+ for (const nullifier of this.nullifierMap.keys()) {
220
+ if (!live.has(nullifier)) {
221
+ this.nullifierMap.delete(nullifier);
222
+ removed++;
223
+ }
224
+ }
225
+ return removed;
226
+ }
205
227
  // ── Scanning ──────────────────────────────────────────────────────────────
206
228
  /**
207
229
  * Scan blocks (strictly ascending heights, all above the last synced
@@ -286,6 +308,11 @@ export class PivxWallet {
286
308
  throw new Error('wallet is busy: another sync or spend is in progress');
287
309
  this.busy = true;
288
310
  try {
311
+ const throwIfAborted = () => {
312
+ if (opts.signal?.aborted) {
313
+ throw opts.signal.reason ?? new DOMException('sync aborted', 'AbortError');
314
+ }
315
+ };
289
316
  // NaN/0/fractional → sane integer; 0 would loop forever.
290
317
  const batchSize = Math.max(1, Math.floor(opts.batchSize ?? 100) || 1);
291
318
  // getblock verbosity 2 is heavy. A default node has 4 RPC threads and a
@@ -306,11 +333,13 @@ export class PivxWallet {
306
333
  return block;
307
334
  };
308
335
  while (this.lastProcessedBlock < tip) {
336
+ throwIfAborted(); // batch boundary: nothing applied yet, state consistent
309
337
  const from = this.lastProcessedBlock + 1;
310
338
  const to = Math.min(from + batchSize - 1, tip);
311
339
  const heights = Array.from({ length: to - from + 1 }, (_, i) => from + i);
312
340
  const blocks = [];
313
341
  for (let i = 0; i < heights.length; i += concurrency) {
342
+ throwIfAborted(); // chunk boundary: before issuing the next RPCs
314
343
  blocks.push(...(await Promise.all(heights.slice(i, i + concurrency).map(fetchBlock))));
315
344
  }
316
345
  // Snapshot so a failed root check can't leave partial state behind.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pivx-wallet",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
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": {
@@ -44,7 +44,7 @@
44
44
  "@noble/hashes": "^2.2.0",
45
45
  "@scure/base": "^2.2.0",
46
46
  "@scure/bip32": "^2.2.0",
47
- "pivx-rpc": "^0.1.0",
47
+ "pivx-rpc": "^0.3.0",
48
48
  "pivx-shield-rust": "^1.4.0"
49
49
  },
50
50
  "optionalDependencies": {