@zill-protocol/client 4.1.2

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 (108) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +18 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/package.json +58 -0
  7. package/dist/src/NocturneClient.d.ts +68 -0
  8. package/dist/src/NocturneClient.d.ts.map +1 -0
  9. package/dist/src/NocturneClient.js +264 -0
  10. package/dist/src/NocturneClient.js.map +1 -0
  11. package/dist/src/NocturneDB.d.ts +100 -0
  12. package/dist/src/NocturneDB.d.ts.map +1 -0
  13. package/dist/src/NocturneDB.js +525 -0
  14. package/dist/src/NocturneDB.js.map +1 -0
  15. package/dist/src/OpTracker.d.ts +13 -0
  16. package/dist/src/OpTracker.d.ts.map +1 -0
  17. package/dist/src/OpTracker.js +34 -0
  18. package/dist/src/OpTracker.js.map +1 -0
  19. package/dist/src/conversion/converter.d.ts +5 -0
  20. package/dist/src/conversion/converter.d.ts.map +1 -0
  21. package/dist/src/conversion/converter.js +15 -0
  22. package/dist/src/conversion/converter.js.map +1 -0
  23. package/dist/src/conversion/index.d.ts +3 -0
  24. package/dist/src/conversion/index.d.ts.map +1 -0
  25. package/dist/src/conversion/index.js +21 -0
  26. package/dist/src/conversion/index.js.map +1 -0
  27. package/dist/src/conversion/mock.d.ts +6 -0
  28. package/dist/src/conversion/mock.d.ts.map +1 -0
  29. package/dist/src/conversion/mock.js +14 -0
  30. package/dist/src/conversion/mock.js.map +1 -0
  31. package/dist/src/index.d.ts +14 -0
  32. package/dist/src/index.d.ts.map +1 -0
  33. package/dist/src/index.js +39 -0
  34. package/dist/src/index.js.map +1 -0
  35. package/dist/src/opRequestGas.d.ts +20 -0
  36. package/dist/src/opRequestGas.d.ts.map +1 -0
  37. package/dist/src/opRequestGas.js +321 -0
  38. package/dist/src/opRequestGas.js.map +1 -0
  39. package/dist/src/operationRequest/builder.d.ts +40 -0
  40. package/dist/src/operationRequest/builder.d.ts.map +1 -0
  41. package/dist/src/operationRequest/builder.js +192 -0
  42. package/dist/src/operationRequest/builder.js.map +1 -0
  43. package/dist/src/operationRequest/index.d.ts +3 -0
  44. package/dist/src/operationRequest/index.d.ts.map +1 -0
  45. package/dist/src/operationRequest/index.js +6 -0
  46. package/dist/src/operationRequest/index.js.map +1 -0
  47. package/dist/src/operationRequest/operationRequest.d.ts +50 -0
  48. package/dist/src/operationRequest/operationRequest.d.ts.map +1 -0
  49. package/dist/src/operationRequest/operationRequest.js +16 -0
  50. package/dist/src/operationRequest/operationRequest.js.map +1 -0
  51. package/dist/src/prepareOperation.d.ts +21 -0
  52. package/dist/src/prepareOperation.d.ts.map +1 -0
  53. package/dist/src/prepareOperation.js +256 -0
  54. package/dist/src/prepareOperation.js.map +1 -0
  55. package/dist/src/proveOperation.d.ts +7 -0
  56. package/dist/src/proveOperation.d.ts.map +1 -0
  57. package/dist/src/proveOperation.js +79 -0
  58. package/dist/src/proveOperation.js.map +1 -0
  59. package/dist/src/signOperation.d.ts +3 -0
  60. package/dist/src/signOperation.d.ts.map +1 -0
  61. package/dist/src/signOperation.js +61 -0
  62. package/dist/src/signOperation.js.map +1 -0
  63. package/dist/src/snapJsonRpc.d.ts +55 -0
  64. package/dist/src/snapJsonRpc.d.ts.map +1 -0
  65. package/dist/src/snapJsonRpc.js +63 -0
  66. package/dist/src/snapJsonRpc.js.map +1 -0
  67. package/dist/src/syncSDK.d.ts +17 -0
  68. package/dist/src/syncSDK.d.ts.map +1 -0
  69. package/dist/src/syncSDK.js +188 -0
  70. package/dist/src/syncSDK.js.map +1 -0
  71. package/dist/src/types.d.ts +60 -0
  72. package/dist/src/types.d.ts.map +1 -0
  73. package/dist/src/types.js +3 -0
  74. package/dist/src/types.js.map +1 -0
  75. package/dist/src/utils/constants.d.ts +3 -0
  76. package/dist/src/utils/constants.d.ts.map +1 -0
  77. package/dist/src/utils/constants.js +20 -0
  78. package/dist/src/utils/constants.js.map +1 -0
  79. package/dist/src/utils/index.d.ts +3 -0
  80. package/dist/src/utils/index.d.ts.map +1 -0
  81. package/dist/src/utils/index.js +19 -0
  82. package/dist/src/utils/index.js.map +1 -0
  83. package/dist/src/utils/misc.d.ts +13 -0
  84. package/dist/src/utils/misc.d.ts.map +1 -0
  85. package/dist/src/utils/misc.js +77 -0
  86. package/dist/src/utils/misc.js.map +1 -0
  87. package/dist/tsconfig.tsbuildinfo +1 -0
  88. package/package.json +58 -0
  89. package/src/NocturneClient.ts +415 -0
  90. package/src/NocturneDB.ts +761 -0
  91. package/src/OpTracker.ts +44 -0
  92. package/src/conversion/converter.ts +22 -0
  93. package/src/conversion/index.ts +2 -0
  94. package/src/conversion/mock.ts +11 -0
  95. package/src/index.ts +14 -0
  96. package/src/opRequestGas.ts +487 -0
  97. package/src/operationRequest/builder.ts +359 -0
  98. package/src/operationRequest/index.ts +16 -0
  99. package/src/operationRequest/operationRequest.ts +87 -0
  100. package/src/prepareOperation.ts +420 -0
  101. package/src/proveOperation.ts +124 -0
  102. package/src/signOperation.ts +116 -0
  103. package/src/snapJsonRpc.ts +109 -0
  104. package/src/syncSDK.ts +285 -0
  105. package/src/types.ts +83 -0
  106. package/src/utils/constants.ts +16 -0
  107. package/src/utils/index.ts +2 -0
  108. package/src/utils/misc.ts +107 -0
