@shielded-x402/client 0.2.4 → 0.4.0

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.
Files changed (63) hide show
  1. package/dist/agentPaymentFetch.d.ts +56 -0
  2. package/dist/agentPaymentFetch.d.ts.map +1 -0
  3. package/dist/agentPaymentFetch.js +208 -0
  4. package/dist/agentPaymentFetch.js.map +1 -0
  5. package/dist/client.d.ts +2 -5
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +8 -25
  8. package/dist/client.js.map +1 -1
  9. package/dist/counterpartyPolicy.d.ts +9 -0
  10. package/dist/counterpartyPolicy.d.ts.map +1 -0
  11. package/dist/counterpartyPolicy.js +87 -0
  12. package/dist/counterpartyPolicy.js.map +1 -0
  13. package/dist/creditChannel.d.ts +28 -0
  14. package/dist/creditChannel.d.ts.map +1 -0
  15. package/dist/creditChannel.js +143 -0
  16. package/dist/creditChannel.js.map +1 -0
  17. package/dist/creditClose.d.ts +13 -0
  18. package/dist/creditClose.d.ts.map +1 -0
  19. package/dist/creditClose.js +21 -0
  20. package/dist/creditClose.js.map +1 -0
  21. package/dist/creditFetch.d.ts +18 -0
  22. package/dist/creditFetch.d.ts.map +1 -0
  23. package/dist/creditFetch.js +56 -0
  24. package/dist/creditFetch.js.map +1 -0
  25. package/dist/creditSignatures.d.ts +18 -0
  26. package/dist/creditSignatures.d.ts.map +1 -0
  27. package/dist/creditSignatures.js +18 -0
  28. package/dist/creditSignatures.js.map +1 -0
  29. package/dist/crypto.d.ts +1 -1
  30. package/dist/crypto.d.ts.map +1 -1
  31. package/dist/crypto.js +4 -7
  32. package/dist/crypto.js.map +1 -1
  33. package/dist/http.d.ts +10 -0
  34. package/dist/http.d.ts.map +1 -0
  35. package/dist/http.js +27 -0
  36. package/dist/http.js.map +1 -0
  37. package/dist/index.d.ts +7 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +7 -2
  40. package/dist/index.js.map +1 -1
  41. package/dist/proofProvider.d.ts +1 -1
  42. package/dist/proofProvider.d.ts.map +1 -1
  43. package/dist/proofProvider.js +40 -44
  44. package/dist/proofProvider.js.map +1 -1
  45. package/dist/requirementAdapters.d.ts +16 -0
  46. package/dist/requirementAdapters.d.ts.map +1 -0
  47. package/dist/requirementAdapters.js +277 -0
  48. package/dist/requirementAdapters.js.map +1 -0
  49. package/dist/types.d.ts +3 -3
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/walletState.d.ts +20 -4
  52. package/dist/walletState.d.ts.map +1 -1
  53. package/dist/walletState.js +282 -191
  54. package/dist/walletState.js.map +1 -1
  55. package/package.json +4 -3
  56. package/dist/relayerFetch.d.ts +0 -43
  57. package/dist/relayerFetch.d.ts.map +0 -1
  58. package/dist/relayerFetch.js +0 -313
  59. package/dist/relayerFetch.js.map +0 -1
  60. package/dist/shieldedFetch.d.ts +0 -41
  61. package/dist/shieldedFetch.d.ts.map +0 -1
  62. package/dist/shieldedFetch.js +0 -91
  63. package/dist/shieldedFetch.js.map +0 -1
@@ -1,16 +1,16 @@
1
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { dirname } from 'node:path';
3
+ import { normalizeHex } from '@shielded-x402/shared-types';
3
4
  import { createPublicClient, http, parseAbiItem } from 'viem';
4
5
  import { deriveWitness } from './merkle.js';
6
+ import { postJson } from './http.js';
5
7
  const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000';
6
8
  const depositedEvent = parseAbiItem('event Deposited(bytes32 indexed commitment, uint256 indexed leafIndex, bytes32 indexed root, uint256 amount)');
