@shielded-x402/client 0.3.0 → 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 +1 -4
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +2 -22
  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 +0 -3
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/walletState.d.ts +15 -1
  52. package/dist/walletState.d.ts.map +1 -1
  53. package/dist/walletState.js +272 -187
  54. package/dist/walletState.js.map +1 -1
  55. package/package.json +3 -2
  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: 2,
13
+ version: 3,
14
14
  poolAddress,
15
15
  lastSyncedBlock: state.lastSyncedBlock.toString(),
16
16
  commitments: state.commitments,
@@ -23,10 +23,45 @@ function serialize(state, poolAddress) {
23
23
  leafIndex: note.leafIndex,
24
24
  ...(note.depositBlock !== undefined ? { depositBlock: note.depositBlock.toString() } : {}),
25
25
  ...(note.spent !== undefined ? { spent: note.spent } : {})
26
- }))
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
+ ]))
27
44
  };
28
45
  }
29
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
+ }
30
65
  return {
31
66
  lastSyncedBlock: BigInt(payload.lastSyncedBlock),
32
67
  commitments: payload.commitments,
@@ -39,7 +74,8 @@ function deserialize(payload) {
39
74
  leafIndex: note.leafIndex,
40
75
  ...(note.depositBlock !== undefined ? { depositBlock: BigInt(note.depositBlock) } : {}),
41
76
  ...(note.spent !== undefined ? { spent: note.spent } : {})
42
- }))
77
+ })),
78
+ creditStates
43
79
  };
44
80
  }
