@eulerxyz/euler-v2-sdk 1.0.3 → 1.0.4
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/dist/src/entities/Account.d.ts +14 -0
- package/dist/src/entities/Account.d.ts.map +1 -1
- package/dist/src/entities/Account.js +5 -3
- package/dist/src/entities/Account.js.map +1 -1
- package/dist/src/entities/Wallet.d.ts +7 -0
- package/dist/src/entities/Wallet.d.ts.map +1 -1
- package/dist/src/entities/Wallet.js +10 -1
- package/dist/src/entities/Wallet.js.map +1 -1
- package/dist/src/sdk/buildSDK.d.ts.map +1 -1
- package/dist/src/sdk/buildSDK.js +1 -0
- package/dist/src/sdk/buildSDK.js.map +1 -1
- package/dist/src/services/accountService/adapters/accountOnchainAdapter/accountInfoConverter.d.ts.map +1 -1
- package/dist/src/services/accountService/adapters/accountOnchainAdapter/accountInfoConverter.js +9 -0
- package/dist/src/services/accountService/adapters/accountOnchainAdapter/accountInfoConverter.js.map +1 -1
- package/dist/src/services/accountService/adapters/accountOnchainAdapter/accountLensTypes.d.ts +5 -0
- package/dist/src/services/accountService/adapters/accountOnchainAdapter/accountLensTypes.d.ts.map +1 -1
- package/dist/src/services/executionService/executionService.d.ts.map +1 -1
- package/dist/src/services/executionService/executionService.js +16 -2
- package/dist/src/services/executionService/executionService.js.map +1 -1
- package/dist/src/services/executionService/executionServiceTypes.d.ts +6 -0
- package/dist/src/services/executionService/executionServiceTypes.d.ts.map +1 -1
- package/dist/src/services/executionService/executionServiceTypes.js.map +1 -1
- package/dist/src/services/executionService/simulate.d.ts +27 -0
- package/dist/src/services/executionService/simulate.d.ts.map +1 -1
- package/dist/src/services/executionService/simulate.js +454 -36
- package/dist/src/services/executionService/simulate.js.map +1 -1
- package/dist/src/services/reulLockService/reulLockService.d.ts +2 -2
- package/dist/src/services/reulLockService/reulLockService.d.ts.map +1 -1
- package/dist/src/services/reulLockService/reulLockService.js +25 -7
- package/dist/src/services/reulLockService/reulLockService.js.map +1 -1
- package/dist/src/services/reulLockService/reulLockServiceTypes.d.ts +1 -1
- package/dist/src/services/reulLockService/reulLockServiceTypes.d.ts.map +1 -1
- package/dist/src/services/rewardsService/index.d.ts +1 -1
- package/dist/src/services/rewardsService/index.d.ts.map +1 -1
- package/dist/src/services/rewardsService/index.js.map +1 -1
- package/dist/src/services/rewardsService/rewardsService.d.ts +16 -1
- package/dist/src/services/rewardsService/rewardsService.d.ts.map +1 -1
- package/dist/src/services/rewardsService/rewardsService.js +163 -3
- package/dist/src/services/rewardsService/rewardsService.js.map +1 -1
- package/dist/src/services/rewardsService/rewardsServiceTypes.d.ts +18 -0
- package/dist/src/services/rewardsService/rewardsServiceTypes.d.ts.map +1 -1
- package/dist/src/services/rewardsService/rewardsServiceTypes.js.map +1 -1
- package/dist/src/utils/stateOverrides/approvalOverrides.d.ts +1 -1
- package/dist/src/utils/stateOverrides/approvalOverrides.d.ts.map +1 -1
- package/dist/src/utils/stateOverrides/approvalOverrides.js +3 -9
- package/dist/src/utils/stateOverrides/approvalOverrides.js.map +1 -1
- package/dist/src/utils/stateOverrides/balanceOverrides.d.ts +8 -6
- package/dist/src/utils/stateOverrides/balanceOverrides.d.ts.map +1 -1
- package/dist/src/utils/stateOverrides/balanceOverrides.js +62 -55
- package/dist/src/utils/stateOverrides/balanceOverrides.js.map +1 -1
- package/dist/src/utils/stateOverrides/getStateOverrides.d.ts.map +1 -1
- package/dist/src/utils/stateOverrides/getStateOverrides.js +9 -8
- package/dist/src/utils/stateOverrides/getStateOverrides.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,70 @@
|
|
|
1
|
-
import { decodeFunctionResult, getAddress, parseEther, } from "viem";
|
|
1
|
+
import { decodeFunctionData, decodeFunctionResult, encodeFunctionData, getAddress, parseEther, toFunctionSelector, zeroAddress, } from "viem";
|
|
2
2
|
import { estimateContractGas } from "viem/actions";
|
|
3
|
+
// Minimal ABI for resolving a vault's underlying asset and reading wallet ERC20
|
|
4
|
+
// balances inside the simulated batch (per-layer wallet-balance capture).
|
|
5
|
+
const walletBalanceAbi = [
|
|
6
|
+
{ type: "function", name: "asset", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
|
|
7
|
+
{ type: "function", name: "balanceOf", stateMutability: "view", inputs: [{ type: "address" }], outputs: [{ type: "uint256" }] },
|
|
8
|
+
{ type: "function", name: "underlying", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
|
|
9
|
+
];
|
|
10
|
+
const merklClaimAbi = [
|
|
11
|
+
{
|
|
12
|
+
type: "function",
|
|
13
|
+
name: "claim",
|
|
14
|
+
inputs: [
|
|
15
|
+
{ name: "users", type: "address[]" },
|
|
16
|
+
{ name: "tokens", type: "address[]" },
|
|
17
|
+
{ name: "amounts", type: "uint256[]" },
|
|
18
|
+
{ name: "proofs", type: "bytes32[][]" },
|
|
19
|
+
],
|
|
20
|
+
outputs: [],
|
|
21
|
+
stateMutability: "nonpayable",
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
const fuulClaimAbi = [
|
|
25
|
+
{
|
|
26
|
+
type: "function",
|
|
27
|
+
name: "claim",
|
|
28
|
+
inputs: [
|
|
29
|
+
{
|
|
30
|
+
name: "claimChecks",
|
|
31
|
+
type: "tuple[]",
|
|
32
|
+
components: [
|
|
33
|
+
{ name: "projectAddress", type: "address" },
|
|
34
|
+
{ name: "to", type: "address" },
|
|
35
|
+
{ name: "currency", type: "address" },
|
|
36
|
+
{ name: "currencyType", type: "uint8" },
|
|
37
|
+
{ name: "amount", type: "uint256" },
|
|
38
|
+
{ name: "reason", type: "uint8" },
|
|
39
|
+
{ name: "tokenId", type: "uint256" },
|
|
40
|
+
{ name: "deadline", type: "uint256" },
|
|
41
|
+
{ name: "proof", type: "bytes32" },
|
|
42
|
+
{ name: "signatures", type: "bytes[]" },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
outputs: [],
|
|
47
|
+
stateMutability: "payable",
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
const rewardStreamsClaimAbi = [
|
|
51
|
+
{
|
|
52
|
+
type: "function",
|
|
53
|
+
name: "claimReward",
|
|
54
|
+
inputs: [
|
|
55
|
+
{ name: "rewarded", type: "address" },
|
|
56
|
+
{ name: "reward", type: "address" },
|
|
57
|
+
{ name: "to", type: "address" },
|
|
58
|
+
{ name: "ignoreRecentReward", type: "bool" },
|
|
59
|
+
],
|
|
60
|
+
outputs: [{ name: "amount", type: "uint256" }],
|
|
61
|
+
stateMutability: "nonpayable",
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
const MERKL_CLAIM_SELECTOR = toFunctionSelector("function claim(address[],address[],uint256[],bytes32[][])");
|
|
65
|
+
const FUUL_CLAIM_SELECTOR = toFunctionSelector("function claim((address,address,address,uint8,uint256,uint8,uint256,uint256,bytes32,bytes[])[])");
|
|
66
|
+
const REWARD_STREAM_CLAIM_SELECTOR = toFunctionSelector("function claimReward(address,address,address,bool)");
|
|
67
|
+
const REUL_UNLOCK_SELECTOR = toFunctionSelector("function withdrawToByLockTimestamp(address,uint256,bool)");
|
|
3
68
|
import { Account, } from "../../entities/Account.js";
|
|
4
69
|
import { EulerEarn } from "../../entities/EulerEarn.js";
|
|
5
70
|
import { EVault } from "../../entities/EVault.js";
|
|
@@ -18,7 +83,7 @@ import { vaultLensAbi } from "../vaults/eVaultService/adapters/eVaultOnchainAdap
|
|
|
18
83
|
import { getVaultInfoFullLensBatchItem } from "../vaults/eVaultService/adapters/eVaultOnchainAdapter/eVaultOnchainAdapter.js";
|
|
19
84
|
import { convertVaultInfoFullToIEVault } from "../vaults/eVaultService/adapters/eVaultOnchainAdapter/vaultInfoConverter.js";
|
|
20
85
|
import { ethereumVaultConnectorAbi } from "./abis/ethereumVaultConnectorAbi.js";
|
|
21
|
-
import { assertNoCowSwapPlanItems, flattenBatchEntries, } from "./executionServiceTypes.js";
|
|
86
|
+
import { assertNoCowSwapPlanItems, flattenBatchEntries, isEVCBatchOperation, } from "./executionServiceTypes.js";
|
|
22
87
|
export async function deriveStateOverrides(ctx, chainId, account, transactionPlan, options) {
|
|
23
88
|
assertNoCowSwapPlanItems(transactionPlan, "deriveStateOverrides");
|
|
24
89
|
const owner = getAddress(account);
|
|
@@ -97,7 +162,11 @@ export async function simulateTransactionPlan(ctx, chainId, account, transaction
|
|
|
97
162
|
if (useStateOverrides) {
|
|
98
163
|
effectiveStateOverrides = await deriveStateOverrides(ctx, chainId, owner, transactionPlan, options?.stateOverrideOptions);
|
|
99
164
|
}
|
|
100
|
-
|
|
165
|
+
// Preserve operation boundaries (EVCBatchOperation entries emitted by the
|
|
166
|
+
// planners / mergePlans) so reverts can be attributed to the operation that
|
|
167
|
+
// caused them and so we can snapshot state after each operation.
|
|
168
|
+
const operations = collectOperations(transactionPlan);
|
|
169
|
+
const batch = operations.flatMap((op) => op.items);
|
|
101
170
|
if (batch.length === 0) {
|
|
102
171
|
return {
|
|
103
172
|
simulatedAccounts: [],
|
|
@@ -106,7 +175,32 @@ export async function simulateTransactionPlan(ctx, chainId, account, transaction
|
|
|
106
175
|
};
|
|
107
176
|
}
|
|
108
177
|
const diagnostics = await fetchSimulationDiagnostics(ctx, chainId, owner, transactionPlan);
|
|
109
|
-
const {
|
|
178
|
+
const { lensItems, lensMeta, evcAddress } = await buildSimulationBatch(ctx, chainId, owner, operations);
|
|
179
|
+
// Assemble the EVC batch, interleaving a lens-read block before the first
|
|
180
|
+
// operation (layer 0 = real state) and one after every operation (layer i =
|
|
181
|
+
// state after op i), so the full per-layer history is always captured.
|
|
182
|
+
const fullBatch = [];
|
|
183
|
+
const actionPositions = []; // fullBatch indices of real items, in `batch` order
|
|
184
|
+
const opOfPosition = []; // operation index for each action position
|
|
185
|
+
const layerSlices = [];
|
|
186
|
+
const pushLensBlock = () => {
|
|
187
|
+
layerSlices.push({ lensStart: fullBatch.length });
|
|
188
|
+
for (const it of lensItems)
|
|
189
|
+
fullBatch.push(it);
|
|
190
|
+
};
|
|
191
|
+
const pushOperation = (op, opIndex) => {
|
|
192
|
+
for (const it of op.items) {
|
|
193
|
+
actionPositions.push(fullBatch.length);
|
|
194
|
+
opOfPosition.push(opIndex);
|
|
195
|
+
fullBatch.push(it);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
pushLensBlock();
|
|
199
|
+
for (let i = 0; i < operations.length; i++) {
|
|
200
|
+
pushOperation(operations[i], i);
|
|
201
|
+
pushLensBlock();
|
|
202
|
+
}
|
|
203
|
+
const totalValue = fullBatch.reduce((sum, item) => sum + item.value, 0n);
|
|
110
204
|
const simulationResult = await runSimulation(ctx, chainId, account, evcAddress, fullBatch, totalValue, effectiveStateOverrides);
|
|
111
205
|
if ("simulationError" in simulationResult) {
|
|
112
206
|
return {
|
|
@@ -116,7 +210,7 @@ export async function simulateTransactionPlan(ctx, chainId, account, transaction
|
|
|
116
210
|
};
|
|
117
211
|
}
|
|
118
212
|
const { batchResults, accountStatusErrors, vaultStatusErrors } = simulationResult;
|
|
119
|
-
const rawBatchResults =
|
|
213
|
+
const rawBatchResults = actionPositions.map((pos) => batchResults[pos]);
|
|
120
214
|
let describedBatch;
|
|
121
215
|
try {
|
|
122
216
|
describedBatch = ctx.describeBatch(batch);
|
|
@@ -131,27 +225,149 @@ export async function simulateTransactionPlan(ctx, chainId, account, transaction
|
|
|
131
225
|
args: {},
|
|
132
226
|
});
|
|
133
227
|
const failedBatchItems = (await Promise.all(rawBatchResults.map(async (itemResult, index) => {
|
|
134
|
-
if (itemResult.success)
|
|
228
|
+
if (!itemResult || itemResult.success)
|
|
135
229
|
return null;
|
|
136
230
|
const decodedError = await decodeSmartContractErrors(itemResult.result);
|
|
137
231
|
const decodedItem = describedBatch && describedBatch.length === batch.length
|
|
138
232
|
? describedBatch[index]
|
|
139
233
|
: fallbackDescription(batch[index]);
|
|
234
|
+
const operationIndex = opOfPosition[index];
|
|
140
235
|
return {
|
|
141
236
|
index,
|
|
237
|
+
operationIndex,
|
|
238
|
+
operationName: operations[operationIndex]?.name,
|
|
142
239
|
item: decodedItem,
|
|
143
240
|
error: itemResult.result,
|
|
144
241
|
decodedError,
|
|
145
242
|
};
|
|
146
243
|
}))).filter((item) => item !== null);
|
|
244
|
+
// One account/vault/wallet snapshot per layer: [pre-batch, after op0, after op1, …].
|
|
245
|
+
const snapshots = [];
|
|
246
|
+
for (const slice of layerSlices) {
|
|
247
|
+
snapshots.push(await decodeAccountSnapshot(ctx, chainId, owner, lensMeta, (i) => batchResults[slice.lensStart + i], options));
|
|
248
|
+
}
|
|
249
|
+
const simulatedAccounts = snapshots.map((s) => s.account);
|
|
250
|
+
const simulatedVaultsLayers = snapshots.map((s) => s.vaults);
|
|
251
|
+
const simulatedVaults = simulatedVaultsLayers[simulatedVaultsLayers.length - 1] ?? [];
|
|
252
|
+
const simulatedWalletBalances = snapshots.map((s) => s.walletBalances);
|
|
253
|
+
// Accurate wallet shortfall from the per-layer balances (running-min over the
|
|
254
|
+
// real-anchored balance), which nets out intra-batch funding. Overrides the
|
|
255
|
+
// static approval-based estimate from fetchSimulationDiagnostics.
|
|
256
|
+
const insufficientWalletAssets = await computeWalletShortfall(ctx, chainId, owner, simulatedWalletBalances);
|
|
257
|
+
const canExecute = failedBatchItems.length === 0 &&
|
|
258
|
+
accountStatusErrors.length === 0 &&
|
|
259
|
+
vaultStatusErrors.length === 0 &&
|
|
260
|
+
!insufficientWalletAssets?.length &&
|
|
261
|
+
!diagnostics.insufficientPermit2Allowances?.length &&
|
|
262
|
+
!diagnostics.insufficientDirectAllowances?.length;
|
|
263
|
+
return {
|
|
264
|
+
simulatedAccounts,
|
|
265
|
+
simulatedVaults,
|
|
266
|
+
simulatedVaultsLayers,
|
|
267
|
+
simulatedWalletBalances,
|
|
268
|
+
canExecute,
|
|
269
|
+
rawBatchResults,
|
|
270
|
+
failedBatchItems: failedBatchItems.length > 0 ? failedBatchItems : undefined,
|
|
271
|
+
accountStatusErrors: accountStatusErrors.length > 0 ? accountStatusErrors : undefined,
|
|
272
|
+
vaultStatusErrors: vaultStatusErrors.length > 0 ? vaultStatusErrors : undefined,
|
|
273
|
+
...diagnostics,
|
|
274
|
+
insufficientWalletAssets,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function collectOperations(transactionPlan) {
|
|
278
|
+
const operations = [];
|
|
279
|
+
for (const item of transactionPlan) {
|
|
280
|
+
if (item.type !== "evcBatch")
|
|
281
|
+
continue;
|
|
282
|
+
for (const entry of item.items) {
|
|
283
|
+
if (isEVCBatchOperation(entry)) {
|
|
284
|
+
operations.push({
|
|
285
|
+
name: entry.name,
|
|
286
|
+
items: entry.items,
|
|
287
|
+
walletBalanceTokens: entry.walletBalanceTokens,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
operations.push({ name: undefined, items: [entry] });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return operations;
|
|
296
|
+
}
|
|
297
|
+
const addWalletToken = (tokens, token) => {
|
|
298
|
+
if (!token)
|
|
299
|
+
return;
|
|
300
|
+
try {
|
|
301
|
+
const checksum = getAddress(token);
|
|
302
|
+
if (checksum !== zeroAddress)
|
|
303
|
+
tokens.add(checksum);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// Ignore malformed optional metadata / decoded calldata.
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const getSelector = (data) => data.slice(0, 10);
|
|
310
|
+
function collectClaimWalletBalanceTokens(batch) {
|
|
311
|
+
const tokens = new Set();
|
|
312
|
+
for (const item of batch) {
|
|
313
|
+
const selector = getSelector(item.data);
|
|
314
|
+
try {
|
|
315
|
+
if (selector === MERKL_CLAIM_SELECTOR) {
|
|
316
|
+
const decoded = decodeFunctionData({
|
|
317
|
+
abi: merklClaimAbi,
|
|
318
|
+
data: item.data,
|
|
319
|
+
});
|
|
320
|
+
const rewardTokens = decoded.args[1];
|
|
321
|
+
for (const token of rewardTokens)
|
|
322
|
+
addWalletToken(tokens, token);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (selector === FUUL_CLAIM_SELECTOR) {
|
|
326
|
+
const decoded = decodeFunctionData({
|
|
327
|
+
abi: fuulClaimAbi,
|
|
328
|
+
data: item.data,
|
|
329
|
+
});
|
|
330
|
+
const claimChecks = decoded.args[0];
|
|
331
|
+
for (const check of claimChecks) {
|
|
332
|
+
addWalletToken(tokens, check.currency);
|
|
333
|
+
}
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (selector === REWARD_STREAM_CLAIM_SELECTOR) {
|
|
337
|
+
const decoded = decodeFunctionData({
|
|
338
|
+
abi: rewardStreamsClaimAbi,
|
|
339
|
+
data: item.data,
|
|
340
|
+
});
|
|
341
|
+
addWalletToken(tokens, decoded.args[1]);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// Unknown or malformed claim-like calldata should not block simulation.
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return [...tokens];
|
|
349
|
+
}
|
|
350
|
+
// Decode the lens-read block for a single layer into a populated Account plus
|
|
351
|
+
// the EVault/EulerEarn entities observed at that point in the batch.
|
|
352
|
+
async function decodeAccountSnapshot(ctx, chainId, owner, lensMeta, resultAt, options) {
|
|
147
353
|
const vaultsByAddress = new Map();
|
|
148
354
|
const evcInfos = new Map();
|
|
149
355
|
const vaultInfosBySub = new Map();
|
|
356
|
+
const walletBalances = {};
|
|
150
357
|
for (let i = 0; i < lensMeta.length; i++) {
|
|
151
358
|
const meta = lensMeta[i];
|
|
152
|
-
const resultItem =
|
|
359
|
+
const resultItem = resultAt(i);
|
|
153
360
|
if (!resultItem?.success)
|
|
154
361
|
continue;
|
|
362
|
+
if (meta.kind === "walletBalance") {
|
|
363
|
+
const bal = decodeFunctionResult({
|
|
364
|
+
abi: walletBalanceAbi,
|
|
365
|
+
functionName: "balanceOf",
|
|
366
|
+
data: resultItem.result,
|
|
367
|
+
});
|
|
368
|
+
walletBalances[getAddress(meta.token).toLowerCase()] = bal;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
155
371
|
if (meta.kind === "eVault") {
|
|
156
372
|
const decodedVault = decodeFunctionResult({
|
|
157
373
|
abi: vaultLensAbi,
|
|
@@ -240,22 +456,7 @@ export async function simulateTransactionPlan(ctx, chainId, account, transaction
|
|
|
240
456
|
if (accountFetchOptions?.populateUserRewards && ctx.rewardsService) {
|
|
241
457
|
await populatedAccount.populateUserRewards(ctx.rewardsService);
|
|
242
458
|
}
|
|
243
|
-
|
|
244
|
-
simulatedAccounts: [populatedAccount],
|
|
245
|
-
simulatedVaults,
|
|
246
|
-
canExecute: failedBatchItems.length === 0 &&
|
|
247
|
-
accountStatusErrors.length === 0 &&
|
|
248
|
-
vaultStatusErrors.length === 0 &&
|
|
249
|
-
!diagnostics.insufficientWalletAssets?.length &&
|
|
250
|
-
!diagnostics.insufficientPermit2Allowances?.length &&
|
|
251
|
-
!diagnostics.insufficientDirectAllowances?.length,
|
|
252
|
-
rawBatchResults,
|
|
253
|
-
failedBatchItems: failedBatchItems.length > 0 ? failedBatchItems : undefined,
|
|
254
|
-
accountStatusErrors: accountStatusErrors.length > 0 ? accountStatusErrors : undefined,
|
|
255
|
-
vaultStatusErrors: vaultStatusErrors.length > 0 ? vaultStatusErrors : undefined,
|
|
256
|
-
...diagnostics,
|
|
257
|
-
};
|
|
258
|
-
return result;
|
|
459
|
+
return { account: populatedAccount, vaults: simulatedVaults, walletBalances };
|
|
259
460
|
}
|
|
260
461
|
export async function estimateGasForTransactionPlan(ctx, chainId, account, transactionPlan, options) {
|
|
261
462
|
assertNoCowSwapPlanItems(transactionPlan, "estimateGasForTransactionPlan");
|
|
@@ -312,8 +513,47 @@ function getAccountAdapter(ctx, methodName) {
|
|
|
312
513
|
const emptyPositionsAdapter = { fetchAccountVaults: async () => ({}) };
|
|
313
514
|
return new AccountOnchainAdapter(ctx.providerService, ctx.deploymentService, emptyPositionsAdapter);
|
|
314
515
|
}
|
|
315
|
-
async function buildSimulationBatch(ctx, chainId, owner,
|
|
516
|
+
async function buildSimulationBatch(ctx, chainId, owner, operations) {
|
|
517
|
+
const batch = operations.flatMap((op) => op.items);
|
|
316
518
|
const { candidateVaults, subAccountVaults } = collectCandidateVaults(ctx, owner, batch);
|
|
519
|
+
// Each touched sub-account's account-level liquidity (health factor, current
|
|
520
|
+
// LTV, collateral/liability values) rides on its *controller* vault's
|
|
521
|
+
// getVaultAccountInfo. An op that only touches collateral vaults (e.g. a
|
|
522
|
+
// collateral withdraw or supply) wouldn't otherwise read the controller, so
|
|
523
|
+
// the simulated liquidity would stay at the pre-batch (stale) state. Pull each
|
|
524
|
+
// touched sub-account's enabled controllers and read them too, so liquidity is
|
|
525
|
+
// recomputed post-op for every operation, not just borrows/repays.
|
|
526
|
+
const ctrlProvider = ctx.providerService?.getProvider(chainId);
|
|
527
|
+
const touchedSubs = Array.from(subAccountVaults.keys());
|
|
528
|
+
if (ctrlProvider && touchedSubs.length) {
|
|
529
|
+
const evc = ctx.deploymentService.getDeployment(chainId).addresses.coreAddrs
|
|
530
|
+
.evc;
|
|
531
|
+
try {
|
|
532
|
+
const controllerResults = await ctrlProvider.multicall({
|
|
533
|
+
allowFailure: true,
|
|
534
|
+
contracts: touchedSubs.map((sub) => ({
|
|
535
|
+
address: evc,
|
|
536
|
+
abi: ethereumVaultConnectorAbi,
|
|
537
|
+
functionName: "getControllers",
|
|
538
|
+
args: [sub],
|
|
539
|
+
})),
|
|
540
|
+
});
|
|
541
|
+
touchedSubs.forEach((sub, i) => {
|
|
542
|
+
const r = controllerResults[i];
|
|
543
|
+
if (r?.status !== "success")
|
|
544
|
+
return;
|
|
545
|
+
for (const controller of r.result ?? []) {
|
|
546
|
+
const c = getAddress(controller);
|
|
547
|
+
subAccountVaults.get(sub)?.add(c);
|
|
548
|
+
candidateVaults.add(c);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
// Best-effort: if controllers can't be fetched, liquidity may stay stale
|
|
554
|
+
// for collateral-only ops, but the simulation itself is unaffected.
|
|
555
|
+
}
|
|
556
|
+
}
|
|
317
557
|
const vaultCandidatesList = Array.from(candidateVaults);
|
|
318
558
|
if (!ctx.vaultMetaService) {
|
|
319
559
|
throw new Error("ExecutionService.simulateTransactionPlan requires a vaultMetaService. Pass it to the ExecutionService constructor or call setVaultMetaService().");
|
|
@@ -373,9 +613,71 @@ async function buildSimulationBatch(ctx, chainId, owner, batch) {
|
|
|
373
613
|
});
|
|
374
614
|
}
|
|
375
615
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
616
|
+
// Wallet-balance reads: resolve the underlying asset of each touched vault,
|
|
617
|
+
// plus reward tokens from claim/unlock operations, and read balanceOf(owner)
|
|
618
|
+
// for each token. Interleaved per layer like the other lens reads, so wallet
|
|
619
|
+
// balances are captured after every operation. Balances are forged by state
|
|
620
|
+
// overrides, so consumers stitch using the delta vs the pre-batch layer.
|
|
621
|
+
const provider = ctx.providerService?.getProvider(chainId);
|
|
622
|
+
if (provider) {
|
|
623
|
+
const vaultsForAssets = [...eVaults, ...eulerEarnVaults];
|
|
624
|
+
const assets = await Promise.all(vaultsForAssets.map((vault) => provider
|
|
625
|
+
.readContract({
|
|
626
|
+
address: vault,
|
|
627
|
+
abi: walletBalanceAbi,
|
|
628
|
+
functionName: "asset",
|
|
629
|
+
})
|
|
630
|
+
.then((a) => getAddress(a))
|
|
631
|
+
.catch(() => null)));
|
|
632
|
+
const assetTokens = new Set();
|
|
633
|
+
for (const a of assets)
|
|
634
|
+
if (a)
|
|
635
|
+
assetTokens.add(getAddress(a));
|
|
636
|
+
for (const operation of operations) {
|
|
637
|
+
for (const token of operation.walletBalanceTokens ?? []) {
|
|
638
|
+
addWalletToken(assetTokens, token);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
for (const token of collectClaimWalletBalanceTokens(batch)) {
|
|
642
|
+
addWalletToken(assetTokens, token);
|
|
643
|
+
}
|
|
644
|
+
const unlockItems = batch.filter((item) => getSelector(item.data) === REUL_UNLOCK_SELECTOR);
|
|
645
|
+
if (unlockItems.length > 0) {
|
|
646
|
+
const configuredEul = deployment.addresses.tokenAddrs?.EUL;
|
|
647
|
+
if (configuredEul) {
|
|
648
|
+
addWalletToken(assetTokens, configuredEul);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
const underlyingTokens = await Promise.all(unlockItems.map((item) => provider
|
|
652
|
+
.readContract({
|
|
653
|
+
address: item.targetContract,
|
|
654
|
+
abi: walletBalanceAbi,
|
|
655
|
+
functionName: "underlying",
|
|
656
|
+
})
|
|
657
|
+
.then((a) => getAddress(a))
|
|
658
|
+
.catch(() => null)));
|
|
659
|
+
for (const token of underlyingTokens) {
|
|
660
|
+
if (token)
|
|
661
|
+
addWalletToken(assetTokens, token);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
for (const token of assetTokens) {
|
|
666
|
+
pushLensItem({
|
|
667
|
+
targetContract: token,
|
|
668
|
+
onBehalfOfAccount: owner,
|
|
669
|
+
value: 0n,
|
|
670
|
+
data: encodeFunctionData({
|
|
671
|
+
abi: walletBalanceAbi,
|
|
672
|
+
functionName: "balanceOf",
|
|
673
|
+
args: [owner],
|
|
674
|
+
}),
|
|
675
|
+
}, { kind: "walletBalance", token });
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// The caller assembles the final batch (interleaving these lens reads per the
|
|
679
|
+
// chosen layers mode), so we just return the reusable lens block.
|
|
680
|
+
return { lensItems, lensMeta, evcAddress };
|
|
379
681
|
}
|
|
380
682
|
function collectCandidateVaults(ctx, owner, batch) {
|
|
381
683
|
const candidateVaults = new Set();
|
|
@@ -399,9 +701,20 @@ function collectCandidateVaults(ctx, owner, batch) {
|
|
|
399
701
|
for (const item of described) {
|
|
400
702
|
const fn = item.functionName.toLowerCase();
|
|
401
703
|
const target = getAddress(item.targetContract);
|
|
402
|
-
if (fn === "
|
|
704
|
+
if (fn === "enablecollateral" ||
|
|
705
|
+
fn === "disablecollateral" ||
|
|
706
|
+
fn === "enablecontroller") {
|
|
707
|
+
const account = item.args.account;
|
|
708
|
+
const vault = item.args.vault;
|
|
709
|
+
if (vault)
|
|
710
|
+
addCandidateVault(vault);
|
|
711
|
+
if (account && vault)
|
|
712
|
+
addSubAccountVault(account, vault);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
if (fn === "transfer" || fn === "transferfrom" || fn === "transferfrommax") {
|
|
403
716
|
const to = item.args.to;
|
|
404
|
-
const from = fn === "transferfrom"
|
|
717
|
+
const from = fn === "transferfrom" || fn === "transferfrommax"
|
|
405
718
|
? item.args.from
|
|
406
719
|
: (item.args.from ?? item.onBehalfOfAccount);
|
|
407
720
|
if (from)
|
|
@@ -411,12 +724,68 @@ function collectCandidateVaults(ctx, owner, batch) {
|
|
|
411
724
|
addCandidateVault(target);
|
|
412
725
|
continue;
|
|
413
726
|
}
|
|
727
|
+
if (fn === "withdraw" || fn === "redeem") {
|
|
728
|
+
const owner = item.args.owner;
|
|
729
|
+
if (owner)
|
|
730
|
+
addSubAccountVault(owner, target);
|
|
731
|
+
addCandidateVault(target);
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (fn === "liquidate") {
|
|
735
|
+
const violator = item.args.violator;
|
|
736
|
+
const collateral = item.args.collateral;
|
|
737
|
+
if (violator)
|
|
738
|
+
addSubAccountVault(violator, target);
|
|
739
|
+
if (violator && collateral)
|
|
740
|
+
addSubAccountVault(violator, collateral);
|
|
741
|
+
if (collateral) {
|
|
742
|
+
addSubAccountVault(item.onBehalfOfAccount, collateral);
|
|
743
|
+
addCandidateVault(collateral);
|
|
744
|
+
}
|
|
745
|
+
addCandidateVault(target);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (fn === "pulldebt") {
|
|
749
|
+
const from = item.args.from;
|
|
750
|
+
if (from)
|
|
751
|
+
addSubAccountVault(from, target);
|
|
752
|
+
addCandidateVault(target);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
414
755
|
if (fn === "deposit" || fn === "skim" || fn === "mint") {
|
|
415
756
|
const receiver = item.args.receiver;
|
|
416
757
|
if (receiver)
|
|
417
758
|
addSubAccountVault(receiver, target);
|
|
418
759
|
addCandidateVault(target);
|
|
419
760
|
}
|
|
761
|
+
// repay(amount, receiver) / repayWithShares(amount, receiver): the *receiver*
|
|
762
|
+
// is the account whose debt is reduced, and it can differ from the batch
|
|
763
|
+
// item's onBehalfOfAccount (e.g. a repay-from-wallet runs on behalf of the
|
|
764
|
+
// owner but repays a sub-account's debt). Map the receiver so its post-repay
|
|
765
|
+
// debt position (and liquidity) is read — otherwise the simulated debt/health
|
|
766
|
+
// of the repaid sub-account stays at the pre-batch state.
|
|
767
|
+
if (fn === "repay" || fn === "repaywithshares") {
|
|
768
|
+
const receiver = item.args.receiver;
|
|
769
|
+
if (receiver)
|
|
770
|
+
addSubAccountVault(receiver, target);
|
|
771
|
+
addCandidateVault(target);
|
|
772
|
+
}
|
|
773
|
+
if (fn === "verifyamountminanddeposit" || fn === "verifyamountminandskim") {
|
|
774
|
+
const vault = item.args.vault;
|
|
775
|
+
const receiver = item.args.receiver;
|
|
776
|
+
if (vault)
|
|
777
|
+
addCandidateVault(vault);
|
|
778
|
+
if (vault && receiver)
|
|
779
|
+
addSubAccountVault(receiver, vault);
|
|
780
|
+
}
|
|
781
|
+
if (fn === "verifydebtmax") {
|
|
782
|
+
const vault = item.args.vault;
|
|
783
|
+
const account = item.args.account;
|
|
784
|
+
if (vault)
|
|
785
|
+
addCandidateVault(vault);
|
|
786
|
+
if (vault && account)
|
|
787
|
+
addSubAccountVault(account, vault);
|
|
788
|
+
}
|
|
420
789
|
}
|
|
421
790
|
if (!subAccountVaults.has(owner)) {
|
|
422
791
|
subAccountVaults.set(owner, new Set());
|
|
@@ -476,6 +845,50 @@ async function runSimulation(ctx, chainId, account, evcAddress, fullBatch, total
|
|
|
476
845
|
})));
|
|
477
846
|
return { batchResults, accountStatusErrors, vaultStatusErrors };
|
|
478
847
|
}
|
|
848
|
+
/**
|
|
849
|
+
* Wallet shortfall derived from the per-layer simulated balances. In-sim
|
|
850
|
+
* balances are forged by state overrides, so we anchor to the real on-chain
|
|
851
|
+
* balance and track the running balance across layers — real balance plus each
|
|
852
|
+
* layer's net delta vs the pre-batch layer. The shortfall per token is the worst
|
|
853
|
+
* dip below zero. This nets out intra-batch funding (e.g. withdraw then deposit
|
|
854
|
+
* the same asset → never dips) yet still flags a step that consumes more than is
|
|
855
|
+
* genuinely available at that point. Returns undefined when balances can't be
|
|
856
|
+
* resolved (no walletService / no touched tokens), so callers don't over-block.
|
|
857
|
+
*/
|
|
858
|
+
async function computeWalletShortfall(ctx, chainId, owner, simulatedWalletBalances) {
|
|
859
|
+
if (!ctx.walletService || simulatedWalletBalances.length === 0)
|
|
860
|
+
return undefined;
|
|
861
|
+
const base = simulatedWalletBalances[0] ?? {};
|
|
862
|
+
const tokens = Object.keys(base);
|
|
863
|
+
if (tokens.length === 0)
|
|
864
|
+
return undefined;
|
|
865
|
+
let realByToken;
|
|
866
|
+
try {
|
|
867
|
+
const wallet = (await ctx.walletService.fetchWallet(chainId, owner, tokens.map((t) => ({ asset: getAddress(t), spenders: [] })))).result;
|
|
868
|
+
realByToken = {};
|
|
869
|
+
for (const t of tokens)
|
|
870
|
+
realByToken[t] = wallet.getBalance(getAddress(t));
|
|
871
|
+
}
|
|
872
|
+
catch {
|
|
873
|
+
return undefined;
|
|
874
|
+
}
|
|
875
|
+
const shortfalls = [];
|
|
876
|
+
for (const t of tokens) {
|
|
877
|
+
const baseBal = base[t] ?? 0n;
|
|
878
|
+
const real = realByToken[t] ?? 0n;
|
|
879
|
+
// Worst running balance across layers (layer 0's delta is 0 ⇒ starts at real).
|
|
880
|
+
let minAvailable = real;
|
|
881
|
+
for (let i = 1; i < simulatedWalletBalances.length; i++) {
|
|
882
|
+
const delta = (simulatedWalletBalances[i][t] ?? 0n) - baseBal;
|
|
883
|
+
const available = real + delta;
|
|
884
|
+
if (available < minAvailable)
|
|
885
|
+
minAvailable = available;
|
|
886
|
+
}
|
|
887
|
+
if (minAvailable < 0n)
|
|
888
|
+
shortfalls.push({ token: getAddress(t), amount: -minAvailable });
|
|
889
|
+
}
|
|
890
|
+
return shortfalls.length > 0 ? shortfalls : undefined;
|
|
891
|
+
}
|
|
479
892
|
async function fetchSimulationDiagnostics(ctx, chainId, account, transactionPlan) {
|
|
480
893
|
if (!ctx.walletService || !transactionPlan)
|
|
481
894
|
return {};
|
|
@@ -549,20 +962,25 @@ async function fetchSimulationDiagnostics(ctx, chainId, account, transactionPlan
|
|
|
549
962
|
: {}),
|
|
550
963
|
};
|
|
551
964
|
}
|
|
552
|
-
|
|
553
|
-
|
|
965
|
+
// Sum the required amount per token across every approval the owner must fund.
|
|
966
|
+
// Each requiredApproval is a wallet outflow, and several can draw on the same
|
|
967
|
+
// token within one batch (e.g. supplying it into two vaults, or supply + repay),
|
|
968
|
+
// so the wallet must cover their *total*, not the largest single one. Forging
|
|
969
|
+
// the sum lets an underfunded batch simulate through to completion — so the
|
|
970
|
+
// running-balance shortfall reports the true peak deficit instead of the batch
|
|
971
|
+
// reverting partway with E_InsufficientBalance. Over-forging is harmless: the
|
|
972
|
+
// shortfall is computed against the real balance, not this forged amount.
|
|
973
|
+
export function extractBalanceRequirements(transactionPlan, account) {
|
|
974
|
+
const totalPerToken = new Map();
|
|
554
975
|
for (const item of transactionPlan) {
|
|
555
976
|
if (item.type !== "requiredApproval")
|
|
556
977
|
continue;
|
|
557
978
|
if (getAddress(item.owner) !== getAddress(account))
|
|
558
979
|
continue;
|
|
559
980
|
const token = getAddress(item.token);
|
|
560
|
-
|
|
561
|
-
if (item.amount > current) {
|
|
562
|
-
maxPerToken.set(token, item.amount);
|
|
563
|
-
}
|
|
981
|
+
totalPerToken.set(token, (totalPerToken.get(token) ?? 0n) + item.amount);
|
|
564
982
|
}
|
|
565
|
-
return Array.from(
|
|
983
|
+
return Array.from(totalPerToken.entries());
|
|
566
984
|
}
|
|
567
985
|
function extractApprovalRequirements(transactionPlan, account) {
|
|
568
986
|
const seen = new Set();
|