7
9
  const spentEvent = parseAbiItem('event Spent(bytes32 indexed nullifier, bytes32 indexed merchantCommitment, bytes32 indexed changeCommitment, uint256 amount, bytes32 challengeHash, uint256 merchantLeafIndex, uint256 changeLeafIndex, bytes32 newRoot)');
8
- function normalizeHex(value) {
9
- return value.toLowerCase();
10
- }
10
+ const INDEXER_PAGE_SIZE = 500;
11
11
  function serialize(state, poolAddress) {
12
12
  return {
13
- version: 1,
13
+ version: 3,
14
14
  poolAddress,
15
15
  lastSyncedBlock: state.lastSyncedBlock.toString(),
16
16
  commitments: state.commitments,
@@ -18,14 +18,50 @@ function serialize(state, poolAddress) {
18
18
  amount: note.amount.toString(),
19
19
  rho: note.rho,
20
20
  pkHash: note.pkHash,
21
+ nullifierSecret: note.nullifierSecret,
21
22
  commitment: note.commitment,
22
23
  leafIndex: note.leafIndex,
23
24
  ...(note.depositBlock !== undefined ? { depositBlock: note.depositBlock.toString() } : {}),
24
25
  ...(note.spent !== undefined ? { spent: note.spent } : {})
25
- }))
26
+ })),
27
+ creditStates: Object.fromEntries(Object.entries(state.creditStates).map(([channelId, signed]) => [
28
+ channelId,
29
+ {
30
+ state: {
31
+ channelId: signed.state.channelId,
32
+ seq: signed.state.seq,
33
+ available: signed.state.available,
34
+ cumulativeSpent: signed.state.cumulativeSpent,
35
+ lastDebitDigest: signed.state.lastDebitDigest,
36
+ updatedAt: signed.state.updatedAt,
37
+ agentAddress: signed.state.agentAddress,
38
+ relayerAddress: signed.state.relayerAddress
39
+ },
40
+ agentSignature: signed.agentSignature,
41
+ relayerSignature: signed.relayerSignature
42
+ }
43
+ ]))
26
44
  };
27
45
  }
28
46
  function deserialize(payload) {
47
+ const persistedCreditStates = payload.creditStates ?? {};
48
+ const creditStates = {};
49
+ for (const [channelId, signed] of Object.entries(persistedCreditStates)) {
50
+ creditStates[channelId] = {
51
+ state: {
52
+ channelId: signed.state.channelId,
53
+ seq: signed.state.seq,
54
+ available: signed.state.available,
55
+ cumulativeSpent: signed.state.cumulativeSpent,
56
+ lastDebitDigest: signed.state.lastDebitDigest,
57
+ updatedAt: signed.state.updatedAt,
58
+ agentAddress: signed.state.agentAddress,
59
+ relayerAddress: signed.state.relayerAddress
60
+ },
61
+ agentSignature: signed.agentSignature,
62
+ relayerSignature: signed.relayerSignature
63
+ };
64
+ }
29
65
  return {
30
66
  lastSyncedBlock: BigInt(payload.lastSyncedBlock),
31
67
  commitments: payload.commitments,
@@ -33,11 +69,13 @@ function deserialize(payload) {
33
69
  amount: BigInt(note.amount),
34
70
  rho: note.rho,
35
71
  pkHash: note.pkHash,
72
+ nullifierSecret: note.nullifierSecret,
36
73
  commitment: note.commitment,
37
74
  leafIndex: note.leafIndex,
38
75
  ...(note.depositBlock !== undefined ? { depositBlock: BigInt(note.depositBlock) } : {}),
39
76
  ...(note.spent !== undefined ? { spent: note.spent } : {})
40
- }))
77
+ })),
78
+ creditStates
41
79
  };
42
80
  }