45
81
  export class FileBackedWalletState {
@@ -52,6 +88,8 @@ export class FileBackedWalletState {
52
88
  startBlock;
53
89
  indexerFieldNames;
54
90
  state;
91
+ noteIndexesByCommitment;
92
+ leafIndexByCommitment;
55
93
  constructor(config) {
56
94
  this.filePath = config.filePath;
57
95
  this.rpcUrl = config.rpcUrl;
@@ -63,8 +101,12 @@ export class FileBackedWalletState {
63
101
  this.state = {
64
102
  lastSyncedBlock: this.startBlock - 1n,
65
103
  commitments: [],
66
- notes: []
104
+ notes: [],
105
+ creditStates: {}
67
106
  };
107
+ this.noteIndexesByCommitment = new Map();
108
+ this.leafIndexByCommitment = new Map();
109
+ this.rebuildCommitmentIndexes();
68
110
  }
69
111
  static async create(config) {
70
112
  const instance = new FileBackedWalletState(config);
@@ -75,13 +117,14 @@ export class FileBackedWalletState {
75
117
  try {
76
118
  const raw = await readFile(this.filePath, 'utf8');
77
119
  const parsed = JSON.parse(raw);
78
- if (parsed.version !== 2) {
120
+ if (parsed.version !== 3) {
79
121
  throw new Error(`unsupported wallet state version: ${String(parsed.version)}`);
80
122
  }
81
123
  if (normalizeHex(parsed.poolAddress) !== normalizeHex(this.shieldedPoolAddress)) {
82
124
  throw new Error(`wallet state pool mismatch: expected ${this.shieldedPoolAddress}, found ${parsed.poolAddress}`);
83
125
  }
84
126
  this.state = deserialize(parsed);
127
+ this.rebuildCommitmentIndexes();
85
128
  }
86
129
  catch (error) {
87
130
  const message = error instanceof Error ? error.message : String(error);
@@ -108,9 +151,30 @@ export class FileBackedWalletState {
108
151
  getNotes() {
109
152
  return this.state.notes.map((note) => ({ ...note }));
110
153
  }
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
+ }
111
176
  async addOrUpdateNote(note, nullifierSecret, depositBlock) {
112
- const normalizedCommitment = normalizeHex(note.commitment);
113
- const existingIndex = this.state.notes.findIndex((candidate) => normalizeHex(candidate.commitment) === normalizedCommitment);
177
+ const existingIndex = this.findNoteIndexByCommitment(note.commitment);
114
178
  const existing = existingIndex >= 0 ? this.state.notes[existingIndex] : undefined;
115
179
  const record = {
116
180
  ...note,
@@ -127,11 +191,11 @@ export class FileBackedWalletState {
127
191
  if (record.leafIndex >= 0) {
128
192
  this.state.commitments[record.leafIndex] = record.commitment;
129
193
  }
194
+ this.rebuildCommitmentIndexes();
130
195
  await this.persist();
131
196
  }
132
197
  async markNoteSpent(commitment) {
133
- const normalized = normalizeHex(commitment);
134
- const existingIndex = this.state.notes.findIndex((candidate) => normalizeHex(candidate.commitment) === normalized);
198
+ const existingIndex = this.findNoteIndexByCommitment(commitment);
135
199
  if (existingIndex < 0) {
136
200
  return false;
137
201
  }
@@ -151,6 +215,7 @@ export class FileBackedWalletState {
151
215
  this.state.commitments[params.merchantLeafIndex] = params.merchantCommitment;
152
216
  this.state.commitments[params.changeLeafIndex] = params.changeCommitment;
153
217
  }
218
+ this.rebuildCommitmentIndexes();
154
219
  await this.persist();
155
220
  }
156
221
  async applyRelayerSettlement(params) {
@@ -177,6 +242,131 @@ export class FileBackedWalletState {
177
242
  }, params.changeNullifierSecret, this.state.lastSyncedBlock >= 0n ? this.state.lastSyncedBlock : undefined);
178
243
  }
179
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;
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 };
369
+ }
180
370
  async sync() {
181
371
  if (this.indexerGraphqlUrl) {
182
372
  return this.syncFromIndexer();
@@ -218,68 +408,13 @@ export class FileBackedWalletState {
218
408
  toBlock
219
409
  })
220
410
  ]);
221
- const writes = [];
222
- for (const log of depositLogs) {
223
- const args = log.args;
224
- if (args.commitment === undefined || args.leafIndex === undefined)
225
- continue;
226
- writes.push({
227
- blockNumber: log.blockNumber ?? 0n,
228
- logIndex: log.logIndex ?? 0,
229
- order: 0,
230
- leafIndex: Number(args.leafIndex),
231
- commitment: args.commitment
232
- });
233
- depositsApplied += 1;
234
- }
235
- for (const log of spendLogs) {
236
- const args = log.args;
237
- if (args.merchantCommitment === undefined ||
238
- args.changeCommitment === undefined ||
239
- args.merchantLeafIndex === undefined ||
240
- args.changeLeafIndex === undefined) {
241
- continue;
242
- }
243
- writes.push({
244
- blockNumber: log.blockNumber ?? 0n,
245
- logIndex: log.logIndex ?? 0,
246
- order: 0,
247
- leafIndex: Number(args.merchantLeafIndex),
248
- commitment: args.merchantCommitment
249
- });
250
- writes.push({
251
- blockNumber: log.blockNumber ?? 0n,
252
- logIndex: log.logIndex ?? 0,
253
- order: 1,
254
- leafIndex: Number(args.changeLeafIndex),
255
- commitment: args.changeCommitment
256
- });
257
- spendsApplied += 1;
258
- }
259
- writes.sort((a, b) => {
260
- if (a.blockNumber !== b.blockNumber)
261
- return a.blockNumber < b.blockNumber ? -1 : 1;
262
- if (a.logIndex !== b.logIndex)
263
- return a.logIndex < b.logIndex ? -1 : 1;
264
- return a.order - b.order;
411
+ const mapped = this.mapRpcEventsToWrites({
412
+ depositLogs: depositLogs,
413
+ spendLogs: spendLogs
265
414
  });
266
- for (const write of writes) {
267
- if (write.leafIndex < 0)
268
- continue;
269
- this.state.commitments[write.leafIndex] = write.commitment;
270
- const normalized = normalizeHex(write.commitment);
271
- for (let i = 0; i < this.state.notes.length; i += 1) {
272
- const note = this.state.notes[i];
273
- if (!note)
274
- continue;
275
- if (normalizeHex(note.commitment) !== normalized)
276
- continue;
277
- note.leafIndex = write.leafIndex;
278
- if (note.depositBlock === undefined) {
279
- note.depositBlock = write.blockNumber;
280
- }
281
- }
282
- }
415
+ this.applyCommitmentWrites(mapped.writes);
416
+ depositsApplied += mapped.depositsApplied;
417
+ spendsApplied += mapped.spendsApplied;
283
418
  this.state.lastSyncedBlock = toBlock;
284
419
  await this.persist();
285
420
  cursor = toBlock + 1n;
@@ -294,117 +429,62 @@ export class FileBackedWalletState {
294
429
  async syncFromIndexer() {
295
430
  const fromBlock = this.state.lastSyncedBlock + 1n;
296
431
  const { deposits, spends, maxBlockNumber } = await this.fetchIndexerEvents();
297
- const writes = [];
298
- let depositsApplied = 0;
299
- let spendsApplied = 0;
300
- for (const deposit of deposits) {
301
- const { blockNumber, logIndex } = parseEventPositionFromId(deposit.id);
302
- writes.push({
303
- blockNumber,
304
- logIndex,
305
- order: 0,
306
- leafIndex: Number(deposit.leafIndex),
307
- commitment: deposit.commitment
308
- });
309
- depositsApplied += 1;
310
- }
311
- for (const spend of spends) {
312
- const { blockNumber, logIndex } = parseEventPositionFromId(spend.id);
313
- writes.push({
314
- blockNumber,
315
- logIndex,
316
- order: 0,
317
- leafIndex: Number(spend.merchantLeafIndex),
318
- commitment: spend.merchantCommitment
319
- });
320
- writes.push({
321
- blockNumber,
322
- logIndex,
323
- order: 1,
324
- leafIndex: Number(spend.changeLeafIndex),
325
- commitment: spend.changeCommitment
326
- });
327
- spendsApplied += 1;
328
- }
329
- writes.sort((a, b) => {
330
- if (a.blockNumber !== b.blockNumber)
331
- return a.blockNumber < b.blockNumber ? -1 : 1;
332
- if (a.logIndex !== b.logIndex)
333
- return a.logIndex < b.logIndex ? -1 : 1;
334
- return a.order - b.order;
335
- });
336
- for (const write of writes) {
337
- if (write.leafIndex < 0)
338
- continue;
339
- this.state.commitments[write.leafIndex] = write.commitment;
340
- const normalized = normalizeHex(write.commitment);
341
- for (let i = 0; i < this.state.notes.length; i += 1) {
342
- const note = this.state.notes[i];
343
- if (!note)
344
- continue;
345
- if (normalizeHex(note.commitment) !== normalized)
346
- continue;
347
- note.leafIndex = write.leafIndex;
348
- if (note.depositBlock === undefined) {
349
- note.depositBlock = write.blockNumber;
350
- }
351
- }
352
- }
432
+ const mapped = this.mapIndexerEventsToWrites(deposits, spends);
433
+ this.applyCommitmentWrites(mapped.writes);
353
434
  const targetBlock = maxBlockNumber >= this.state.lastSyncedBlock ? maxBlockNumber : this.state.lastSyncedBlock;
354
435
  this.state.lastSyncedBlock = targetBlock;
355
436
  await this.persist();
356
437
  return {
357
438
  fromBlock,
358
439
  toBlock: targetBlock,
359
- depositsApplied,
360
- spendsApplied
440
+ depositsApplied: mapped.depositsApplied,
441
+ spendsApplied: mapped.spendsApplied
361
442
  };
362
443
  }
363
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) {
364
453
  if (!this.indexerGraphqlUrl) {
365
454
  throw new Error('indexerGraphqlUrl is not configured');
366
455
  }
367
- const endpoint = this.indexerGraphqlUrl;
368
- const post = async (query, variables) => {
369
- const response = await fetch(endpoint, {
370
- method: 'POST',
371
- headers: {
372
- 'content-type': 'application/json'
373
- },
374
- body: JSON.stringify({
375
- query,
376
- ...(variables ? { variables } : {})
377
- })
378
- });
379
- if (!response.ok) {
380
- const text = await response.text();
381
- throw new Error(`indexer graphql request failed: ${response.status} ${text}`);
382
- }
383
- const payload = (await response.json());
384
- if (payload.errors && payload.errors.length > 0) {
385
- const message = payload.errors.map((error) => error.message ?? 'unknown error').join('; ');
386
- throw new Error(`indexer graphql error: ${message}`);
387
- }
388
- if (!payload.data) {
389
- throw new Error('indexer graphql response missing data');
390
- }
391
- return payload.data;
392
- };
393
- if (!this.indexerFieldNames) {
394
- const introspection = await post(`query WalletIndexerFields { __schema { queryType { fields { name } } } }`);
395
- const fieldNames = introspection.__schema.queryType.fields.map((field) => field.name);
396
- const pickField = (needle) => fieldNames.find((name) => name.toLowerCase().includes(needle) &&
397
- !name.toLowerCase().includes('aggregate') &&
398
- !name.toLowerCase().includes('by_pk'));
399
- const deposits = pickField('shieldedpool_deposited');
400
- const spends = pickField('shieldedpool_spent');
401
- if (!deposits || !spends) {
402
- throw new Error(`unable to detect Envio fields for deposits/spends at ${endpoint}; available fields: ${fieldNames.join(', ')}`);
403
- }
404
- 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');
405
466
  }
406
- const depositsField = this.indexerFieldNames.deposits;
407
- 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();
408
488
  const withPaginationQuery = `
409
489
  query WalletIndexerData($limit: Int!, $offset: Int!) {
410
490
  deposits: ${depositsField}(limit: $limit, offset: $offset) {
@@ -440,23 +520,28 @@ export class FileBackedWalletState {
440
520
  let deposits = [];
441
521
  let spends = [];
442
522
  try {
443
- const pageSize = 500;
444
523
  let offset = 0;
445
524
  while (true) {
446
- const page = await post(withPaginationQuery, { limit: pageSize, offset });
525
+ const page = await this.indexerPost(withPaginationQuery, {
526
+ limit: INDEXER_PAGE_SIZE,
527
+ offset
528
+ });
447
529
  deposits = deposits.concat(page.deposits);
448
530
  spends = spends.concat(page.spends);
449
- if (page.deposits.length < pageSize && page.spends.length < pageSize) {
531
+ if (page.deposits.length < INDEXER_PAGE_SIZE && page.spends.length < INDEXER_PAGE_SIZE) {
450
532
  break;
451
533
  }
452
- offset += pageSize;
534
+ offset += INDEXER_PAGE_SIZE;
453
535
  }
454
536
  }
455
537
  catch {
456
- const data = await post(fullQuery);
538
+ const data = await this.indexerPost(fullQuery);
457
539
  deposits = data.deposits;
458
540
  spends = data.spends;
459
541
  }
542
+ return { deposits, spends };
543
+ }
544
+ computeIndexerMaxBlock(deposits, spends) {
460
545
  let maxBlockNumber = this.state.lastSyncedBlock;
461
546
  for (const deposit of deposits) {
462
547
  const position = parseEventPositionFromId(deposit.id);
@@ -470,11 +555,11 @@ export class FileBackedWalletState {
470
555
  maxBlockNumber = position.blockNumber;
471
556
  }
472
557
  }
473
- return { deposits, spends, maxBlockNumber };
558
+ return maxBlockNumber;
474
559
  }
475
560
  getSpendContextByCommitment(commitment) {
476
- const normalized = normalizeHex(commitment);
477
- const note = this.state.notes.find((candidate) => normalizeHex(candidate.commitment) === normalized);
561
+ const noteIndex = this.findNoteIndexByCommitment(commitment);
562
+ const note = noteIndex >= 0 ? this.state.notes[noteIndex] : undefined;
478
563
  if (!note) {
479
564
  throw new Error(`note not found in wallet state for commitment ${commitment}; add note secrets first with addOrUpdateNote()`);
480
565
  }
@@ -497,16 +582,16 @@ export class FileBackedWalletState {
497
582
  nullifierSecret: note.nullifierSecret
498
583
  };
499
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
+ }
500
592
  findLeafIndex(commitment) {
501
593
  const normalized = normalizeHex(commitment);
502
- for (let i = this.state.commitments.length - 1; i >= 0; i -= 1) {
503
- const candidate = this.state.commitments[i];
504
- if (!candidate)
505
- continue;
506
- if (normalizeHex(candidate) === normalized)
507
- return i;
508
- }
509
- return -1;
594
+ return this.leafIndexByCommitment.get(normalized) ?? -1;
510
595
  }
511
596
  }
512
597
  function parseEventPositionFromId(id) {