@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,44 @@
1
+ import { OperationStatus, OperationStatusResponse } from "@zill-protocol/core";
2
+
3
+ export interface OpTracker {
4
+ operationIsInFlight(opDigest: bigint): Promise<boolean>;
5
+ }
6
+
7
+ export class BundlerOpTracker implements OpTracker {
8
+ endpoint: string;
9
+
10
+ constructor(bundlerEndpoint: string) {
11
+ this.endpoint = bundlerEndpoint;
12
+ }
13
+
14
+ async operationIsInFlight(opDigest: bigint): Promise<boolean> {
15
+ const res = await fetch(
16
+ `${this.endpoint}/operations/${opDigest.toString()}`,
17
+ {
18
+ method: "GET",
19
+ }
20
+ );
21
+
22
+ let response: OperationStatusResponse;
23
+ try {
24
+ response = await res.json();
25
+ } catch (err) {
26
+ throw new Error(`failed to parse bundler response: ${err}`);
27
+ }
28
+
29
+ return (
30
+ response.status === OperationStatus.QUEUED ||
31
+ response.status === OperationStatus.PRE_BATCH ||
32
+ response.status === OperationStatus.IN_BATCH ||
33
+ response.status === OperationStatus.IN_FLIGHT
34
+ );
35
+ }
36
+ }
37
+
38
+ export class MockOpTracker implements OpTracker {
39
+ constructor() {}
40
+
41
+ async operationIsInFlight(_opDigest: bigint): Promise<boolean> {
42
+ return true;
43
+ }
44
+ }
@@ -0,0 +1,22 @@
1
+ export abstract class EthToTokenConverter {
2
+ abstract weiToTargetErc20(
3
+ amountWei: bigint,
4
+ targetTicker: string
5
+ ): Promise<bigint>;
6
+
7
+ async gasEstimatesInGasAssets(
8
+ amountWei: bigint,
9
+ tickers: string[]
10
+ ): Promise<Map<string, bigint>> {
11
+ const gasEstimates = new Map<string, bigint>();
12
+
13
+ await Promise.all(
14
+ tickers.map(async (ticker) => {
15
+ const targetErc20 = await this.weiToTargetErc20(amountWei, ticker);
16
+ gasEstimates.set(ticker, targetErc20);
17
+ })
18
+ );
19
+
20
+ return gasEstimates;
21
+ }
22
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./converter";
2
+ export { MockEthToTokenConverter } from "./mock";
@@ -0,0 +1,11 @@
1
+ import { EthToTokenConverter } from "./converter";
2
+
3
+ export class MockEthToTokenConverter extends EthToTokenConverter {
4
+ constructor() {
5
+ super();
6
+ }
7
+
8
+ weiToTargetErc20(amountWei: bigint, _targetTicker: string): Promise<bigint> {
9
+ return Promise.resolve(amountWei);
10
+ }
11
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export * from "./types";
2
+ export * from "./conversion";
3
+ export * from "./operationRequest";
4
+ export * from "./snapJsonRpc";
5
+
6
+ export { NocturneClient } from "./NocturneClient";
7
+ export { NocturneDB, GetNotesOpts } from "./NocturneDB";
8
+ export { SyncOpts } from "./syncSDK";
9
+ export { BundlerOpTracker } from "./OpTracker";
10
+ export { NotEnoughGasTokensError } from "./opRequestGas";
11
+ export { NotEnoughFundsError } from "./prepareOperation";
12
+ export { signOperation } from "./signOperation";
13
+ export { proveOperation } from "./proveOperation";
14
+ export { isTerminalOpStatus, isFailedOpStatus } from "./utils";
@@ -0,0 +1,487 @@
1
+ import { Handler } from "@zill-protocol/contracts";
2
+ import {
3
+ Asset,
4
+ AssetType,
5
+ BLOCK_GAS_LIMIT,
6
+ ERC20_ID,
7
+ IncludedNote,
8
+ MAX_GAS_FOR_ADDITIONAL_JOINSPLIT,
9
+ MapWithObjectKeys,
10
+ AssetTrait,
11
+ Operation,
12
+ OperationResult,
13
+ OperationTrait,
14
+ PreSignOperation,
15
+ ProvenJoinSplit,
16
+ SparseMerkleProver,
17
+ SubmittableOperationWithNetworkInfo,
18
+ groupByMap,
19
+ maxGasForOperation,
20
+ partition,
21
+ } from "@zill-protocol/core";
22
+ import { NocturneViewer, StealthAddress } from "@zill-protocol/crypto";
23
+ import * as JSON from "bigint-json-serialization";
24
+ import { NocturneDB } from "./NocturneDB";
25
+ import { EthToTokenConverter } from "./conversion";
26
+ import {
27
+ GasAccountedOperationRequest,
28
+ JoinSplitRequest,
29
+ OperationRequest,
30
+ } from "./operationRequest/operationRequest";
31
+ import { gatherNotes, prepareOperation } from "./prepareOperation";
32
+ import { getIncludedNotesFromOp, getJoinSplitRequestTotalValue } from "./utils";
33
+
34
+ // If gas asset refund is less than this amount * gasPrice denominated in the gas asset, refund will
35
+ // not be processed and funds will be sent to bundler. This is because cost of processing would
36
+ // outweight value of note.
37
+ const DEFAULT_GAS_ASSET_REFUND_THRESHOLD_GAS = 600_000n;
38
+
39
+ const DUMMY_GAS_ASSET: Asset = {
40
+ assetType: AssetType.ERC20,
41
+ assetAddr: "0x0000000000000000000000000000000000000000",
42
+ id: ERC20_ID,
43
+ };
44
+
45
+ interface AssetAndTicker {
46
+ asset: Asset;
47
+ ticker: string;
48
+ }
49
+
50
+ export interface HandleOpRequestGasDeps {
51
+ db: NocturneDB;
52
+ handlerContract: Handler;
53
+ gasAssets: Map<string, Asset>;
54
+ tokenConverter: EthToTokenConverter;
55
+ merkle: SparseMerkleProver;
56
+ }
57
+
58
+ interface GasEstimatedOperationRequest
59
+ extends Omit<OperationRequest, "executionGasLimit" | "gasPrice"> {
60
+ executionGasLimit: bigint;
61
+ gasPrice: bigint;
62
+ }
63
+
64
+ interface OpRequestTraceParams {
65
+ totalGasLimit: bigint;
66
+ executionGasLimit: bigint;
67
+ gasPrice: bigint;
68
+ usedNotes: MapWithObjectKeys<Asset, IncludedNote[]>;
69
+ }
70
+
71
+ // VK corresponding to SK of 1 with minimum valid nonce
72
+ const DUMMY_VIEWER = new NocturneViewer(
73
+ 655374300543486358510310527362452574140738137308572239595396943710924175576n,
74
+ 3n
75
+ );
76
+
77
+ const DUMMY_REFUND_ADDR: StealthAddress =
78
+ DUMMY_VIEWER.generateRandomStealthAddress();
79
+
80
+ export class NotEnoughGasTokensError extends Error {
81
+ constructor(
82
+ public readonly gasAssets: Asset[],
83
+ public readonly gasEstimates: bigint[],
84
+ public readonly gasAssetBalances: bigint[]
85
+ ) {
86
+ super("Not enough gas to execute operation");
87
+ this.name = "NotEnoughGasTokensError";
88
+ }
89
+ }
90
+
91
+ export async function handleGasForOperationRequest(
92
+ deps: HandleOpRequestGasDeps,
93
+ opRequest: OperationRequest,
94
+ gasMultiplier: number
95
+ ): Promise<GasAccountedOperationRequest> {
96
+ // estimate gas params for opRequest
97
+ console.log("estimating gas for op request");
98
+ const { totalGasLimit, executionGasLimit, gasPrice, usedNotes } =
99
+ await getOperationRequestTrace(deps, opRequest, gasMultiplier);
100
+
101
+ const gasEstimatedOpRequest: GasEstimatedOperationRequest = {
102
+ ...opRequest,
103
+ executionGasLimit,
104
+ gasPrice,
105
+ };
106
+
107
+ if (opRequest?.gasPrice == 0n) {
108
+ // If gasPrice = 0, override dummy gas asset and don't further modify opRequest
109
+ console.log("returning dummy gas asset");
110
+ return {
111
+ ...gasEstimatedOpRequest,
112
+ gasPrice: 0n,
113
+ gasAsset: DUMMY_GAS_ASSET,
114
+ gasAssetRefundThreshold: 0n,
115
+ totalGasLimit,
116
+ };
117
+ } else {
118
+ console.log(`total gas limit pre gas update: ${totalGasLimit}`);
119
+
120
+ // attempt to update the joinSplitRequests with gas compensation
121
+ // gasAsset will be `undefined` if the user's too broke to pay for gas
122
+ const [joinSplitRequests, gasAssetAndTicker] =
123
+ await tryUpdateJoinSplitRequestsForGasEstimate(
124
+ deps.gasAssets,
125
+ deps.db,
126
+ gasEstimatedOpRequest.joinSplitRequests,
127
+ totalGasLimit,
128
+ gasPrice,
129
+ deps.tokenConverter,
130
+ usedNotes
131
+ );
132
+
133
+ const gasAssetRefundThreshold = await deps.tokenConverter.weiToTargetErc20(
134
+ DEFAULT_GAS_ASSET_REFUND_THRESHOLD_GAS * gasPrice,
135
+ gasAssetAndTicker.ticker
136
+ );
137
+
138
+ return {
139
+ ...gasEstimatedOpRequest,
140
+ gasAssetRefundThreshold,
141
+ joinSplitRequests,
142
+ gasAsset: gasAssetAndTicker.asset,
143
+ totalGasLimit,
144
+ };
145
+ }
146
+ }
147
+
148
+ // update the joinSplitRequests to include any additional gas compensation, if needed
149
+ // returns the updated JoinSplitRequests and the gas asset used to pay for gas if the user can afford gas
150
+ // if the user can't afford gas, returns an empty array and undefined.
151
+ async function tryUpdateJoinSplitRequestsForGasEstimate(
152
+ gasAssets: Map<string, Asset>,
153
+ db: NocturneDB,
154
+ joinSplitRequests: JoinSplitRequest[],
155
+ gasUnitsEstimate: bigint,
156
+ gasPrice: bigint,
157
+ tokenConverter: EthToTokenConverter,
158
+ usedNotes: MapWithObjectKeys<Asset, IncludedNote[]>
159
+ ): Promise<[JoinSplitRequest[], AssetAndTicker]> {
160
+ // group joinSplitRequests by asset address
161
+ const joinSplitRequestsByAsset = groupByMap(
162
+ joinSplitRequests,
163
+ (request) => request.asset.assetAddr
164
+ );
165
+ const gasCompJoinSplitRequestsByAsset = groupByMap(
166
+ joinSplitRequests.filter(
167
+ (request) => request.allowGasCompensation !== false
168
+ ),
169
+ (request) => request.asset.assetAddr
170
+ );
171
+
172
+ const gasEstimateWei = gasPrice * gasUnitsEstimate;
173
+ const gasEstimatesInGasAssets = await tokenConverter.gasEstimatesInGasAssets(
174
+ gasEstimateWei,
175
+ Array.from(gasAssets.keys())
176
+ );
177
+
178
+ const [matchingGasAssets, nonMatchingGasAssets] = partition(
179
+ Array.from(gasAssets.entries()),
180
+ ([_, gasAsset]) => gasCompJoinSplitRequestsByAsset.has(gasAsset.assetAddr)
181
+ );
182
+
183
+ const failedGasEstimates = [];
184
+ const failedGasAssets = [];
185
+ const failedGasAssetBalances = [];
186
+
187
+ // attempt to find matching gas asset with enough balance
188
+ for (const [ticker, gasAsset] of matchingGasAssets) {
189
+ const totalOwnedGasAsset = await db.getBalanceForAsset(gasAsset);
190
+ const matchingJoinSplitRequests = gasCompJoinSplitRequestsByAsset.get(
191
+ gasAsset.assetAddr
192
+ );
193
+ if (!matchingJoinSplitRequests || matchingJoinSplitRequests.length === 0) {
194
+ continue;
195
+ }
196
+
197
+ const totalAmountInMatchingJoinSplitRequests =
198
+ matchingJoinSplitRequests.reduce((acc, request) => {
199
+ return acc + getJoinSplitRequestTotalValue(request);
200
+ }, 0n);
201
+
202
+ // if they have enough for request + gas, modify one of the requests to include the gas, and
203
+ // we're done
204
+ const estimateInGasAsset = gasEstimatesInGasAssets.get(ticker)!;
205
+ if (
206
+ totalOwnedGasAsset >=
207
+ estimateInGasAsset + totalAmountInMatchingJoinSplitRequests
208
+ ) {
209
+ const usedNotesForGasAsset =
210
+ usedNotes.get(gasAsset) ??
211
+ Array.from(usedNotes.entries()).find(([asset]) =>
212
+ AssetTrait.isSameAsset(asset, gasAsset)
213
+ )?.[1] ??
214
+ [];
215
+ const usedMerkleIndicesForGasAsset = new Set(
216
+ usedNotesForGasAsset.map((note) => note.merkleIndex)
217
+ );
218
+ const totalValueInExistingJoinSplits = usedNotesForGasAsset.reduce(
219
+ (acc, note) => acc + note.value,
220
+ 0n
221
+ );
222
+ const remainingValueNeededInNewJoinSplits =
223
+ totalAmountInMatchingJoinSplitRequests +
224
+ estimateInGasAsset -
225
+ totalValueInExistingJoinSplits;
226
+
227
+ let extraJoinSplitsGas: bigint;
228
+ let numExtraJoinSplits: number;
229
+ if (remainingValueNeededInNewJoinSplits > 0n) {
230
+ // Add enough to cover gas needed for existing joinsplits + gas for an extra joinsplits
231
+ const extraNotes = await gatherNotes(
232
+ db,
233
+ remainingValueNeededInNewJoinSplits,
234
+ gasAsset,
235
+ usedMerkleIndicesForGasAsset
236
+ );
237
+
238
+ console.log(`usedNotes: ${JSON.stringify(usedNotes)}`);
239
+ console.log(`need ${extraNotes.length} extra notes for gas comp`);
240
+ numExtraJoinSplits =
241
+ Math.ceil((usedNotesForGasAsset.length + extraNotes.length) / 2) -
242
+ Math.ceil(usedNotesForGasAsset.length / 2);
243
+ extraJoinSplitsGas =
244
+ BigInt(numExtraJoinSplits) *
245
+ MAX_GAS_FOR_ADDITIONAL_JOINSPLIT *
246
+ gasPrice;
247
+ const estimateInGasAssetIncludingNewJoinSplits =
248
+ estimateInGasAsset + extraJoinSplitsGas;
249
+
250
+ if (totalOwnedGasAsset < estimateInGasAssetIncludingNewJoinSplits) {
251
+ // Don't have enough for new joinsplits gas overhead, try new asset
252
+ failedGasAssets.push(gasAsset);
253
+ failedGasEstimates.push(estimateInGasAssetIncludingNewJoinSplits);
254
+ failedGasAssetBalances.push(totalOwnedGasAsset);
255
+ continue;
256
+ }
257
+ } else {
258
+ // otherwise we don't need any extra joinsplits because existing notes cover gas cost
259
+ extraJoinSplitsGas = 0n;
260
+ numExtraJoinSplits = 0;
261
+ }
262
+
263
+ console.log(
264
+ `adding ${extraJoinSplitsGas} tokens for ${numExtraJoinSplits} extra joinsplits for gas compensation`
265
+ );
266
+ matchingJoinSplitRequests[0].unwrapValue +=
267
+ estimateInGasAsset + extraJoinSplitsGas;
268
+ joinSplitRequestsByAsset.set(
269
+ gasAsset.assetAddr,
270
+ matchingJoinSplitRequests
271
+ );
272
+
273
+ console.log(
274
+ `amount to add for gas asset by modifying existing joinsplit for gas asset with ticker ${ticker}: ${estimateInGasAsset}}`,
275
+ { gasAssetTicker: ticker, gasAsset, estimateInGasAsset }
276
+ );
277
+
278
+ return [
279
+ Array.from(joinSplitRequestsByAsset.values()).flat(),
280
+ { asset: gasAsset, ticker },
281
+ ];
282
+ } else {
283
+ failedGasAssets.push(gasAsset);
284
+ failedGasEstimates.push(estimateInGasAsset);
285
+ failedGasAssetBalances.push(totalOwnedGasAsset);
286
+ }
287
+ }
288
+
289
+ // if we couldn't find an existing joinsplit request with a supported gas asset,
290
+ // attempt to make a new joinsplit request to include the gas comp
291
+ // iterate through each gas asset
292
+ for (const [ticker, gasAsset] of nonMatchingGasAssets) {
293
+ const estimateInGasAsset = gasEstimatesInGasAssets.get(ticker)!;
294
+ const totalOwnedGasAsset = await db.getBalanceForAsset(gasAsset);
295
+
296
+ if (totalOwnedGasAsset >= estimateInGasAsset) {
297
+ // Add enough to cover gas needed for existing joinsplits + gas for an extra joinsplits
298
+ const extraNotes = await gatherNotes(db, estimateInGasAsset, gasAsset);
299
+ const numExtraJoinSplits = Math.ceil(extraNotes.length / 2);
300
+ const additionaJoinSplitGas =
301
+ BigInt(numExtraJoinSplits) *
302
+ MAX_GAS_FOR_ADDITIONAL_JOINSPLIT *
303
+ gasPrice;
304
+ const estimateInGasAssetIncludingNewJoinSplits =
305
+ estimateInGasAsset + additionaJoinSplitGas;
306
+
307
+ if (totalOwnedGasAsset < estimateInGasAssetIncludingNewJoinSplits) {
308
+ // Don't have enough for new joinsplits gas overhead, try new asset
309
+ failedGasAssets.push(gasAsset);
310
+ failedGasEstimates.push(estimateInGasAssetIncludingNewJoinSplits);
311
+ failedGasAssetBalances.push(totalOwnedGasAsset);
312
+ continue;
313
+ }
314
+
315
+ const modifiedJoinSplitRequests = joinSplitRequests.concat({
316
+ asset: gasAsset,
317
+ unwrapValue: estimateInGasAssetIncludingNewJoinSplits,
318
+ });
319
+
320
+ console.log(
321
+ `amount to add for gas asset by adding a new joinsplit for gas asset with ticker ${ticker}: ${estimateInGasAssetIncludingNewJoinSplits}`,
322
+ {
323
+ gasAssetTicker: ticker,
324
+ gasAsset,
325
+ estimateInGasAssetIncludingNewJoinSplits,
326
+ }
327
+ );
328
+
329
+ return [modifiedJoinSplitRequests, { asset: gasAsset, ticker }];
330
+ } else {
331
+ failedGasAssets.push(gasAsset);
332
+ failedGasEstimates.push(estimateInGasAsset);
333
+ failedGasAssetBalances.push(totalOwnedGasAsset);
334
+ }
335
+ }
336
+
337
+ // if we get here, the user can't afford the gas
338
+ throw new NotEnoughGasTokensError(
339
+ failedGasAssets,
340
+ failedGasEstimates,
341
+ failedGasAssetBalances
342
+ );
343
+ }
344
+
345
+ // estimate gas params for opRequest
346
+ async function getOperationRequestTrace(
347
+ { handlerContract, ...deps }: HandleOpRequestGasDeps,
348
+ opRequest: OperationRequest,
349
+ gasMultiplier: number
350
+ ): Promise<OpRequestTraceParams> {
351
+ let { executionGasLimit, gasPrice } = opRequest;
352
+
353
+ // Simulate operation to get number of joinSplits
354
+ const dummyOpRequest: GasAccountedOperationRequest = {
355
+ ...opRequest,
356
+ gasAssetRefundThreshold: 0n,
357
+ executionGasLimit: BLOCK_GAS_LIMIT,
358
+ refundAddr: DUMMY_REFUND_ADDR,
359
+ // Use 0 gas price and dummy asset for simulation
360
+ gasPrice: 0n,
361
+ gasAsset: DUMMY_GAS_ASSET,
362
+ totalGasLimit: BLOCK_GAS_LIMIT,
363
+ };
364
+
365
+ // prepare the request into an operation using a dummy viewer
366
+ const preparedOp = await prepareOperation(
367
+ { viewer: DUMMY_VIEWER, ...deps },
368
+ dummyOpRequest
369
+ );
370
+ const usedNotes = getIncludedNotesFromOp(preparedOp);
371
+
372
+ // simulate the operation
373
+ if (!executionGasLimit) {
374
+ console.log("simulating operation");
375
+ const result = await simulateOperation(
376
+ handlerContract,
377
+ preparedOp as PreSignOperation
378
+ );
379
+ if (!result.opProcessed) {
380
+ throw Error("cannot estimate gas with Error: " + result.failureReason);
381
+ }
382
+
383
+ // set executionGasLimit with 20% buffer above the simulation result
384
+ executionGasLimit = (result.executionGas * 12n) / 10n;
385
+ }
386
+
387
+ preparedOp.executionGasLimit = executionGasLimit;
388
+ const totalGasLimit = maxGasForOperation(preparedOp);
389
+
390
+ const scale = 100;
391
+ const gasMultiplierScaled = BigInt(Math.floor(gasMultiplier * scale));
392
+ // if gasPrice is not specified, get it from RPC node
393
+ // NOTE: gasPrice returned in wei
394
+ gasPrice =
395
+ gasPrice ??
396
+ ((await handlerContract.provider.getGasPrice()).toBigInt() *
397
+ gasMultiplierScaled) /
398
+ BigInt(Math.floor(scale));
399
+
400
+ return {
401
+ totalGasLimit,
402
+ executionGasLimit,
403
+ gasPrice,
404
+ usedNotes,
405
+ };
406
+ }
407
+
408
+ async function simulateOperation(
409
+ handlerContract: Handler,
410
+ op: Operation
411
+ ): Promise<OperationResult> {
412
+ // We need to do staticCall, which fails if wallet is connected to a signer
413
+ // https://github.com/ethers-io/ethers.js/discussions/3327#discussioncomment-3539505
414
+ // Switching to a regular provider underlying the signer
415
+ if (handlerContract.signer) {
416
+ handlerContract = handlerContract.connect(handlerContract.provider);
417
+ }
418
+
419
+ // Fill-in the some fake proof
420
+ const opWithFakeProofs = fakeProvenOperation(op);
421
+
422
+ // Set gasPrice to 0 so that gas payment does not interfere with amount of
423
+ // assets unwrapped pre gas estimation
424
+ // ?: does this actually do anything if it's after `fakeProvenOperation` dummy provenOp?
425
+ op.gasPrice = 0n;
426
+
427
+ // Set dummy parameters which should not affect operation simulation
428
+ const verificationGasForOp = 0n;
429
+ const bundler = handlerContract.address;
430
+
431
+ const tellerAddress = await handlerContract._teller();
432
+
433
+ const result = await handlerContract.callStatic.handleOperation(
434
+ // TODO: fix after contract changes
435
+ //@ts-ignore
436
+ opWithFakeProofs,
437
+ verificationGasForOp,
438
+ bundler,
439
+ {
440
+ from: tellerAddress,
441
+ }
442
+ );
443
+ const {
444
+ opProcessed,
445
+ failureReason,
446
+ callSuccesses,
447
+ callResults,
448
+ verificationGas,
449
+ executionGas,
450
+ numRefunds,
451
+ } = result;
452
+
453
+ return {
454
+ opProcessed,
455
+ failureReason,
456
+ callSuccesses,
457
+ callResults,
458
+ verificationGas: verificationGas.toBigInt(),
459
+ executionGas: executionGas.toBigInt(),
460
+ numRefunds: numRefunds.toBigInt(),
461
+ };
462
+ }
463
+
464
+ function fakeProvenOperation(
465
+ op: Operation
466
+ ): SubmittableOperationWithNetworkInfo {
467
+ const provenJoinSplits = op.joinSplits.map((js) => {
468
+ return {
469
+ ...js,
470
+ proof: [0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n],
471
+ };
472
+ }) as ProvenJoinSplit[];
473
+
474
+ return OperationTrait.toSubmittable({
475
+ networkInfo: op.networkInfo,
476
+ joinSplits: provenJoinSplits,
477
+ refundAddr: op.refundAddr,
478
+ actions: op.actions,
479
+ refunds: op.refunds,
480
+ encodedGasAsset: op.encodedGasAsset,
481
+ gasAssetRefundThreshold: op.gasAssetRefundThreshold,
482
+ executionGasLimit: op.executionGasLimit,
483
+ gasPrice: op.gasPrice,
484
+ deadline: op.deadline,
485
+ atomicActions: op.atomicActions,
486
+ });
487
+ }