43
81
  export class FileBackedWalletState {
@@ -50,6 +88,8 @@ export class FileBackedWalletState {
50
88
  startBlock;
51
89
  indexerFieldNames;
52
90
  state;
91
+ noteIndexesByCommitment;
92
+ leafIndexByCommitment;
53
93
  constructor(config) {
54
94
  this.filePath = config.filePath;
55
95
  this.rpcUrl = config.rpcUrl;
@@ -61,8 +101,12 @@ export class FileBackedWalletState {
61
101
  this.state = {
62
102
  lastSyncedBlock: this.startBlock - 1n,
63
103
  commitments: [],
64
- notes: []
104
+ notes: [],
105
+ creditStates: {}
65
106
  };
107
+ this.noteIndexesByCommitment = new Map();
108
+ this.leafIndexByCommitment = new Map();
109
+ this.rebuildCommitmentIndexes();
66
110
  }
67
111
  static async create(config) {
68
112
  const instance = new FileBackedWalletState(config);
@@ -73,13 +117,14 @@ export class FileBackedWalletState {
73
117
  try {
74
118
  const raw = await readFile(this.filePath, 'utf8');
75
119
  const parsed = JSON.parse(raw);
76
- if (parsed.version !== 1) {
120
+ if (parsed.version !== 3) {
77
121
  throw new Error(`unsupported wallet state version: ${String(parsed.version)}`);
78
122
  }
79
123
  if (normalizeHex(parsed.poolAddress) !== normalizeHex(this.shieldedPoolAddress)) {
80
124
  throw new Error(`wallet state pool mismatch: expected ${this.shieldedPoolAddress}, found ${parsed.poolAddress}`);
81
125
  }
82
126
  this.state = deserialize(parsed);
127
+ this.rebuildCommitmentIndexes();
83
128
  }
84
129
  catch (error) {
85
130
  const message = error instanceof Error ? error.message : String(error);
@@ -106,12 +151,34 @@ export class FileBackedWalletState {
106
151
  getNotes() {
107
152
  return this.state.notes.map((note) => ({ ...note }));
108
153
  }
109
- async addOrUpdateNote(note, depositBlock) {
110
- const normalizedCommitment = normalizeHex(note.commitment);
111
- const existingIndex = this.state.notes.findIndex((candidate) => normalizeHex(candidate.commitment) === normalizedCommitment);
154
+ getCreditState(channelId) {
155
+ const state = this.state.creditStates[channelId.toLowerCase()];
156
+ if (!state)
157
+ return undefined;
158
+ return {
159
+ state: { ...state.state },
160
+ agentSignature: state.agentSignature,
161
+ relayerSignature: state.relayerSignature
162
+ };
163
+ }
164
+ async setCreditState(signedState) {
165
+ this.state.creditStates[signedState.state.channelId.toLowerCase()] = {
166
+ state: { ...signedState.state },
167
+ agentSignature: signedState.agentSignature,
168
+ relayerSignature: signedState.relayerSignature
169
+ };
170
+ await this.persist();
171
+ }
172
+ async clearCreditState(channelId) {
173
+ delete this.state.creditStates[channelId.toLowerCase()];
174
+ await this.persist();
175
+ }
176
+ async addOrUpdateNote(note, nullifierSecret, depositBlock) {
177
+ const existingIndex = this.findNoteIndexByCommitment(note.commitment);
112
178
  const existing = existingIndex >= 0 ? this.state.notes[existingIndex] : undefined;
113
179
  const record = {
114
180
  ...note,
181
+ nullifierSecret,
115
182
  ...(depositBlock !== undefined ? { depositBlock } : {}),
116
183
  ...(existing?.spent !== undefined ? { spent: existing.spent } : {})
117
184
  };
@@ -124,11 +191,11 @@ export class FileBackedWalletState {
124
191
  if (record.leafIndex >= 0) {
125
192
  this.state.commitments[record.leafIndex] = record.commitment;
126
193
  }
194
+ this.rebuildCommitmentIndexes();
127
195
  await this.persist();
128
196
  }
129
197
  async markNoteSpent(commitment) {
130
- const normalized = normalizeHex(commitment);
131
- const existingIndex = this.state.notes.findIndex((candidate) => normalizeHex(candidate.commitment) === normalized);
198
+ const existingIndex = this.findNoteIndexByCommitment(commitment);
132
199
  if (existingIndex < 0) {
133
200
  return false;
134
201
  }
@@ -148,6 +215,7 @@ export class FileBackedWalletState {
148
215
  this.state.commitments[params.merchantLeafIndex] = params.merchantCommitment;
149
216
  this.state.commitments[params.changeLeafIndex] = params.changeCommitment;
150
217
  }
218
+ this.rebuildCommitmentIndexes();
151
219
  await this.persist();
152
220
  }
153
221
  async applyRelayerSettlement(params) {
@@ -165,11 +233,139 @@ export class FileBackedWalletState {
165
233
  });
166
234
  if (params.changeNote) {
167
235
  const changeLeafIndex = delta.changeLeafIndex ?? -1;
236
+ if (!params.changeNullifierSecret) {
237
+ throw new Error('applyRelayerSettlement requires changeNullifierSecret when changeNote is provided');
238
+ }
168
239
  await this.addOrUpdateNote({
169
240
  ...params.changeNote,
170
241
  leafIndex: changeLeafIndex
171
- }, this.state.lastSyncedBlock >= 0n ? this.state.lastSyncedBlock : undefined);
242
+ }, params.changeNullifierSecret, this.state.lastSyncedBlock >= 0n ? this.state.lastSyncedBlock : undefined);
243
+ }
244
+ }
245
+ applyCommitmentWrites(writes) {
246
+ writes.sort((a, b) => {
247
+ if (a.blockNumber !== b.blockNumber)
248
+ return a.blockNumber < b.blockNumber ? -1 : 1;
249
+ if (a.logIndex !== b.logIndex)
250
+ return a.logIndex < b.logIndex ? -1 : 1;
251
+ return a.order - b.order;
252
+ });
253
+ for (const write of writes) {
254
+ if (write.leafIndex < 0)
255
+ continue;
256
+ this.state.commitments[write.leafIndex] = write.commitment;
257
+ const normalizedCommitment = normalizeHex(write.commitment);
258
+ this.leafIndexByCommitment.set(normalizedCommitment, write.leafIndex);
259
+ const noteIndexes = this.noteIndexesByCommitment.get(normalizedCommitment);
260
+ if (!noteIndexes)
261
+ continue;
262
+ for (const noteIndex of noteIndexes) {
263
+ const note = this.state.notes[noteIndex];
264
+ if (!note)
265
+ continue;
266
+ note.leafIndex = write.leafIndex;
267
+ if (note.depositBlock === undefined) {
268
+ note.depositBlock = write.blockNumber;
269
+ }
270
+ }
271
+ }
272
+ }
273
+ rebuildCommitmentIndexes() {
274
+ this.noteIndexesByCommitment.clear();
275
+ for (let i = 0; i < this.state.notes.length; i += 1) {
276
+ const note = this.state.notes[i];
277
+ if (!note)
278
+ continue;
279
+ const normalizedCommitment = normalizeHex(note.commitment);
280
+ const indexes = this.noteIndexesByCommitment.get(normalizedCommitment) ?? [];
281
+ indexes.push(i);
282
+ this.noteIndexesByCommitment.set(normalizedCommitment, indexes);
283
+ }
284
+ this.leafIndexByCommitment.clear();
285
+ for (let i = 0; i < this.state.commitments.length; i += 1) {
286
+ const commitment = this.state.commitments[i];
287
+ if (!commitment)
288
+ continue;
289
+ this.leafIndexByCommitment.set(normalizeHex(commitment), i);
290
+ }
291
+ }
292
+ mapRpcEventsToWrites(params) {
293
+ const writes = [];
294
+ let depositsApplied = 0;
295
+ let spendsApplied = 0;
296
+ for (const log of params.depositLogs) {
297
+ const args = log.args;
298
+ if (args.commitment === undefined || args.leafIndex === undefined)
299
+ continue;
300
+ writes.push({
301
+ blockNumber: log.blockNumber ?? 0n,
302
+ logIndex: log.logIndex ?? 0,
303
+ order: 0,
304
+ leafIndex: Number(args.leafIndex),
305
+ commitment: args.commitment
306
+ });
307
+ depositsApplied += 1;
172
308
  }
309
+ for (const log of params.spendLogs) {
310
+ const args = log.args;
311
+ if (args.merchantCommitment === undefined ||
312
+ args.changeCommitment === undefined ||
313
+ args.merchantLeafIndex === undefined ||
314
+ args.changeLeafIndex === undefined) {
315
+ continue;
316
+ }
317
+ writes.push({
318
+ blockNumber: log.blockNumber ?? 0n,
319
+ logIndex: log.logIndex ?? 0,
320
+ order: 0,
321
+ leafIndex: Number(args.merchantLeafIndex),
322
+ commitment: args.merchantCommitment
323
+ });
324
+ writes.push({
325
+ blockNumber: log.blockNumber ?? 0n,
326
+ logIndex: log.logIndex ?? 0,
327
+ order: 1,
328
+ leafIndex: Number(args.changeLeafIndex),
329
+ commitment: args.changeCommitment
330
+ });
331
+ spendsApplied += 1;
332
+ }
333
+ return { writes, depositsApplied, spendsApplied };
334
+ }
335
+ mapIndexerEventsToWrites(deposits, spends) {
336
+ const writes = [];
337
+ let depositsApplied = 0;
338
+ let spendsApplied = 0;
339
+ for (const deposit of deposits) {
340
+ const { blockNumber, logIndex } = parseEventPositionFromId(deposit.id);
341
+ writes.push({
342
+ blockNumber,
343
+ logIndex,
344
+ order: 0,
345
+ leafIndex: Number(deposit.leafIndex),
346
+ commitment: deposit.commitment
347
+ });
348
+ depositsApplied += 1;
349
+ }
350
+ for (const spend of spends) {
351
+ const { blockNumber, logIndex } = parseEventPositionFromId(spend.id);
352
+ writes.push({
353
+ blockNumber,
354
+ logIndex,
355
+ order: 0,
356
+ leafIndex: Number(spend.merchantLeafIndex),
357
+ commitment: spend.merchantCommitment
358
+ });
359
+ writes.push({
360
+ blockNumber,
361
+ logIndex,
362
+ order: 1,
363
+ leafIndex: Number(spend.changeLeafIndex),
364
+ commitment: spend.changeCommitment
365
+ });
366
+ spendsApplied += 1;
367
+ }
368
+ return { writes, depositsApplied, spendsApplied };
173
369
  }
174
370
  async sync() {
175
371
  if (this.indexerGraphqlUrl) {
@@ -212,68 +408,13 @@ export class FileBackedWalletState {
212
408
  toBlock
213
409
  })
214
410
  ]);
215
- const writes = [];
216
- for (const log of depositLogs) {
217
- const args = log.args;
218
- if (args.commitment === undefined || args.leafIndex === undefined)
219
- continue;
220
- writes.push({
221
- blockNumber: log.blockNumber ?? 0n,
222
- logIndex: log.logIndex ?? 0,
223
- order: 0,
224
- leafIndex: Number(args.leafIndex),
225
- commitment: args.commitment
226
- });
227
- depositsApplied += 1;
228
- }
229
- for (const log of spendLogs) {
230
- const args = log.args;
231
- if (args.merchantCommitment === undefined ||
232
- args.changeCommitment === undefined ||
233
- args.merchantLeafIndex === undefined ||
234
- args.changeLeafIndex === undefined) {
235
- continue;
236
- }
237
- writes.push({
238
- blockNumber: log.blockNumber ?? 0n,
239
- logIndex: log.logIndex ?? 0,
240
- order: 0,
241
- leafIndex: Number(args.merchantLeafIndex),
242
- commitment: args.merchantCommitment
243
- });
244
- writes.push({
245
- blockNumber: log.blockNumber ?? 0n,
246
- logIndex: log.logIndex ?? 0,
247
- order: 1,
248
- leafIndex: Number(args.changeLeafIndex),
249
- commitment: args.changeCommitment
250
- });
251
- spendsApplied += 1;
252
- }
253
- writes.sort((a, b) => {
254
- if (a.blockNumber !== b.blockNumber)
255
- return a.blockNumber < b.blockNumber ? -1 : 1;
256
- if (a.logIndex !== b.logIndex)
257
- return a.logIndex < b.logIndex ? -1 : 1;
258
- return a.order - b.order;
411
+ const mapped = this.mapRpcEventsToWrites({
412
+ depositLogs: depositLogs,
413
+ spendLogs: spendLogs
259
414
  });
260
- for (const write of writes) {
261
- if (write.leafIndex < 0)
262
- continue;
263
- this.state.commitments[write.leafIndex] = write.commitment;
264
- const normalized = normalizeHex(write.commitment);
265
- for (let i = 0; i < this.state.notes.length; i += 1) {
266
- const note = this.state.notes[i];
267
- if (!note)
268
- continue;
269
- if (normalizeHex(note.commitment) !== normalized)
270
- continue;
271
- note.leafIndex = write.leafIndex;
272
- if (note.depositBlock === undefined) {
273
- note.depositBlock = write.blockNumber;
274
- }
275
- }
276
- }
415
+ this.applyCommitmentWrites(mapped.writes);
416
+ depositsApplied += mapped.depositsApplied;
417
+ spendsApplied += mapped.spendsApplied;
277
418
  this.state.lastSyncedBlock = toBlock;
278
419
  await this.persist();
279
420
  cursor = toBlock + 1n;
@@ -288,117 +429,62 @@ export class FileBackedWalletState {
288
429
  async syncFromIndexer() {
289
430
  const fromBlock = this.state.lastSyncedBlock + 1n;
290
431
  const { deposits, spends, maxBlockNumber } = await this.fetchIndexerEvents();
291
- const writes = [];
292
- let depositsApplied = 0;
293
- let spendsApplied = 0;
294
- for (const deposit of deposits) {
295
- const { blockNumber, logIndex } = parseEventPositionFromId(deposit.id);
296
- writes.push({
297
- blockNumber,
298
- logIndex,
299
- order: 0,
300
- leafIndex: Number(deposit.leafIndex),
301
- commitment: deposit.commitment
302
- });
303
- depositsApplied += 1;
304
- }
305
- for (const spend of spends) {
306
- const { blockNumber, logIndex } = parseEventPositionFromId(spend.id);
307
- writes.push({
308
- blockNumber,
309
- logIndex,
310
- order: 0,
311
- leafIndex: Number(spend.merchantLeafIndex),
312
- commitment: spend.merchantCommitment
313
- });
314
- writes.push({
315
- blockNumber,
316
- logIndex,
317
- order: 1,
318
- leafIndex: Number(spend.changeLeafIndex),
319
- commitment: spend.changeCommitment
320
- });
321
- spendsApplied += 1;
322
- }
323
- writes.sort((a, b) => {
324
- if (a.blockNumber !== b.blockNumber)
325
- return a.blockNumber < b.blockNumber ? -1 : 1;
326
- if (a.logIndex !== b.logIndex)
327
- return a.logIndex < b.logIndex ? -1 : 1;
328
- return a.order - b.order;
329
- });
330
- for (const write of writes) {
331
- if (write.leafIndex < 0)
332
- continue;
333
- this.state.commitments[write.leafIndex] = write.commitment;
334
- const normalized = normalizeHex(write.commitment);
335
- for (let i = 0; i < this.state.notes.length; i += 1) {
336
- const note = this.state.notes[i];
337
- if (!note)
338
- continue;
339
- if (normalizeHex(note.commitment) !== normalized)
340
- continue;
341
- note.leafIndex = write.leafIndex;
342
- if (note.depositBlock === undefined) {
343
- note.depositBlock = write.blockNumber;
344
- }
345
- }
346
- }
432
+ const mapped = this.mapIndexerEventsToWrites(deposits, spends);
433
+ this.applyCommitmentWrites(mapped.writes);
347
434
  const targetBlock = maxBlockNumber >= this.state.lastSyncedBlock ? maxBlockNumber : this.state.lastSyncedBlock;
348
435
  this.state.lastSyncedBlock = targetBlock;
349
436
  await this.persist();
350
437
  return {
351
438
  fromBlock,
352
439
  toBlock: targetBlock,
353
- depositsApplied,
354
- spendsApplied
440
+ depositsApplied: mapped.depositsApplied,
441
+ spendsApplied: mapped.spendsApplied
355
442
  };
356
443
  }
357
444
  async fetchIndexerEvents() {
445
+ const { deposits, spends } = await this.fetchIndexerRows();
446
+ return {
447
+ deposits,
448
+ spends,
449
+ maxBlockNumber: this.computeIndexerMaxBlock(deposits, spends)
450
+ };
451
+ }
452
+ async indexerPost(query, variables) {
358
453
  if (!this.indexerGraphqlUrl) {
359
454
  throw new Error('indexerGraphqlUrl is not configured');
360
455
  }
361
- const endpoint = this.indexerGraphqlUrl;
362
- const post = async (query, variables) => {
363
- const response = await fetch(endpoint, {
364
- method: 'POST',
365
- headers: {
366
- 'content-type': 'application/json'
367
- },
368
- body: JSON.stringify({
369
- query,
370
- ...(variables ? { variables } : {})
371
- })
372
- });
373
- if (!response.ok) {
374
- const text = await response.text();
375
- throw new Error(`indexer graphql request failed: ${response.status} ${text}`);
376
- }
377
- const payload = (await response.json());
378
- if (payload.errors && payload.errors.length > 0) {
379
- const message = payload.errors.map((error) => error.message ?? 'unknown error').join('; ');
380
- throw new Error(`indexer graphql error: ${message}`);
381
- }
382
- if (!payload.data) {
383
- throw new Error('indexer graphql response missing data');
384
- }
385
- return payload.data;
386
- };
387
- if (!this.indexerFieldNames) {
388
- const introspection = await post(`query WalletIndexerFields { __schema { queryType { fields { name } } } }`);
389
- const fieldNames = introspection.__schema.queryType.fields.map((field) => field.name);
390
- const pickField = (needle) => fieldNames.find((name) => name.toLowerCase().includes(needle) &&
391
- !name.toLowerCase().includes('aggregate') &&
392
- !name.toLowerCase().includes('by_pk'));
393
- const deposits = pickField('shieldedpool_deposited');
394
- const spends = pickField('shieldedpool_spent');
395
- if (!deposits || !spends) {
396
- throw new Error(`unable to detect Envio fields for deposits/spends at ${endpoint}; available fields: ${fieldNames.join(', ')}`);
397
- }
398
- this.indexerFieldNames = { deposits, spends };
456
+ const payload = await postJson(fetch, this.indexerGraphqlUrl, {
457
+ query,
458
+ ...(variables ? { variables } : {})
459
+ }, { errorPrefix: 'indexer graphql request failed' });
460
+ if (payload.errors && payload.errors.length > 0) {
461
+ const message = payload.errors.map((error) => error.message ?? 'unknown error').join('; ');
462
+ throw new Error(`indexer graphql error: ${message}`);
463
+ }
464
+ if (!payload.data) {
465
+ throw new Error('indexer graphql response missing data');
399
466
  }
400
- const depositsField = this.indexerFieldNames.deposits;
401
- const spendsField = this.indexerFieldNames.spends;
467
+ return payload.data;
468
+ }
469
+ async ensureIndexerFieldNames() {
470
+ if (this.indexerFieldNames) {
471
+ return this.indexerFieldNames;
472
+ }
473
+ const introspection = await this.indexerPost(`query WalletIndexerFields { __schema { queryType { fields { name } } } }`);
474
+ const fieldNames = introspection.__schema.queryType.fields.map((field) => field.name);
475
+ const pickField = (needle) => fieldNames.find((name) => name.toLowerCase().includes(needle) &&
476
+ !name.toLowerCase().includes('aggregate') &&
477
+ !name.toLowerCase().includes('by_pk'));
478
+ const deposits = pickField('shieldedpool_deposited');
479
+ const spends = pickField('shieldedpool_spent');
480
+ if (!deposits || !spends) {
481
+ throw new Error(`unable to detect Envio fields for deposits/spends at ${this.indexerGraphqlUrl}; available fields: ${fieldNames.join(', ')}`);
482
+ }
483
+ this.indexerFieldNames = { deposits, spends };
484
+ return this.indexerFieldNames;
485
+ }
486
+ async fetchIndexerRows() {
487
+ const { deposits: depositsField, spends: spendsField } = await this.ensureIndexerFieldNames();
402
488
  const withPaginationQuery = `
403
489
  query WalletIndexerData($limit: Int!, $offset: Int!) {
404
490
  deposits: ${depositsField}(limit: $limit, offset: $offset) {
@@ -434,23 +520,28 @@ export class FileBackedWalletState {
434
520
  let deposits = [];
435
521
  let spends = [];
436
522
  try {
437
- const pageSize = 500;
438
523
  let offset = 0;
439
524
  while (true) {
440
- const page = await post(withPaginationQuery, { limit: pageSize, offset });
525
+ const page = await this.indexerPost(withPaginationQuery, {
526
+ limit: INDEXER_PAGE_SIZE,
527
+ offset
528
+ });
441
529
  deposits = deposits.concat(page.deposits);
442
530
  spends = spends.concat(page.spends);
443
- if (page.deposits.length < pageSize && page.spends.length < pageSize) {
531
+ if (page.deposits.length < INDEXER_PAGE_SIZE && page.spends.length < INDEXER_PAGE_SIZE) {
444
532
  break;
445
533
  }
446
- offset += pageSize;
534
+ offset += INDEXER_PAGE_SIZE;
447
535
  }
448
536
  }
449
537
  catch {
450
- const data = await post(fullQuery);
538
+ const data = await this.indexerPost(fullQuery);
451
539
  deposits = data.deposits;
452
540
  spends = data.spends;
453
541
  }
542
+ return { deposits, spends };
543
+ }
544
+ computeIndexerMaxBlock(deposits, spends) {
454
545
  let maxBlockNumber = this.state.lastSyncedBlock;
455
546
  for (const deposit of deposits) {
456
547
  const position = parseEventPositionFromId(deposit.id);
@@ -464,11 +555,11 @@ export class FileBackedWalletState {
464
555
  maxBlockNumber = position.blockNumber;
465
556
  }
466
557
  }
467
- return { deposits, spends, maxBlockNumber };
558
+ return maxBlockNumber;
468
559
  }
469
- getSpendContextByCommitment(commitment, payerPkHash) {
470
- const normalized = normalizeHex(commitment);
471
- const note = this.state.notes.find((candidate) => normalizeHex(candidate.commitment) === normalized);
560
+ getSpendContextByCommitment(commitment) {
561
+ const noteIndex = this.findNoteIndexByCommitment(commitment);
562
+ const note = noteIndex >= 0 ? this.state.notes[noteIndex] : undefined;
472
563
  if (!note) {
473
564
  throw new Error(`note not found in wallet state for commitment ${commitment}; add note secrets first with addOrUpdateNote()`);
474
565
  }
@@ -488,19 +579,19 @@ export class FileBackedWalletState {
488
579
  return {
489
580
  note: { ...note, leafIndex },
490
581
  witness,
491
- payerPkHash
582
+ nullifierSecret: note.nullifierSecret
492
583
  };
493
584
  }
585
+ findNoteIndexByCommitment(commitment) {
586
+ const normalized = normalizeHex(commitment);
587
+ const noteIndexes = this.noteIndexesByCommitment.get(normalized);
588
+ if (!noteIndexes || noteIndexes.length === 0)
589
+ return -1;
590
+ return noteIndexes[0] ?? -1;
591
+ }
494
592
  findLeafIndex(commitment) {
495
593
  const normalized = normalizeHex(commitment);
496
- for (let i = this.state.commitments.length - 1; i >= 0; i -= 1) {
497
- const candidate = this.state.commitments[i];
498
- if (!candidate)
499
- continue;
500
- if (normalizeHex(candidate) === normalized)
501
- return i;
502
- }
503
- return -1;
594
+ return this.leafIndexByCommitment.get(normalized) ?? -1;
504
595
  }
505
596
  }
506
597
  function parseEventPositionFromId(id) {