@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.
- package/CHANGELOG.md +17 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/package.json +58 -0
- package/dist/src/NocturneClient.d.ts +68 -0
- package/dist/src/NocturneClient.d.ts.map +1 -0
- package/dist/src/NocturneClient.js +264 -0
- package/dist/src/NocturneClient.js.map +1 -0
- package/dist/src/NocturneDB.d.ts +100 -0
- package/dist/src/NocturneDB.d.ts.map +1 -0
- package/dist/src/NocturneDB.js +525 -0
- package/dist/src/NocturneDB.js.map +1 -0
- package/dist/src/OpTracker.d.ts +13 -0
- package/dist/src/OpTracker.d.ts.map +1 -0
- package/dist/src/OpTracker.js +34 -0
- package/dist/src/OpTracker.js.map +1 -0
- package/dist/src/conversion/converter.d.ts +5 -0
- package/dist/src/conversion/converter.d.ts.map +1 -0
- package/dist/src/conversion/converter.js +15 -0
- package/dist/src/conversion/converter.js.map +1 -0
- package/dist/src/conversion/index.d.ts +3 -0
- package/dist/src/conversion/index.d.ts.map +1 -0
- package/dist/src/conversion/index.js +21 -0
- package/dist/src/conversion/index.js.map +1 -0
- package/dist/src/conversion/mock.d.ts +6 -0
- package/dist/src/conversion/mock.d.ts.map +1 -0
- package/dist/src/conversion/mock.js +14 -0
- package/dist/src/conversion/mock.js.map +1 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +39 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/opRequestGas.d.ts +20 -0
- package/dist/src/opRequestGas.d.ts.map +1 -0
- package/dist/src/opRequestGas.js +321 -0
- package/dist/src/opRequestGas.js.map +1 -0
- package/dist/src/operationRequest/builder.d.ts +40 -0
- package/dist/src/operationRequest/builder.d.ts.map +1 -0
- package/dist/src/operationRequest/builder.js +192 -0
- package/dist/src/operationRequest/builder.js.map +1 -0
- package/dist/src/operationRequest/index.d.ts +3 -0
- package/dist/src/operationRequest/index.d.ts.map +1 -0
- package/dist/src/operationRequest/index.js +6 -0
- package/dist/src/operationRequest/index.js.map +1 -0
- package/dist/src/operationRequest/operationRequest.d.ts +50 -0
- package/dist/src/operationRequest/operationRequest.d.ts.map +1 -0
- package/dist/src/operationRequest/operationRequest.js +16 -0
- package/dist/src/operationRequest/operationRequest.js.map +1 -0
- package/dist/src/prepareOperation.d.ts +21 -0
- package/dist/src/prepareOperation.d.ts.map +1 -0
- package/dist/src/prepareOperation.js +256 -0
- package/dist/src/prepareOperation.js.map +1 -0
- package/dist/src/proveOperation.d.ts +7 -0
- package/dist/src/proveOperation.d.ts.map +1 -0
- package/dist/src/proveOperation.js +79 -0
- package/dist/src/proveOperation.js.map +1 -0
- package/dist/src/signOperation.d.ts +3 -0
- package/dist/src/signOperation.d.ts.map +1 -0
- package/dist/src/signOperation.js +61 -0
- package/dist/src/signOperation.js.map +1 -0
- package/dist/src/snapJsonRpc.d.ts +55 -0
- package/dist/src/snapJsonRpc.d.ts.map +1 -0
- package/dist/src/snapJsonRpc.js +63 -0
- package/dist/src/snapJsonRpc.js.map +1 -0
- package/dist/src/syncSDK.d.ts +17 -0
- package/dist/src/syncSDK.d.ts.map +1 -0
- package/dist/src/syncSDK.js +188 -0
- package/dist/src/syncSDK.js.map +1 -0
- package/dist/src/types.d.ts +60 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +3 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/constants.d.ts +3 -0
- package/dist/src/utils/constants.d.ts.map +1 -0
- package/dist/src/utils/constants.js +20 -0
- package/dist/src/utils/constants.js.map +1 -0
- package/dist/src/utils/index.d.ts +3 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +19 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/misc.d.ts +13 -0
- package/dist/src/utils/misc.d.ts.map +1 -0
- package/dist/src/utils/misc.js +77 -0
- package/dist/src/utils/misc.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +58 -0
- package/src/NocturneClient.ts +415 -0
- package/src/NocturneDB.ts +761 -0
- package/src/OpTracker.ts +44 -0
- package/src/conversion/converter.ts +22 -0
- package/src/conversion/index.ts +2 -0
- package/src/conversion/mock.ts +11 -0
- package/src/index.ts +14 -0
- package/src/opRequestGas.ts +487 -0
- package/src/operationRequest/builder.ts +359 -0
- package/src/operationRequest/index.ts +16 -0
- package/src/operationRequest/operationRequest.ts +87 -0
- package/src/prepareOperation.ts +420 -0
- package/src/proveOperation.ts +124 -0
- package/src/signOperation.ts +116 -0
- package/src/snapJsonRpc.ts +109 -0
- package/src/syncSDK.ts +285 -0
- package/src/types.ts +83 -0
- package/src/utils/constants.ts +16 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/misc.ts +107 -0
package/src/OpTracker.ts
ADDED
|
@@ -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,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
|
+
}
|