@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,420 @@
1
+ import { NocturneDB } from "./NocturneDB";
2
+ import {
3
+ JoinSplitRequest,
4
+ GasAccountedOperationRequest,
5
+ } from "./operationRequest/operationRequest";
6
+ import {
7
+ NocturneViewer,
8
+ CanonAddress,
9
+ StealthAddressTrait,
10
+ randomFr,
11
+ CompressedStealthAddress,
12
+ } from "@zill-protocol/crypto";
13
+ import {
14
+ PreSignJoinSplit,
15
+ Note,
16
+ NoteTrait,
17
+ IncludedNote,
18
+ Asset,
19
+ AssetTrait,
20
+ PreSignOperation,
21
+ encryptNote,
22
+ min,
23
+ iterChunks,
24
+ groupByArr,
25
+ SparseMerkleProver,
26
+ MerkleProofInput,
27
+ computeJoinSplitInfoCommitment,
28
+ computeSenderCommitment,
29
+ } from "@zill-protocol/core";
30
+ import { sortNotesByValue, getJoinSplitRequestTotalValue } from "./utils";
31
+
32
+ export const __private = {
33
+ gatherNotes,
34
+ };
35
+
36
+ export interface PrepareOperationDeps {
37
+ db: NocturneDB;
38
+ viewer: NocturneViewer;
39
+ merkle: SparseMerkleProver;
40
+ }
41
+
42
+ export async function prepareOperation(
43
+ deps: PrepareOperationDeps,
44
+ opRequest: GasAccountedOperationRequest
45
+ ): Promise<PreSignOperation> {
46
+ const { refunds, joinSplitRequests, chainId, tellerContract, deadline } =
47
+ opRequest;
48
+ const encodedGasAsset = AssetTrait.encode(opRequest.gasAsset);
49
+
50
+ // if refundAddr is not set, generate a random one
51
+ const refundAddr = StealthAddressTrait.compress(
52
+ opRequest.refundAddr ?? deps.viewer.generateRandomStealthAddress()
53
+ );
54
+
55
+ // prepare joinSplits
56
+ let joinSplits: PreSignJoinSplit[] = [];
57
+ const usedMerkleIndices = new Set<number>();
58
+ for (const joinSplitRequest of joinSplitRequests) {
59
+ console.log("preparing joinSplits for request: ", joinSplitRequest);
60
+ const newJoinSplits = await prepareJoinSplits(
61
+ deps,
62
+ joinSplitRequest,
63
+ refundAddr,
64
+ usedMerkleIndices
65
+ );
66
+
67
+ newJoinSplits.forEach((js) => {
68
+ // If note value == 0, its just a dummy and we don't want to count in used merkle indices
69
+ if (js.oldNoteA.value !== 0n) {
70
+ usedMerkleIndices.add(js.oldNoteA.merkleIndex);
71
+ }
72
+ if (js.oldNoteB.value !== 0n) {
73
+ usedMerkleIndices.add(js.oldNoteB.merkleIndex);
74
+ }
75
+ });
76
+ joinSplits.push(...newJoinSplits);
77
+ }
78
+
79
+ joinSplits = groupByArr(joinSplits, (joinSplit) =>
80
+ AssetTrait.encodedAssetToString(joinSplit.encodedAsset)
81
+ ).flat();
82
+
83
+ // construct op
84
+ const op: PreSignOperation = {
85
+ networkInfo: { chainId, tellerContract },
86
+ refundAddr,
87
+ joinSplits,
88
+ actions: opRequest.actions,
89
+ refunds,
90
+ encodedGasAsset,
91
+ gasAssetRefundThreshold: opRequest.gasAssetRefundThreshold,
92
+ executionGasLimit: opRequest.executionGasLimit,
93
+ gasPrice: opRequest.gasPrice,
94
+ deadline,
95
+ atomicActions: true, // always default to atomic until we find reason not to
96
+
97
+ gasFeeEstimate: opRequest.totalGasLimit * opRequest.gasPrice,
98
+ };
99
+
100
+ return op;
101
+ }
102
+
103
+ async function prepareJoinSplits(
104
+ { db, viewer, merkle }: PrepareOperationDeps,
105
+ joinSplitRequest: JoinSplitRequest,
106
+ refundAddr: CompressedStealthAddress,
107
+ alreadyUsedNoteMerkleIndices: Set<number> = new Set()
108
+ ): Promise<PreSignJoinSplit[]> {
109
+ let notes: IncludedNote[];
110
+ if (joinSplitRequest.notes && joinSplitRequest.notes.length > 0) {
111
+ notes = joinSplitRequest.notes;
112
+ const mismatchedAsset = notes.find(
113
+ (note) => !AssetTrait.isSameAsset(note.asset, joinSplitRequest.asset)
114
+ );
115
+ if (mismatchedAsset) {
116
+ throw new Error("join split request notes asset mismatch");
117
+ }
118
+ const reusedNote = notes.find((note) =>
119
+ alreadyUsedNoteMerkleIndices.has(note.merkleIndex)
120
+ );
121
+ if (reusedNote) {
122
+ throw new Error("join split request notes already used");
123
+ }
124
+ } else {
125
+ notes = await gatherNotes(
126
+ db,
127
+ getJoinSplitRequestTotalValue(joinSplitRequest),
128
+ joinSplitRequest.asset,
129
+ alreadyUsedNoteMerkleIndices
130
+ );
131
+ }
132
+
133
+ const unwrapAmount = joinSplitRequest.unwrapValue;
134
+ const paymentAmount = joinSplitRequest.payment?.value ?? 0n;
135
+
136
+ const totalNotesValue = notes.reduce((acc, note) => acc + note.value, 0n);
137
+ const amountToReturn = totalNotesValue - unwrapAmount - paymentAmount;
138
+
139
+ const receiver = joinSplitRequest.payment?.receiver;
140
+
141
+ console.log(`getting joinsplits from notes. Num notes: ${notes.length}`);
142
+ return getJoinSplitsFromNotes(
143
+ viewer,
144
+ merkle,
145
+ notes,
146
+ paymentAmount,
147
+ amountToReturn,
148
+ refundAddr,
149
+ receiver
150
+ );
151
+ }
152
+
153
+ export class NotEnoughFundsError extends Error {
154
+ constructor(
155
+ public readonly requestedAmount: bigint,
156
+ public readonly ownedAmount: bigint,
157
+ public readonly asset: Asset
158
+ ) {
159
+ super(
160
+ `attempted to spend more funds than owned. Address: ${asset.assetAddr}. Attempted: ${requestedAmount}. Owned: ${ownedAmount}.`
161
+ );
162
+ this.name = "NotEnoughFundsError";
163
+ }
164
+ }
165
+
166
+ export async function gatherNotes(
167
+ db: NocturneDB,
168
+ requestedAmount: bigint,
169
+ asset: Asset,
170
+ noteMerkleIndicesToIgnore: Set<number> = new Set()
171
+ ): Promise<IncludedNote[]> {
172
+ console.log("indices to ignore", noteMerkleIndicesToIgnore);
173
+
174
+ // check that the user has enough notes to cover the request
175
+ const notes = (await db.getNotesForAsset(asset)).filter(
176
+ (n) => !noteMerkleIndicesToIgnore.has(n.merkleIndex)
177
+ );
178
+ const balance = notes.reduce((acc, note) => acc + note.value, 0n);
179
+ if (balance < requestedAmount) {
180
+ // TODO: have a better way to handle following edge case:
181
+ // 1. there are multiple JS requests for the same asset
182
+ // 2. the user has enough notes to cover the total amount
183
+ // 3. the user does *not* have enough to cover each request individually.
184
+ throw new NotEnoughFundsError(requestedAmount, balance, asset);
185
+ }
186
+
187
+ // Goal: want to utilize small notes so they don't pile up.
188
+ // But we also don't want to use too many notes because that will increase the gas cost.
189
+ // So we take the following approach that strikes a good balance
190
+ // 1. sort notes from small to large
191
+ // 2. compute the sums of each sequence of notes starting from the smallest.
192
+ // Stop when the sum is >= to the requested amount.
193
+ // 3. until we've gathered notes totalling at least the requested amount, repeat the following:
194
+ // a. find the smallest subsequence sum that is >= to the remaining amount to gather
195
+ // b. add the largest note of that subsequence to the set of notes to use.
196
+ // 4. If this process results in an odd number of notes to spend and there is still room, grab the smallest unused note for dust collection
197
+
198
+ // 1. Sort notes from small to large
199
+ const sortedNotes = sortNotesByValue(notes);
200
+
201
+ // 2. compute the subsequence sums
202
+ const subsequenceSums: bigint[] = [];
203
+ let curr = 0n;
204
+ for (const note of sortedNotes) {
205
+ curr += note.value;
206
+ subsequenceSums.push(curr);
207
+ }
208
+
209
+ // 3. Construct the set of notes to use.
210
+ const notesToUse: IncludedNote[] = [];
211
+ const usedNoteIndexes: Set<number> = new Set();
212
+ let remainingAmount = requestedAmount;
213
+ let subseqIndex = subsequenceSums.length - 1;
214
+ while (remainingAmount > 0n) {
215
+ // find the index of smallest subsequence sum >= remaining amount to gather
216
+ // the note at that index is the next note to add
217
+ while (
218
+ subseqIndex > 0 &&
219
+ subsequenceSums[subseqIndex - 1] >= remainingAmount
220
+ ) {
221
+ subseqIndex--;
222
+ }
223
+
224
+ const note = sortedNotes[subseqIndex];
225
+ notesToUse.push(note);
226
+ remainingAmount -= note.value;
227
+
228
+ usedNoteIndexes.add(subseqIndex);
229
+ // Skip to next note
230
+ subseqIndex--;
231
+ }
232
+
233
+ // 4. If this process results in an odd number of notes to spend and there is still room, grab the smallest unused note for dust collection
234
+ if (notesToUse.length % 2 == 1 && notesToUse.length < notes.length) {
235
+ const smallestUnusedNote = sortedNotes.find(
236
+ (note, i) => !usedNoteIndexes.has(i)
237
+ );
238
+ if (smallestUnusedNote) {
239
+ notesToUse.push(smallestUnusedNote);
240
+ }
241
+ }
242
+
243
+ console.log(
244
+ `gathered notes to satisfy request for ${requestedAmount} of assest ${asset.assetAddr}`,
245
+ { notesToUse, requestedAmount, asset }
246
+ );
247
+ return notesToUse;
248
+ }
249
+
250
+ async function getJoinSplitsFromNotes(
251
+ viewer: NocturneViewer,
252
+ merkle: SparseMerkleProver,
253
+ notes: IncludedNote[],
254
+ paymentAmount: bigint,
255
+ amountLeftOver: bigint,
256
+ refundAddr: CompressedStealthAddress,
257
+ receiver?: CanonAddress
258
+ ): Promise<PreSignJoinSplit[]> {
259
+ // add a dummy note if there are an odd number of notes.
260
+ if (notes.length % 2 == 1) {
261
+ const newAddr = viewer.generateRandomStealthAddress();
262
+ const nonce = randomFr();
263
+ notes.push({
264
+ owner: newAddr,
265
+ nonce,
266
+ asset: notes[0].asset,
267
+ value: 0n,
268
+ merkleIndex: 0,
269
+ });
270
+ }
271
+
272
+ // for each pair of notes, create a JoinSplit with the maximum possible value transfer
273
+ const res = [];
274
+ let remainingPayment = paymentAmount;
275
+ let remainingAmountLeftOver = amountLeftOver;
276
+ for (const [noteA, noteB] of iterChunks(notes, 2)) {
277
+ const pairTotalValue = noteA.value + noteB.value;
278
+ const amountToReturn = min(remainingAmountLeftOver, pairTotalValue);
279
+ remainingAmountLeftOver -= amountToReturn;
280
+
281
+ const remainingPairValue = pairTotalValue - amountToReturn;
282
+ const paymentAmount = min(remainingPairValue, remainingPayment);
283
+ remainingPayment -= paymentAmount;
284
+
285
+ const joinSplit = await makeJoinSplit(
286
+ viewer,
287
+ merkle,
288
+ noteA,
289
+ noteB,
290
+ paymentAmount,
291
+ amountToReturn,
292
+ refundAddr,
293
+ receiver
294
+ );
295
+
296
+ res.push(joinSplit);
297
+ }
298
+
299
+ return res;
300
+ }
301
+
302
+ async function makeJoinSplit(
303
+ viewer: NocturneViewer,
304
+ merkle: SparseMerkleProver,
305
+ oldNoteA: IncludedNote,
306
+ oldNoteB: IncludedNote,
307
+ paymentAmount: bigint,
308
+ amountToReturn: bigint,
309
+ refundAddr: CompressedStealthAddress,
310
+ receiver?: CanonAddress
311
+ ): Promise<PreSignJoinSplit> {
312
+ const sender = viewer.canonicalAddress();
313
+ // if receiver not given, assumme the sender is the receiver
314
+ receiver = receiver ?? sender;
315
+
316
+ const encodedAsset = AssetTrait.encode(oldNoteA.asset);
317
+
318
+ // whatever isn't being sent to the receiver or ourselves is unwrapped and spent in cleartext (presumably as part of an action)
319
+ const totalValue = oldNoteA.value + oldNoteB.value;
320
+ const publicSpend = totalValue - amountToReturn - paymentAmount;
321
+
322
+ const nullifierA = NoteTrait.createNullifier(viewer, oldNoteA);
323
+ const nullifierB = NoteTrait.createNullifier(viewer, oldNoteB);
324
+
325
+ // first note contains the leftovers - return to sender
326
+ const newNoteA: Note = {
327
+ owner: StealthAddressTrait.fromCanonAddress(sender),
328
+ nonce: NoteTrait.generateNewNonce(viewer, nullifierA),
329
+ asset: oldNoteA.asset,
330
+ value: amountToReturn,
331
+ };
332
+
333
+ // the second note contains the confidential payment
334
+ const newNoteB: Note = {
335
+ owner: StealthAddressTrait.fromCanonAddress(receiver),
336
+ nonce: NoteTrait.generateNewNonce(viewer, nullifierB),
337
+ asset: oldNoteA.asset,
338
+ value: paymentAmount,
339
+ };
340
+
341
+ const newNoteACommitment = NoteTrait.toCommitment(newNoteA);
342
+ const newNoteBCommitment = NoteTrait.toCommitment(newNoteB);
343
+
344
+ const newNoteAEncrypted = encryptNote(sender, { ...newNoteA, sender });
345
+ const newNoteBEncrypted = encryptNote(receiver, { ...newNoteB, sender });
346
+
347
+ const membershipProof = merkle.getProof(oldNoteA.merkleIndex);
348
+ const commitmentTreeRoot = membershipProof.root;
349
+ const merkleProofA: MerkleProofInput = {
350
+ path: membershipProof.pathIndices.map((n) => BigInt(n)),
351
+ siblings: membershipProof.siblings,
352
+ };
353
+
354
+ // noteB could have been a dummy note. If it is, we simply duplicate the merkle proof for noteA
355
+ // the circuit will ignore the merkle proof for noteB if it has a value of 0
356
+ const noteBIsDummy = oldNoteB.value === 0n;
357
+ let merkleProofB: MerkleProofInput;
358
+ if (noteBIsDummy) {
359
+ oldNoteB.merkleIndex = oldNoteA.merkleIndex;
360
+ merkleProofB = merkleProofA;
361
+ } else {
362
+ const membershipProof = merkle.getProof(oldNoteB.merkleIndex);
363
+
364
+ // ! merkle tree could be asynchronously updated between us getting the first and second merkle proofs
365
+ // TODO: add a `merkle.getManyProofs` method that does it in one go
366
+ if (membershipProof.root !== commitmentTreeRoot) {
367
+ throw Error(
368
+ "merkleProver was updated between getting the first and second merkle proofs!"
369
+ );
370
+ }
371
+
372
+ merkleProofB = {
373
+ path: membershipProof.pathIndices.map((n) => BigInt(n)),
374
+ siblings: membershipProof.siblings,
375
+ };
376
+ }
377
+
378
+ // commit to the sender's canonical address
379
+ const senderCanonAddr = viewer.canonicalAddress();
380
+ const senderCommitment = computeSenderCommitment(
381
+ senderCanonAddr,
382
+ newNoteB.nonce
383
+ );
384
+
385
+ // compute joinsplit info commitment
386
+ const joinSplitInfoCommitment = computeJoinSplitInfoCommitment(
387
+ viewer,
388
+ receiver,
389
+ oldNoteA,
390
+ oldNoteB,
391
+ newNoteA,
392
+ newNoteB
393
+ );
394
+
395
+ return {
396
+ receiver,
397
+ encodedAsset,
398
+ publicSpend,
399
+
400
+ nullifierA,
401
+ nullifierB,
402
+ oldNoteA,
403
+ oldNoteB,
404
+
405
+ newNoteA,
406
+ newNoteB,
407
+ newNoteAEncrypted,
408
+ newNoteBEncrypted,
409
+
410
+ commitmentTreeRoot,
411
+ newNoteACommitment,
412
+ newNoteBCommitment,
413
+ merkleProofA,
414
+ merkleProofB,
415
+
416
+ senderCommitment,
417
+ joinSplitInfoCommitment,
418
+ refundAddr,
419
+ };
420
+ }
@@ -0,0 +1,124 @@
1
+ import { decomposeCompressedPoint } from "@zill-protocol/crypto";
2
+ import {
3
+ PreProofJoinSplit,
4
+ SignedOperation,
5
+ ProvenJoinSplit,
6
+ ProvenOperation,
7
+ OperationTrait,
8
+ SubmittableOperationWithNetworkInfo,
9
+ JoinSplitProver,
10
+ joinSplitPublicSignalsFromArray,
11
+ packToSolidityProof,
12
+ iterChunks,
13
+ } from "@zill-protocol/core";
14
+
15
+ // SDK will fire off at most this many provers in parallel
16
+ const DEFAULT_MAX_PARALLEL_PROVERS = 4;
17
+
18
+ export type ProveOperationOptions = {
19
+ maxParallel?: number;
20
+ debug?: boolean;
21
+ };
22
+
23
+ export async function proveOperation(
24
+ prover: JoinSplitProver,
25
+ op: SignedOperation,
26
+ opts?: ProveOperationOptions
27
+ ): Promise<SubmittableOperationWithNetworkInfo> {
28
+ const joinSplits: ProvenJoinSplit[] = [];
29
+ const maxParallel = opts?.maxParallel ?? DEFAULT_MAX_PARALLEL_PROVERS;
30
+ for (const batch of iterChunks(op.joinSplits, maxParallel)) {
31
+ const provenBatch = await Promise.all(
32
+ batch.map((joinSplit) => proveJoinSplit(prover, joinSplit, opts))
33
+ );
34
+ joinSplits.push(...provenBatch);
35
+ }
36
+
37
+ const operation: ProvenOperation = {
38
+ ...op,
39
+ joinSplits,
40
+ };
41
+
42
+ return OperationTrait.toSubmittable(operation);
43
+ }
44
+
45
+ async function proveJoinSplit(
46
+ prover: JoinSplitProver,
47
+ signedJoinSplit: PreProofJoinSplit,
48
+ opts?: ProveOperationOptions
49
+ ): Promise<ProvenJoinSplit> {
50
+ const {
51
+ opDigest,
52
+ proofInputs,
53
+ refundAddr,
54
+ senderCommitment,
55
+ joinSplitInfoCommitment,
56
+ ...baseJoinSplit
57
+ } = signedJoinSplit;
58
+ if (opts?.debug) {
59
+ console.log("proving joinSplit", {
60
+ proofInputs,
61
+ merkleProofA: proofInputs.merkleProofA,
62
+ merkleProofB: proofInputs.merkleProofB,
63
+ });
64
+ }
65
+
66
+ const proof = await prover.proveJoinSplit(proofInputs);
67
+
68
+ const [, refundAddrH1CompressedY] = decomposeCompressedPoint(refundAddr.h1);
69
+ const [, refundAddrH2CompressedY] = decomposeCompressedPoint(refundAddr.h2);
70
+
71
+ // Check that snarkjs output is consistent with our precomputed joinsplit values
72
+ const publicSignals = joinSplitPublicSignalsFromArray(proof.publicSignals);
73
+ if (
74
+ baseJoinSplit.newNoteACommitment !== publicSignals.newNoteACommitment ||
75
+ baseJoinSplit.newNoteBCommitment !== publicSignals.newNoteBCommitment ||
76
+ baseJoinSplit.commitmentTreeRoot !== publicSignals.commitmentTreeRoot ||
77
+ baseJoinSplit.publicSpend !== publicSignals.publicSpend ||
78
+ baseJoinSplit.nullifierA !== publicSignals.nullifierA ||
79
+ baseJoinSplit.nullifierB !== publicSignals.nullifierB ||
80
+ opDigest !== publicSignals.opDigest ||
81
+ refundAddrH1CompressedY !== publicSignals.refundAddrH1CompressedY ||
82
+ refundAddrH2CompressedY !== publicSignals.refundAddrH2CompressedY ||
83
+ senderCommitment !== publicSignals.senderCommitment ||
84
+ joinSplitInfoCommitment !== publicSignals.joinSplitInfoCommitment
85
+ ) {
86
+ console.error("successfully generated proof, but PIs don't match", {
87
+ publicSignalsFromProof: publicSignals,
88
+ publicSignalsExpected: {
89
+ newNoteACommitment: baseJoinSplit.newNoteACommitment,
90
+ newNoteBCommitment: baseJoinSplit.newNoteBCommitment,
91
+ commitmentTreeRoot: baseJoinSplit.commitmentTreeRoot,
92
+ publicSpend: baseJoinSplit.publicSpend,
93
+ nullifierA: baseJoinSplit.nullifierA,
94
+ nullifierB: baseJoinSplit.nullifierB,
95
+ senderCommitment,
96
+ joinSplitInfoCommitment,
97
+ opDigest,
98
+ pubEncododedAssetAddrWithSignBits:
99
+ proofInputs.pubEncodedAssetAddrWithSignBits,
100
+ pubEncodedAssetID: proofInputs.pubEncodedAssetId,
101
+ refundAddrH1CompressedY: refundAddrH1CompressedY,
102
+ refundAddrH2CompressedY: refundAddrH2CompressedY,
103
+ },
104
+ });
105
+
106
+ throw new Error(
107
+ `snarkjs generated public input differs from precomputed ones`
108
+ );
109
+ }
110
+
111
+ if (opts?.debug) {
112
+ console.log("successfully generated proofs", {
113
+ proofWithPublicSignals: proof,
114
+ });
115
+ }
116
+
117
+ const solidityProof = packToSolidityProof(proof.proof);
118
+ return {
119
+ proof: solidityProof,
120
+ senderCommitment,
121
+ joinSplitInfoCommitment,
122
+ ...baseJoinSplit,
123
+ };
124
+ }
@@ -0,0 +1,116 @@
1
+ import {
2
+ PreSignJoinSplit,
3
+ NoteTrait,
4
+ PreSignOperation,
5
+ PreProofJoinSplit,
6
+ SignedOperation,
7
+ JoinSplitInputs,
8
+ NocturneSigner,
9
+ encodeEncodedAssetAddrWithSignBitsPI,
10
+ NocturneSignature,
11
+ OperationTrait,
12
+ } from "@zill-protocol/core";
13
+
14
+ export function signOperation(
15
+ signer: NocturneSigner,
16
+ op: PreSignOperation
17
+ ): SignedOperation {
18
+ const opDigest = OperationTrait.computeDigest(OperationTrait.toSignable(op));
19
+ const opSig = signer.sign(opDigest);
20
+
21
+ const joinSplits: PreProofJoinSplit[] = op.joinSplits.map((joinSplit) =>
22
+ makePreProofJoinSplit(signer, joinSplit, opDigest, opSig)
23
+ );
24
+
25
+ const {
26
+ networkInfo,
27
+ actions,
28
+ refundAddr,
29
+ refunds,
30
+ encodedGasAsset,
31
+ gasAssetRefundThreshold,
32
+ executionGasLimit,
33
+ gasPrice,
34
+ deadline,
35
+ atomicActions,
36
+ } = op;
37
+
38
+ return {
39
+ networkInfo,
40
+ joinSplits,
41
+ refundAddr,
42
+ refunds,
43
+ actions,
44
+ encodedGasAsset,
45
+ gasAssetRefundThreshold,
46
+ executionGasLimit,
47
+ gasPrice,
48
+ deadline,
49
+ atomicActions,
50
+ };
51
+ }
52
+
53
+ function makePreProofJoinSplit(
54
+ signer: NocturneSigner,
55
+ preProofJoinSplit: PreSignJoinSplit,
56
+ opDigest: bigint,
57
+ opSig: NocturneSignature
58
+ ): PreProofJoinSplit {
59
+ const {
60
+ merkleProofA,
61
+ merkleProofB,
62
+ oldNoteA,
63
+ oldNoteB,
64
+ newNoteA,
65
+ newNoteB,
66
+ receiver,
67
+ refundAddr,
68
+ senderCommitment,
69
+ publicSpend,
70
+ ...baseJoinSplit
71
+ } = preProofJoinSplit;
72
+
73
+ const { c, z } = opSig;
74
+
75
+ const { x, y } = signer.spendPk;
76
+
77
+ const encodedOldNoteA = NoteTrait.encode(oldNoteA);
78
+ const encodedOldNoteB = NoteTrait.encode(oldNoteB);
79
+ const encodedNewNoteA = NoteTrait.encode(newNoteA);
80
+ const encodedNewNoteB = NoteTrait.encode(newNoteB);
81
+
82
+ // if publicSpend is 0, hide the asset info by masking it to 0
83
+ const pubEncodedAssetAddrWithSignBits = encodeEncodedAssetAddrWithSignBitsPI(
84
+ publicSpend === 0n ? 0n : encodedNewNoteA.encodedAssetAddr,
85
+ refundAddr
86
+ );
87
+ const pubEncodedAssetId =
88
+ publicSpend === 0n ? 0n : encodedNewNoteA.encodedAssetId;
89
+
90
+ const proofInputs: JoinSplitInputs = {
91
+ vk: signer.vk,
92
+ vkNonce: signer.vkNonce,
93
+ spendPk: [x, y],
94
+ c,
95
+ z,
96
+ merkleProofA,
97
+ merkleProofB,
98
+ operationDigest: opDigest,
99
+ oldNoteA: encodedOldNoteA,
100
+ oldNoteB: encodedOldNoteB,
101
+ newNoteA: encodedNewNoteA,
102
+ newNoteB: encodedNewNoteB,
103
+ refundAddr,
104
+ pubEncodedAssetAddrWithSignBits,
105
+ pubEncodedAssetId,
106
+ };
107
+
108
+ return {
109
+ opDigest,
110
+ proofInputs,
111
+ refundAddr,
112
+ senderCommitment,
113
+ publicSpend,
114
+ ...baseJoinSplit,
115
+ };
116
+ }