@@ -0,0 +1,761 @@
1
+ import { ethers } from "ethers";
2
+ import {
3
+ OptimisticNFRecord,
4
+ OpHistoryRecord,
5
+ OperationMetadata,
6
+ PendingOutputRecord,
7
+ } from "./types";
8
+ import * as JSON from "bigint-json-serialization";
9
+ import {
10
+ Asset,
11
+ AssetTrait,
12
+ IncludedNote,
13
+ IncludedNoteCommitment,
14
+ NoteTrait,
15
+ IncludedNoteWithNullifier,
16
+ numberToStringPadded,
17
+ KV,
18
+ KVStore,
19
+ StateDiff,
20
+ TotalEntityIndex,
21
+ WithTotalEntityIndex,
22
+ OperationStatus,
23
+ unzip,
24
+ OperationTrait,
25
+ PreSignOperation,
26
+ SignedOperation,
27
+ } from "@zill-protocol/core";
28
+ import { Mutex } from "async-mutex";
29
+ import {
30
+ OPTIMISTIC_RECORD_TTL,
31
+ getMerkleIndicesAndNfsFromOp,
32
+ isFailedOpStatus,
33
+ isTerminalOpStatus,
34
+ } from "./utils";
35
+
36
+ const NOTES_BY_INDEX_PREFIX = "NOTES_BY_INDEX";
37
+ const NOTES_BY_ASSET_PREFIX = "NOTES_BY_ASSET";
38
+ const NOTES_BY_NULLIFIER_PREFIX = "NOTES_BY_NULLIFIER";
39
+ const MERKLE_INDEX_TIMESTAMP_PREFIX = "MERKLE_INDEX_TIMESTAMP";
40
+ const OPTIMISTIC_NF_RECORD_PREFIX = "OPTIMISTIC_NF_RECORD";
41
+ const CURRENT_TOTAL_ENTITY_INDEX_KEY = "CURRENT_TOTAL_ENTITY_INDEX";
42
+ const LAST_COMMITTED_MERKLE_INDEX_KEY = "LAST_COMMITTED_MERKLE_INDEX";
43
+ const LAST_SYNCED_MERKLE_INDEX_KEY = "LAST_SYNCED_MERKLE_INDEX";
44
+ const LAST_COMMIT_TEI_KEY = "LAST_COMMIT_TEI";
45
+ const OP_HISTORY_KEY = "OP_HISTORY";
46
+ const OP_DIGEST_PREFIX = "OP_DIGEST_";
47
+ const PENDING_OUTPUT_PREFIX = "PENDING_OUTPUT";
48
+
49
+ // ceil(log10(2^32))
50
+ const MAX_MERKLE_INDEX_DIGITS = 10;
51
+
52
+ export type AssetKey = string;
53
+ type AllNotes = Map<AssetKey, IncludedNote[]>;
54
+
55
+ // options for methods that get notes from the DB
56
+ // if includeUncommitted is defined and true, then the method include notes that are not yet committed to the commitment tree
57
+ // if ignoreOptimisticNFs is defined and true, then the method will include notes that have been used by the SDK, but may not have been nullified on-chain yet
58
+ // if both are undefined, then the method will only return notes that have been committed to the commitment tree and have not been used by the SDK yet
59
+ export interface GetNotesOpts {
60
+ includeUncommitted?: boolean;
61
+ ignoreOptimisticNFs?: boolean;
62
+ }
63
+
64
+ export class NocturneDB {
65
+ // store the following mappings:
66
+ // merkleIndex => Note
67
+ // merkleIndex => totalEntityIndex
68
+ // asset => merkleIndex[]
69
+ // nullifier => merkleIndex
70
+ public kv: KVStore;
71
+ protected mutex: Mutex;
72
+
73
+ constructor(kv: KVStore) {
74
+ this.kv = kv;
75
+ this.mutex = new Mutex();
76
+ }
77
+
78
+ static formatIndexKey(merkleIndex: number): string {
79
+ return `${NOTES_BY_INDEX_PREFIX}-${numberToStringPadded(
80
+ merkleIndex,
81
+ MAX_MERKLE_INDEX_DIGITS
82
+ )}`;
83
+ }
84
+
85
+ static formatAssetKey(asset: Asset): string {
86
+ return `${NOTES_BY_ASSET_PREFIX}-${
87
+ asset.assetType
88
+ }-${ethers.utils.getAddress(asset.assetAddr)}-${asset.id.toString()}`;
89
+ }
90
+
91
+ static formatNullifierKey(nullifier: bigint): string {
92
+ return `${NOTES_BY_NULLIFIER_PREFIX}-${nullifier.toString()}`;
93
+ }
94
+
95
+ static formatMerkleIndexToTotalEntityIndexKey(merkleIndex: number): string {
96
+ return `${MERKLE_INDEX_TIMESTAMP_PREFIX}-${numberToStringPadded(
97
+ merkleIndex,
98
+ MAX_MERKLE_INDEX_DIGITS
99
+ )}`;
100
+ }
101
+
102
+ static formatOptimisticNFRecordKey(merkleIndex: number): string {
103
+ return `${OPTIMISTIC_NF_RECORD_PREFIX}-${numberToStringPadded(
104
+ merkleIndex,
105
+ MAX_MERKLE_INDEX_DIGITS
106
+ )}`;
107
+ }
108
+
109
+ static formatOpHistoryRecordKey(digest: bigint): string {
110
+ return `${OP_DIGEST_PREFIX}${digest.toString()}`;
111
+ }
112
+
113
+ static formatPendingOutputKey(commitment: bigint): string {
114
+ return `${PENDING_OUTPUT_PREFIX}-${commitment.toString()}`;
115
+ }
116
+
117
+ static parseMerkleIndexFromOptimisticNFRecordKey(key: string): number {
118
+ return parseInt(key.split("-")[1]);
119
+ }
120
+
121
+ static parseIndexKey(key: string): number {
122
+ return parseInt(key.split("-")[1]);
123
+ }
124
+
125
+ static parseAssetKey(key: string): Asset {
126
+ const [_, assetType, assetAddr, id] = key.split("-");
127
+ return {
128
+ assetType: AssetTrait.parseAssetType(assetType),
129
+ assetAddr,
130
+ id: BigInt(id),
131
+ };
132
+ }
133
+
134
+ protected async _getHistoryRecord(
135
+ digest: bigint
136
+ ): Promise<OpHistoryRecord | undefined> {
137
+ const key = NocturneDB.formatOpHistoryRecordKey(digest);
138
+ const value = await this.kv.getString(key);
139
+ if (value === undefined) {
140
+ return undefined;
141
+ }
142
+
143
+ return JSON.parse(value);
144
+ }
145
+
146
+ protected async _setHistoryRecord(
147
+ digest: bigint,
148
+ record: OpHistoryRecord
149
+ ): Promise<void> {
150
+ const key = NocturneDB.formatOpHistoryRecordKey(digest);
151
+ const value = JSON.stringify(record);
152
+ await this.kv.putString(key, value);
153
+ }
154
+
155
+ protected async getHistoryArray(): Promise<bigint[]> {
156
+ const value = await this.kv.getString(OP_HISTORY_KEY);
157
+ if (value === undefined) {
158
+ return [];
159
+ }
160
+
161
+ return JSON.parse(value);
162
+ }
163
+
164
+ protected async setHistoryArray(history: bigint[]): Promise<void> {
165
+ const value = JSON.stringify(history);
166
+ await this.kv.putString(OP_HISTORY_KEY, value);
167
+ }
168
+
169
+ async getHistory(includePending?: boolean): Promise<OpHistoryRecord[]> {
170
+ return await this.mutex.runExclusive(async () => {
171
+ const history = await this.getHistoryArray();
172
+ const records = await Promise.all(
173
+ history.map((digest) => this._getHistoryRecord(digest))
174
+ );
175
+
176
+ // if any record is missing, something bad happened
177
+ if (records.some((r) => r === undefined)) {
178
+ console.error("missing record in history");
179
+ }
180
+
181
+ if (!includePending) {
182
+ return records.filter(
183
+ (r) => r?.status && isTerminalOpStatus(r.status)
184
+ ) as OpHistoryRecord[];
185
+ }
186
+
187
+ return records as OpHistoryRecord[];
188
+ });
189
+ }
190
+
191
+ async getHistoryRecord(digest: bigint): Promise<OpHistoryRecord | undefined> {
192
+ return await this.mutex.runExclusive(
193
+ async () => await this._getHistoryRecord(digest)
194
+ );
195
+ }
196
+
197
+ async setStatusForOp(
198
+ opDigest: bigint,
199
+ status: OperationStatus
200
+ ): Promise<void> {
201
+ await this.mutex.runExclusive(async () => {
202
+ const record = await this._getHistoryRecord(opDigest);
203
+ if (record === undefined) {
204
+ console.warn(
205
+ "attempting to set status of op whose record has been pruned"
206
+ );
207
+ return;
208
+ }
209
+
210
+ record.status = status;
211
+ record.lastModified = Date.now();
212
+
213
+ await this._setHistoryRecord(opDigest, record);
214
+
215
+ // remove the corresponding optimistic nf records if op failed
216
+ if (isFailedOpStatus(status)) {
217
+ await this.removeOptimisticNFRecords(record.spentNoteMerkleIndices);
218
+ }
219
+ });
220
+ }
221
+
222
+ async addOpToHistory(
223
+ op: PreSignOperation | SignedOperation,
224
+ metadata: OperationMetadata,
225
+ status?: OperationStatus
226
+ ): Promise<void> {
227
+ const digest = OperationTrait.computeDigest(op);
228
+ const pairs: [number, bigint][] = getMerkleIndicesAndNfsFromOp(op).map(
229
+ ({ merkleIndex, nullifier }) => [Number(merkleIndex), nullifier]
230
+ );
231
+ const [spentNoteMerkleIndices] = unzip(pairs);
232
+
233
+ const expirationDate = Date.now() + OPTIMISTIC_RECORD_TTL;
234
+ const optimisticNfKvs = pairs.map(([merkleIndex, nullifier]) =>
235
+ NocturneDB.makeOptimisticNFRecordKV(merkleIndex, {
236
+ nullifier,
237
+ expirationDate,
238
+ })
239
+ );
240
+
241
+ await this.mutex.runExclusive(async () => {
242
+ const now = Date.now();
243
+ const record = {
244
+ digest,
245
+ metadata,
246
+ status,
247
+ spentNoteMerkleIndices,
248
+ createdAt: now,
249
+ lastModified: now,
250
+ };
251
+
252
+ await this._setHistoryRecord(record.digest, record);
253
+
254
+ const history = await this.getHistoryArray();
255
+ history.push(record.digest);
256
+ await this.setHistoryArray(history);
257
+
258
+ await this.kv.putMany(optimisticNfKvs);
259
+ });
260
+ }
261
+
262
+ async removeOpFromHistory(
263
+ digest: bigint,
264
+ removeOptimisticNFs?: boolean
265
+ ): Promise<void> {
266
+ await this.mutex.runExclusive(async () => {
267
+ const history = await this.getHistoryArray();
268
+
269
+ const index = history.indexOf(digest);
270
+ if (index !== -1) {
271
+ history.splice(index, 1);
272
+ await this.setHistoryArray(history);
273
+ } else {
274
+ console.warn("tried to remove op from history that was not in history");
275
+ }
276
+
277
+ if (removeOptimisticNFs) {
278
+ const spentIndices =
279
+ (await this._getHistoryRecord(digest))?.spentNoteMerkleIndices ?? [];
280
+ await this.removeOptimisticNFRecords(spentIndices);
281
+ }
282
+
283
+ await this.kv.remove(NocturneDB.formatOpHistoryRecordKey(digest));
284
+ });
285
+ }
286
+
287
+ async pruneOptimisticNFs(): Promise<void> {
288
+ const optimsiticNfRecords = await this.getAllOptimisticNFRecords();
289
+ const now = Date.now();
290
+ const keysToRemove = [...optimsiticNfRecords.entries()].flatMap(
291
+ ([merkleIndex, record]) => {
292
+ if (now > record.expirationDate) {
293
+ return [NocturneDB.formatOptimisticNFRecordKey(merkleIndex)];
294
+ }
295
+
296
+ return [];
297
+ }
298
+ );
299
+
300
+ await this.kv.removeMany(keysToRemove);
301
+ }
302
+
303
+ async getOptimisticNFRecord(
304
+ merkleIndex: number
305
+ ): Promise<OptimisticNFRecord | undefined> {
306
+ const key = NocturneDB.formatOptimisticNFRecordKey(merkleIndex);
307
+ const value = await this.kv.getString(key);
308
+
309
+ if (value === undefined) {
310
+ return undefined;
311
+ }
312
+
313
+ return JSON.parse(value);
314
+ }
315
+
316
+ async getAllOptimisticNFRecords(): Promise<Map<number, OptimisticNFRecord>> {
317
+ const map = new Map<number, OptimisticNFRecord>();
318
+ const kvs = await this.kv.iterPrefix(OPTIMISTIC_NF_RECORD_PREFIX);
319
+ for await (const [key, value] of kvs) {
320
+ const merkleIndex =
321
+ NocturneDB.parseMerkleIndexFromOptimisticNFRecordKey(key);
322
+ const record = JSON.parse(value);
323
+ map.set(merkleIndex, record);
324
+ }
325
+
326
+ return map;
327
+ }
328
+
329
+ async removeOptimisticNFRecords(merkleIndices: number[]): Promise<void> {
330
+ const keys = merkleIndices.map(NocturneDB.formatOptimisticNFRecordKey);
331
+ await this.kv.removeMany(keys);
332
+ }
333
+
334
+ async storeNotes(
335
+ notesWithTotalEntityIndices: WithTotalEntityIndex<IncludedNoteWithNullifier>[]
336
+ ): Promise<void> {
337
+ const notes = notesWithTotalEntityIndices.map(({ inner }) => inner);
338
+ // make note KVs
339
+ const noteKVs: KV[] = notes.map(({ nullifier, ...note }) =>
340
+ NocturneDB.makeNoteKV(note.merkleIndex, note)
341
+ );
342
+
343
+ // make the nullifier => merkleIndex KV pairs
344
+ const nullifierKVs: KV[] = notes.map(({ merkleIndex, nullifier }) =>
345
+ NocturneDB.makeNullifierKV(merkleIndex, nullifier)
346
+ );
347
+
348
+ // get the updated asset => merkleIndex[] KV pairs
349
+ const assetKVs = await this.getUpdatedAssetKVsWithNotesAdded(notes);
350
+
351
+ const merkleIndexToTotalEntityIndexKVs = notesWithTotalEntityIndices.map(
352
+ ({ inner, totalEntityIndex }) =>
353
+ NocturneDB.makeMerkleIndexToTotalEntityIndexKV(
354
+ inner.merkleIndex,
355
+ totalEntityIndex
356
+ )
357
+ );
358
+
359
+ // write them all into the KV store
360
+ await this.kv.putMany([
361
+ ...noteKVs,
362
+ ...nullifierKVs,
363
+ ...assetKVs,
364
+ ...merkleIndexToTotalEntityIndexKVs,
365
+ ]);
366
+ }
367
+
368
+ // returns the merkle indices of the notes that were nullified
369
+ async nullifyNotes(nullifiers: bigint[]): Promise<number[]> {
370
+ // delete nullifier => merkleIndex KV pairs
371
+ const nfKeys = nullifiers.map((nullifier) =>
372
+ NocturneDB.formatNullifierKey(nullifier)
373
+ );
374
+ const kvs = await this.kv.getMany(nfKeys);
375
+ await this.kv.removeMany([...nfKeys]);
376
+
377
+ // get the notes we're nullifying
378
+ const indices = kvs.map(([_nfKey, stringifiedIdx]) =>
379
+ parseInt(stringifiedIdx)
380
+ );
381
+ const notes = await this.getNotesByMerkleIndices(indices);
382
+
383
+ // get the updated asset => merkleIndex[] KV pairs
384
+ // for each note, remove the note's merkleIndex from its asset's index keys
385
+ const assetKVs = await this.getUpdatedAssetKVsWithNotesRemoved(notes);
386
+
387
+ // write the new commitment KV pairs and the new asset => merkleIndex[] KV pairs to the KV store
388
+ await this.kv.putMany(assetKVs);
389
+
390
+ // remove any optimistic nf records for the nullified notes
391
+ await this.removeOptimisticNFRecords(indices);
392
+
393
+ return indices;
394
+ }
395
+
396
+ /**
397
+ * Get all *committed* notes for an asset
398
+ *
399
+ * @param asset the asset to get notes for
400
+ * @param opts optional options. See `GetNotesOpts` for more details.
401
+ * @returns notes an array of notes for the asset. The array has no guaranteed order.
402
+ */
403
+ async getNotesForAsset(
404
+ asset: Asset,
405
+ opts?: GetNotesOpts
406
+ ): Promise<IncludedNote[]> {
407
+ const indices = await this.getMerkleIndicesForAsset(asset);
408
+ const notes = await this.getNotesByMerkleIndices(indices);
409
+ return this.filterNotesByOpts(notes, opts);
410
+ }
411
+
412
+ private async filterNotesByOpts(
413
+ notes: IncludedNote[],
414
+ opts?: GetNotesOpts
415
+ ): Promise<IncludedNote[]> {
416
+ if (!opts?.includeUncommitted) {
417
+ const latestCommittedMerkleIndex =
418
+ await this.latestCommittedMerkleIndex();
419
+ if (latestCommittedMerkleIndex === undefined) {
420
+ return [];
421
+ }
422
+
423
+ notes = notes.filter(
424
+ (note) => note.merkleIndex <= latestCommittedMerkleIndex
425
+ );
426
+ }
427
+
428
+ if (!opts?.ignoreOptimisticNFs) {
429
+ const hasOptimisticNF = await Promise.all(
430
+ notes.map(
431
+ async (note) => !(await this.getOptimisticNFRecord(note.merkleIndex))
432
+ )
433
+ );
434
+ notes = notes.filter((_, i) => hasOptimisticNF[i]);
435
+ }
436
+
437
+ return notes;
438
+ }
439
+
440
+ /**
441
+ * Get TotalEntityIndex at which an owned note with merkleIndex `merkleIndex` was inserted into the tree (not necessarily committed)
442
+ *
443
+ * @param merkleIndex the merkleIndex to get the TotalEntityIndex for
444
+ * @returns the totalEntityIndex in unix millis at which the merkleIndex was inserted into the tree,
445
+ * or undefined if the corresponding note is nullified or not owned
446
+ */
447
+ async getTotalEntityIndexForMerkleIndex(
448
+ merkleIndex: number
449
+ ): Promise<bigint | undefined> {
450
+ const totalEntityIndexKey =
451
+ NocturneDB.formatMerkleIndexToTotalEntityIndexKey(merkleIndex);
452
+ return await this.kv.getBigInt(totalEntityIndexKey);
453
+ }
454
+
455
+ // return the totalEntityndex that the DB has been synced to
456
+ // this is more/less a "version" number
457
+ async currentTotalEntityIndex(): Promise<TotalEntityIndex | undefined> {
458
+ return await this.kv.getBigInt(CURRENT_TOTAL_ENTITY_INDEX_KEY);
459
+ }
460
+
461
+ // update `currentTotallEntityIndex()`.
462
+ async setCurrentTotalEntityIndex(
463
+ totalEntityIndex: TotalEntityIndex
464
+ ): Promise<void> {
465
+ await this.kv.putBigInt(CURRENT_TOTAL_ENTITY_INDEX_KEY, totalEntityIndex);
466
+ }
467
+
468
+ // index of the last note synced (can be ahead of committed)
469
+ async latestSyncedMerkleIndex(): Promise<number | undefined> {
470
+ return await this.kv.getNumber(LAST_SYNCED_MERKLE_INDEX_KEY);
471
+ }
472
+
473
+ // update `latestSyncedMerkleIndex()`
474
+ async setlatestSyncedMerkleIndex(index: number): Promise<void> {
475
+ await this.kv.putNumber(LAST_SYNCED_MERKLE_INDEX_KEY, index);
476
+ }
477
+
478
+ // index of the last note (dummy or not) to be committed
479
+ async latestCommittedMerkleIndex(): Promise<number | undefined> {
480
+ return await this.kv.getNumber(LAST_COMMITTED_MERKLE_INDEX_KEY);
481
+ }
482
+
483
+ // update `latestCommittedMerkleIndex()`
484
+ async setlatestCommittedMerkleIndex(index: number): Promise<void> {
485
+ await this.kv.putNumber(LAST_COMMITTED_MERKLE_INDEX_KEY, index);
486
+ }
487
+
488
+ // get the total entity index at which the last commit was made
489
+ async lastCommitTotalEntityIndex(): Promise<TotalEntityIndex | undefined> {
490
+ return await this.kv.getBigInt(LAST_COMMIT_TEI_KEY);
491
+ }
492
+
493
+ // update `lastCommitTotalEntityIndex()`
494
+ async setLastCommitTotalEntityIndex(
495
+ totalEntityIndex: TotalEntityIndex
496
+ ): Promise<void> {
497
+ await this.kv.putBigInt(LAST_COMMIT_TEI_KEY, totalEntityIndex);
498
+ }
499
+
500
+ // applies a single state diff to the DB
501
+ // returns the merkle indices of the notes that were nullified
502
+ async applyStateDiff(
503
+ diff: StateDiff,
504
+ opts?: {
505
+ onPendingOutputsConfirmed?: (outputs: PendingOutputRecord[]) => void;
506
+ }
507
+ ): Promise<number[]> {
508
+ const {
509
+ notesAndCommitments,
510
+ nullifiers,
511
+ latestNewlySyncedMerkleIndex,
512
+ latestCommittedMerkleIndex,
513
+ latestCommitTei,
514
+ totalEntityIndex,
515
+ } = diff;
516
+
517
+ // NOTE: order matters here - some `notesAndCommitments` may be nullified in the same state diff
518
+ const notesToStore = notesAndCommitments.filter(
519
+ ({ inner }) =>
520
+ !NoteTrait.isCommitment(inner) &&
521
+ (inner as IncludedNoteWithNullifier).value > 0n
522
+ ) as WithTotalEntityIndex<IncludedNoteWithNullifier>[];
523
+
524
+ // TODO: make this all one write
525
+ await this.storeNotes(notesToStore);
526
+
527
+ const nfIndices = await this.nullifyNotes(nullifiers);
528
+
529
+ const commitmentsToCheck = notesAndCommitments.map(({ inner }) =>
530
+ NoteTrait.isCommitment(inner)
531
+ ? (inner as IncludedNoteCommitment).noteCommitment
532
+ : NoteTrait.toCommitment(inner as IncludedNote)
533
+ );
534
+ const confirmedPending = await this.removePendingOutputsByCommitments(
535
+ commitmentsToCheck
536
+ );
537
+ if (opts?.onPendingOutputsConfirmed && confirmedPending.length > 0) {
538
+ opts.onPendingOutputsConfirmed(confirmedPending);
539
+ }
540
+
541
+ if (latestNewlySyncedMerkleIndex) {
542
+ await this.setlatestSyncedMerkleIndex(latestNewlySyncedMerkleIndex);
543
+ }
544
+ if (latestCommittedMerkleIndex) {
545
+ await this.setlatestCommittedMerkleIndex(latestCommittedMerkleIndex);
546
+ }
547
+ if (latestCommitTei) {
548
+ await this.setLastCommitTotalEntityIndex(latestCommitTei);
549
+ }
550
+
551
+ await this.setCurrentTotalEntityIndex(totalEntityIndex);
552
+
553
+ return nfIndices;
554
+ }
555
+
556
+ async addPendingOutputs(outputs: PendingOutputRecord[]): Promise<void> {
557
+ if (!outputs.length) {
558
+ return;
559
+ }
560
+ const kvs = outputs.map((output) => {
561
+ const key = NocturneDB.formatPendingOutputKey(output.commitment);
562
+ return [key, JSON.stringify(output)] as KV;
563
+ });
564
+ await this.kv.putMany(kvs);
565
+ }
566
+
567
+ async getPendingOutputs(
568
+ opts?: { asset?: Asset }
569
+ ): Promise<PendingOutputRecord[]> {
570
+ const iterPrefix = await this.kv.iterPrefix(PENDING_OUTPUT_PREFIX);
571
+ const outputs: PendingOutputRecord[] = [];
572
+ for await (const [_key, stringified] of iterPrefix) {
573
+ try {
574
+ const parsed = JSON.parse(stringified) as PendingOutputRecord;
575
+ if (opts?.asset) {
576
+ const asset = parsed.asset;
577
+ if (
578
+ asset.assetType !== opts.asset.assetType ||
579
+ asset.assetAddr.toLowerCase() !== opts.asset.assetAddr.toLowerCase() ||
580
+ asset.id !== opts.asset.id
581
+ ) {
582
+ continue;
583
+ }
584
+ }
585
+ outputs.push(parsed);
586
+ } catch {
587
+ // ignore malformed pending outputs
588
+ }
589
+ }
590
+ return outputs;
591
+ }
592
+
593
+ async getPendingBalanceForAsset(asset: Asset): Promise<bigint> {
594
+ const outputs = await this.getPendingOutputs({ asset });
595
+ return outputs.reduce((sum, output) => sum + output.value, 0n);
596
+ }
597
+
598
+ async removePendingOutputsByCommitments(
599
+ commitments: bigint[]
600
+ ): Promise<PendingOutputRecord[]> {
601
+ if (!commitments.length) {
602
+ return [];
603
+ }
604
+ const keys = commitments.map(NocturneDB.formatPendingOutputKey);
605
+ const kvs = await this.kv.getMany(keys);
606
+ const outputs: PendingOutputRecord[] = [];
607
+ for (const [_key, stringified] of kvs) {
608
+ try {
609
+ const parsed = JSON.parse(stringified) as PendingOutputRecord;
610
+ outputs.push(parsed);
611
+ } catch {
612
+ // ignore malformed pending outputs
613
+ }
614
+ }
615
+ await this.kv.removeMany(keys);
616
+ return outputs;
617
+ }
618
+
619
+ async removePendingOutputsByOpDigest(
620
+ opDigest: bigint
621
+ ): Promise<void> {
622
+ const outputs = await this.getPendingOutputs();
623
+ const toRemove = outputs
624
+ .filter((output) => output.opDigest === opDigest)
625
+ .map((output) => output.commitment);
626
+ await this.removePendingOutputsByCommitments(toRemove);
627
+ }
628
+
629
+ /**
630
+ * Get total value of all notes for a given asset
631
+ *
632
+ * @param asset the asset to get balance for
633
+ * @param opts optional options. See `GetNotesOpts` for more details.
634
+ * @returns total value of all notes for the asset summed up
635
+ */
636
+ async getBalanceForAsset(asset: Asset, opts?: GetNotesOpts): Promise<bigint> {
637
+ const notes = await this.getNotesForAsset(asset, opts);
638
+ return notes.reduce((a, b) => a + b.value, 0n);
639
+ }
640
+
641
+ /**
642
+ * Get all notes in the KV store
643
+ *
644
+ * @param opts optional options. See `GetNotesOpts` for more details.
645
+ * @returns allNotes a map of all notes in the KV store. keys are the `NoteAssetKey` for an asset,
646
+ * and values are an array of `IncludedNote`s for that asset. The array has no guaranteed order.
647
+ */
648
+ async getAllNotes(opts?: GetNotesOpts): Promise<AllNotes> {
649
+ const allNotes = new Map<AssetKey, IncludedNote[]>();
650
+
651
+ const iterPrefix = await this.kv.iterPrefix(NOTES_BY_ASSET_PREFIX);
652
+ for await (const [assetKey, stringifiedIndices] of iterPrefix) {
653
+ const indices: number[] = JSON.parse(stringifiedIndices);
654
+ let notes = await this.getNotesByMerkleIndices(indices);
655
+ notes = await this.filterNotesByOpts(notes, opts);
656
+
657
+ const notesForAsset = allNotes.get(assetKey) ?? [];
658
+ notesForAsset.push(...notes);
659
+
660
+ if (notesForAsset.length !== 0) {
661
+ allNotes.set(assetKey, notesForAsset);
662
+ }
663
+ }
664
+
665
+ return allNotes;
666
+ }
667
+
668
+ private static makeNoteKV<N extends IncludedNote>(
669
+ merkleIndex: number,
670
+ note: N
671
+ ): KV {
672
+ return [NocturneDB.formatIndexKey(merkleIndex), JSON.stringify(note)];
673
+ }
674
+
675
+ private static makeNullifierKV(merkleIndex: number, nullifier: bigint): KV {
676
+ return [NocturneDB.formatNullifierKey(nullifier), merkleIndex.toString()];
677
+ }
678
+
679
+ private static makeMerkleIndexToTotalEntityIndexKV(
680
+ merkleIndex: number,
681
+ totalEntityIndex: TotalEntityIndex
682
+ ): KV {
683
+ return [
684
+ NocturneDB.formatMerkleIndexToTotalEntityIndexKey(merkleIndex),
685
+ totalEntityIndex.toString(),
686
+ ];
687
+ }
688
+
689
+ private static makeOptimisticNFRecordKV(
690
+ merkleIndex: number,
691
+ record: OptimisticNFRecord
692
+ ): KV {
693
+ const key = NocturneDB.formatOptimisticNFRecordKey(merkleIndex);
694
+ const value = JSON.stringify(record);
695
+ return [key, value];
696
+ }
697
+
698
+ private async getUpdatedAssetKVsWithNotesAdded<N extends IncludedNote>(
699
+ notes: N[]
700
+ ): Promise<KV[]> {
701
+ const map = new Map<AssetKey, Set<number>>();
702
+ for (const note of notes) {
703
+ const assetKey = NocturneDB.formatAssetKey(note.asset);
704
+ let indices = map.get(assetKey);
705
+ if (!indices) {
706
+ indices = new Set(await this.getMerkleIndicesForAsset(note.asset));
707
+ }
708
+
709
+ indices.add(note.merkleIndex);
710
+
711
+ map.set(assetKey, indices);
712
+ }
713
+
714
+ return Array.from(map.entries()).map(([assetKey, indexKeys]) => [
715
+ assetKey,
716
+ JSON.stringify(Array.from(indexKeys)),
717
+ ]);
718
+ }
719
+
720
+ private async getUpdatedAssetKVsWithNotesRemoved<N extends IncludedNote>(
721
+ notes: N[]
722
+ ): Promise<KV[]> {
723
+ const map = new Map<AssetKey, Set<number>>();
724
+ for (const note of notes) {
725
+ const assetKey = NocturneDB.formatAssetKey(note.asset);
726
+ let indices = map.get(assetKey);
727
+ if (!indices) {
728
+ indices = new Set(await this.getMerkleIndicesForAsset(note.asset));
729
+ }
730
+
731
+ indices.delete(note.merkleIndex);
732
+
733
+ map.set(assetKey, indices);
734
+ }
735
+
736
+ return Array.from(map.entries()).map(([assetKey, indexKeys]) => [
737
+ assetKey,
738
+ JSON.stringify(Array.from(indexKeys)),
739
+ ]);
740
+ }
741
+
742
+ private async getMerkleIndicesForAsset(asset: Asset): Promise<number[]> {
743
+ const assetKey = NocturneDB.formatAssetKey(asset);
744
+ const value = await this.kv.getString(assetKey);
745
+ if (!value) {
746
+ return [];
747
+ }
748
+
749
+ return JSON.parse(value);
750
+ }
751
+
752
+ private async getNotesByMerkleIndices(
753
+ indices: number[]
754
+ ): Promise<IncludedNote[]> {
755
+ const idxKeys = indices.map((index) => NocturneDB.formatIndexKey(index));
756
+ const kvs = await this.kv.getMany(idxKeys);
757
+ return kvs.map(([_, value]) => {
758
+ return JSON.parse(value) as IncludedNote;
759
+ });
760
+ }
761
+ }