@eulerxyz/euler-v2-sdk 1.0.3 → 1.0.5

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 (58) hide show
  1. package/dist/src/entities/Account.d.ts +14 -0
  2. package/dist/src/entities/Account.d.ts.map +1 -1
  3. package/dist/src/entities/Account.js +5 -3
  4. package/dist/src/entities/Account.js.map +1 -1
  5. package/dist/src/entities/Wallet.d.ts +7 -0
  6. package/dist/src/entities/Wallet.d.ts.map +1 -1
  7. package/dist/src/entities/Wallet.js +10 -1
  8. package/dist/src/entities/Wallet.js.map +1 -1
  9. package/dist/src/sdk/buildSDK.d.ts.map +1 -1
  10. package/dist/src/sdk/buildSDK.js +1 -0
  11. package/dist/src/sdk/buildSDK.js.map +1 -1
  12. package/dist/src/services/accountService/adapters/accountOnchainAdapter/accountInfoConverter.d.ts.map +1 -1
  13. package/dist/src/services/accountService/adapters/accountOnchainAdapter/accountInfoConverter.js +9 -0
  14. package/dist/src/services/accountService/adapters/accountOnchainAdapter/accountInfoConverter.js.map +1 -1
  15. package/dist/src/services/accountService/adapters/accountOnchainAdapter/accountLensTypes.d.ts +5 -0
  16. package/dist/src/services/accountService/adapters/accountOnchainAdapter/accountLensTypes.d.ts.map +1 -1
  17. package/dist/src/services/executionService/executionService.d.ts.map +1 -1
  18. package/dist/src/services/executionService/executionService.js +16 -2
  19. package/dist/src/services/executionService/executionService.js.map +1 -1
  20. package/dist/src/services/executionService/executionServiceTypes.d.ts +6 -0
  21. package/dist/src/services/executionService/executionServiceTypes.d.ts.map +1 -1
  22. package/dist/src/services/executionService/executionServiceTypes.js.map +1 -1
  23. package/dist/src/services/executionService/simulate.d.ts +27 -0
  24. package/dist/src/services/executionService/simulate.d.ts.map +1 -1
  25. package/dist/src/services/executionService/simulate.js +523 -38
  26. package/dist/src/services/executionService/simulate.js.map +1 -1
  27. package/dist/src/services/reulLockService/reulLockService.d.ts +2 -2
  28. package/dist/src/services/reulLockService/reulLockService.d.ts.map +1 -1
  29. package/dist/src/services/reulLockService/reulLockService.js +25 -7
  30. package/dist/src/services/reulLockService/reulLockService.js.map +1 -1
  31. package/dist/src/services/reulLockService/reulLockServiceTypes.d.ts +1 -1
  32. package/dist/src/services/reulLockService/reulLockServiceTypes.d.ts.map +1 -1
  33. package/dist/src/services/rewardsService/index.d.ts +1 -1
  34. package/dist/src/services/rewardsService/index.d.ts.map +1 -1
  35. package/dist/src/services/rewardsService/index.js.map +1 -1
  36. package/dist/src/services/rewardsService/rewardsService.d.ts +16 -1
  37. package/dist/src/services/rewardsService/rewardsService.d.ts.map +1 -1
  38. package/dist/src/services/rewardsService/rewardsService.js +163 -3
  39. package/dist/src/services/rewardsService/rewardsService.js.map +1 -1
  40. package/dist/src/services/rewardsService/rewardsServiceTypes.d.ts +18 -0
  41. package/dist/src/services/rewardsService/rewardsServiceTypes.d.ts.map +1 -1
  42. package/dist/src/services/rewardsService/rewardsServiceTypes.js.map +1 -1
  43. package/dist/src/services/vaults/securitizeVaultService/adapters/securitizeVaultOnchainAdapter.d.ts +3 -1
  44. package/dist/src/services/vaults/securitizeVaultService/adapters/securitizeVaultOnchainAdapter.d.ts.map +1 -1
  45. package/dist/src/services/vaults/securitizeVaultService/adapters/securitizeVaultOnchainAdapter.js +20 -2
  46. package/dist/src/services/vaults/securitizeVaultService/adapters/securitizeVaultOnchainAdapter.js.map +1 -1
  47. package/dist/src/utils/stateOverrides/approvalOverrides.d.ts +1 -1
  48. package/dist/src/utils/stateOverrides/approvalOverrides.d.ts.map +1 -1
  49. package/dist/src/utils/stateOverrides/approvalOverrides.js +40 -36
  50. package/dist/src/utils/stateOverrides/approvalOverrides.js.map +1 -1
  51. package/dist/src/utils/stateOverrides/balanceOverrides.d.ts +8 -6
  52. package/dist/src/utils/stateOverrides/balanceOverrides.d.ts.map +1 -1
  53. package/dist/src/utils/stateOverrides/balanceOverrides.js +62 -55
  54. package/dist/src/utils/stateOverrides/balanceOverrides.js.map +1 -1
  55. package/dist/src/utils/stateOverrides/getStateOverrides.d.ts.map +1 -1
  56. package/dist/src/utils/stateOverrides/getStateOverrides.js +9 -8
  57. package/dist/src/utils/stateOverrides/getStateOverrides.js.map +1 -1
  58. 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";
@@ -17,8 +82,13 @@ import { getEulerEarnVaultInfoFullLensBatchItem } from "../vaults/eulerEarnServi
17
82
  import { vaultLensAbi } from "../vaults/eVaultService/adapters/eVaultOnchainAdapter/abis/vaultLensAbi.js";
18
83
  import { getVaultInfoFullLensBatchItem } from "../vaults/eVaultService/adapters/eVaultOnchainAdapter/eVaultOnchainAdapter.js";
19
84
  import { convertVaultInfoFullToIEVault } from "../vaults/eVaultService/adapters/eVaultOnchainAdapter/vaultInfoConverter.js";
85
+ import { SecuritizeCollateralVault } from "../../entities/SecuritizeCollateralVault.js";
86
+ import { utilsLensAbi } from "../vaults/securitizeVaultService/adapters/abis/utilsLensAbi.js";
87
+ import { erc4626EvcCollateralSecuritizeAbi } from "../vaults/securitizeVaultService/adapters/abis/erc4626EvcCollateralSecuritizeAbi.js";
88
+ import { convertToISecuritizeCollateralVault } from "../vaults/securitizeVaultService/adapters/securitizeVaultInfoConverter.js";
89
+ import { getVaultInfoERC4626LensBatchItem, getSecuritizeGovernorAdminBatchItem, getSecuritizeSupplyCapResolvedBatchItem, } from "../vaults/securitizeVaultService/adapters/securitizeVaultOnchainAdapter.js";
20
90
  import { ethereumVaultConnectorAbi } from "./abis/ethereumVaultConnectorAbi.js";
21
- import { assertNoCowSwapPlanItems, flattenBatchEntries, } from "./executionServiceTypes.js";
91
+ import { assertNoCowSwapPlanItems, flattenBatchEntries, isEVCBatchOperation, } from "./executionServiceTypes.js";
22
92
  export async function deriveStateOverrides(ctx, chainId, account, transactionPlan, options) {
23
93
  assertNoCowSwapPlanItems(transactionPlan, "deriveStateOverrides");
24
94
  const owner = getAddress(account);
@@ -97,7 +167,11 @@ export async function simulateTransactionPlan(ctx, chainId, account, transaction
97
167
  if (useStateOverrides) {
98
168
  effectiveStateOverrides = await deriveStateOverrides(ctx, chainId, owner, transactionPlan, options?.stateOverrideOptions);
99
169
  }
100
- const batch = transactionPlan.flatMap((item) => item.type === "evcBatch" ? flattenBatchEntries(item.items) : []);
170
+ // Preserve operation boundaries (EVCBatchOperation entries emitted by the
171
+ // planners / mergePlans) so reverts can be attributed to the operation that
172
+ // caused them and so we can snapshot state after each operation.
173
+ const operations = collectOperations(transactionPlan);
174
+ const batch = operations.flatMap((op) => op.items);
101
175
  if (batch.length === 0) {
102
176
  return {
103
177
  simulatedAccounts: [],
@@ -106,7 +180,32 @@ export async function simulateTransactionPlan(ctx, chainId, account, transaction
106
180
  };
107
181
  }
108
182
  const diagnostics = await fetchSimulationDiagnostics(ctx, chainId, owner, transactionPlan);
109
- const { fullBatch, lensMeta, evcAddress, totalValue } = await buildSimulationBatch(ctx, chainId, owner, batch);
183
+ const { lensItems, lensMeta, evcAddress } = await buildSimulationBatch(ctx, chainId, owner, operations);
184
+ // Assemble the EVC batch, interleaving a lens-read block before the first
185
+ // operation (layer 0 = real state) and one after every operation (layer i =
186
+ // state after op i), so the full per-layer history is always captured.
187
+ const fullBatch = [];
188
+ const actionPositions = []; // fullBatch indices of real items, in `batch` order
189
+ const opOfPosition = []; // operation index for each action position
190
+ const layerSlices = [];
191
+ const pushLensBlock = () => {
192
+ layerSlices.push({ lensStart: fullBatch.length });
193
+ for (const it of lensItems)
194
+ fullBatch.push(it);
195
+ };
196
+ const pushOperation = (op, opIndex) => {
197
+ for (const it of op.items) {
198
+ actionPositions.push(fullBatch.length);
199
+ opOfPosition.push(opIndex);
200
+ fullBatch.push(it);
201
+ }
202
+ };
203
+ pushLensBlock();
204
+ for (let i = 0; i < operations.length; i++) {
205
+ pushOperation(operations[i], i);
206
+ pushLensBlock();
207
+ }
208
+ const totalValue = fullBatch.reduce((sum, item) => sum + item.value, 0n);
110
209
  const simulationResult = await runSimulation(ctx, chainId, account, evcAddress, fullBatch, totalValue, effectiveStateOverrides);
111
210
  if ("simulationError" in simulationResult) {
112
211
  return {
@@ -116,7 +215,7 @@ export async function simulateTransactionPlan(ctx, chainId, account, transaction
116
215
  };
117
216
  }
118
217
  const { batchResults, accountStatusErrors, vaultStatusErrors } = simulationResult;
119
- const rawBatchResults = batchResults.slice(0, batch.length);
218
+ const rawBatchResults = actionPositions.map((pos) => batchResults[pos]);
120
219
  let describedBatch;
121
220
  try {
122
221
  describedBatch = ctx.describeBatch(batch);
@@ -131,27 +230,156 @@ export async function simulateTransactionPlan(ctx, chainId, account, transaction
131
230
  args: {},
132
231
  });
133
232
  const failedBatchItems = (await Promise.all(rawBatchResults.map(async (itemResult, index) => {
134
- if (itemResult.success)
233
+ if (!itemResult || itemResult.success)
135
234
  return null;
136
235
  const decodedError = await decodeSmartContractErrors(itemResult.result);
137
236
  const decodedItem = describedBatch && describedBatch.length === batch.length
138
237
  ? describedBatch[index]
139
238
  : fallbackDescription(batch[index]);
239
+ const operationIndex = opOfPosition[index];
140
240
  return {
141
241
  index,
242
+ operationIndex,
243
+ operationName: operations[operationIndex]?.name,
142
244
  item: decodedItem,
143
245
  error: itemResult.result,
144
246
  decodedError,
145
247
  };
146
248
  }))).filter((item) => item !== null);
249
+ // One account/vault/wallet snapshot per layer: [pre-batch, after op0, after op1, …].
250
+ const snapshots = [];
251
+ for (const slice of layerSlices) {
252
+ snapshots.push(await decodeAccountSnapshot(ctx, chainId, owner, lensMeta, (i) => batchResults[slice.lensStart + i], options));
253
+ }
254
+ const simulatedAccounts = snapshots.map((s) => s.account);
255
+ const simulatedVaultsLayers = snapshots.map((s) => s.vaults);
256
+ const simulatedVaults = simulatedVaultsLayers[simulatedVaultsLayers.length - 1] ?? [];
257
+ const simulatedWalletBalances = snapshots.map((s) => s.walletBalances);
258
+ // Accurate wallet shortfall from the per-layer balances (running-min over the
259
+ // real-anchored balance), which nets out intra-batch funding. Prefer it when
260
+ // available; if an item reverted before wallet deltas could be observed, keep
261
+ // the static requiredApproval diagnostic from fetchSimulationDiagnostics.
262
+ const insufficientWalletAssets = (await computeWalletShortfall(ctx, chainId, owner, simulatedWalletBalances)) ?? diagnostics.insufficientWalletAssets;
263
+ const canExecute = failedBatchItems.length === 0 &&
264
+ accountStatusErrors.length === 0 &&
265
+ vaultStatusErrors.length === 0 &&
266
+ !insufficientWalletAssets?.length &&
267
+ !diagnostics.insufficientPermit2Allowances?.length &&
268
+ !diagnostics.insufficientDirectAllowances?.length;
269
+ return {
270
+ simulatedAccounts,
271
+ simulatedVaults,
272
+ simulatedVaultsLayers,
273
+ simulatedWalletBalances,
274
+ canExecute,
275
+ rawBatchResults,
276
+ failedBatchItems: failedBatchItems.length > 0 ? failedBatchItems : undefined,
277
+ accountStatusErrors: accountStatusErrors.length > 0 ? accountStatusErrors : undefined,
278
+ vaultStatusErrors: vaultStatusErrors.length > 0 ? vaultStatusErrors : undefined,
279
+ ...diagnostics,
280
+ insufficientWalletAssets,
281
+ };
282
+ }
283
+ function collectOperations(transactionPlan) {
284
+ const operations = [];
285
+ for (const item of transactionPlan) {
286
+ if (item.type !== "evcBatch")
287
+ continue;
288
+ for (const entry of item.items) {
289
+ if (isEVCBatchOperation(entry)) {
290
+ operations.push({
291
+ name: entry.name,
292
+ items: entry.items,
293
+ walletBalanceTokens: entry.walletBalanceTokens,
294
+ });
295
+ }
296
+ else {
297
+ operations.push({ name: undefined, items: [entry] });
298
+ }
299
+ }
300
+ }
301
+ return operations;
302
+ }
303
+ const addWalletToken = (tokens, token) => {
304
+ if (!token)
305
+ return;
306
+ try {
307
+ const checksum = getAddress(token);
308
+ if (checksum !== zeroAddress)
309
+ tokens.add(checksum);
310
+ }
311
+ catch {
312
+ // Ignore malformed optional metadata / decoded calldata.
313
+ }
314
+ };
315
+ const getSelector = (data) => data.slice(0, 10);
316
+ function collectClaimWalletBalanceTokens(batch) {
317
+ const tokens = new Set();
318
+ for (const item of batch) {
319
+ const selector = getSelector(item.data);
320
+ try {
321
+ if (selector === MERKL_CLAIM_SELECTOR) {
322
+ const decoded = decodeFunctionData({
323
+ abi: merklClaimAbi,
324
+ data: item.data,
325
+ });
326
+ const rewardTokens = decoded.args[1];
327
+ for (const token of rewardTokens)
328
+ addWalletToken(tokens, token);
329
+ continue;
330
+ }
331
+ if (selector === FUUL_CLAIM_SELECTOR) {
332
+ const decoded = decodeFunctionData({
333
+ abi: fuulClaimAbi,
334
+ data: item.data,
335
+ });
336
+ const claimChecks = decoded.args[0];
337
+ for (const check of claimChecks) {
338
+ addWalletToken(tokens, check.currency);
339
+ }
340
+ continue;
341
+ }
342
+ if (selector === REWARD_STREAM_CLAIM_SELECTOR) {
343
+ const decoded = decodeFunctionData({
344
+ abi: rewardStreamsClaimAbi,
345
+ data: item.data,
346
+ });
347
+ addWalletToken(tokens, decoded.args[1]);
348
+ }
349
+ }
350
+ catch {
351
+ // Unknown or malformed claim-like calldata should not block simulation.
352
+ }
353
+ }
354
+ return [...tokens];
355
+ }
356
+ // Decode the lens-read block for a single layer into a populated Account plus
357
+ // the EVault/EulerEarn entities observed at that point in the batch.
358
+ async function decodeAccountSnapshot(ctx, chainId, owner, lensMeta, resultAt, options) {
147
359
  const vaultsByAddress = new Map();
148
360
  const evcInfos = new Map();
149
361
  const vaultInfosBySub = new Map();
362
+ const walletBalances = {};
363
+ // Securitize collateral vaults are assembled from three separate reads
364
+ // (ERC4626 info via UtilsLens, plus governorAdmin and supplyCapResolved read
365
+ // directly off the vault), keyed by vault address and stitched after the loop.
366
+ const securitizeInfos = new Map();
367
+ const securitizeGovernors = new Map();
368
+ const securitizeSupplyCaps = new Map();
150
369
  for (let i = 0; i < lensMeta.length; i++) {
151
370
  const meta = lensMeta[i];
152
- const resultItem = batchResults[batch.length + i];
371
+ const resultItem = resultAt(i);
153
372
  if (!resultItem?.success)
154
373
  continue;
374
+ if (meta.kind === "walletBalance") {
375
+ const bal = decodeFunctionResult({
376
+ abi: walletBalanceAbi,
377
+ functionName: "balanceOf",
378
+ data: resultItem.result,
379
+ });
380
+ walletBalances[getAddress(meta.token).toLowerCase()] = bal;
381
+ continue;
382
+ }
155
383
  if (meta.kind === "eVault") {
156
384
  const decodedVault = decodeFunctionResult({
157
385
  abi: vaultLensAbi,
@@ -170,6 +398,30 @@ export async function simulateTransactionPlan(ctx, chainId, account, transaction
170
398
  const entity = new EulerEarn(convertEulerEarnVaultInfoFullToIEulerEarn(decodedVault, chainId, []));
171
399
  vaultsByAddress.set(getAddress(meta.vault), entity);
172
400
  }
401
+ if (meta.kind === "securitizeInfo") {
402
+ const decoded = decodeFunctionResult({
403
+ abi: utilsLensAbi,
404
+ functionName: "getVaultInfoERC4626",
405
+ data: resultItem.result,
406
+ });
407
+ securitizeInfos.set(getAddress(meta.vault), decoded);
408
+ }
409
+ if (meta.kind === "securitizeGovernor") {
410
+ const decoded = decodeFunctionResult({
411
+ abi: erc4626EvcCollateralSecuritizeAbi,
412
+ functionName: "governorAdmin",
413
+ data: resultItem.result,
414
+ });
415
+ securitizeGovernors.set(getAddress(meta.vault), decoded);
416
+ }
417
+ if (meta.kind === "securitizeSupplyCap") {
418
+ const decoded = decodeFunctionResult({
419
+ abi: erc4626EvcCollateralSecuritizeAbi,
420
+ functionName: "supplyCapResolved",
421
+ data: resultItem.result,
422
+ });
423
+ securitizeSupplyCaps.set(getAddress(meta.vault), decoded);
424
+ }
173
425
  if (meta.kind === "evcAccount") {
174
426
  const decodedAccount = decodeFunctionResult({
175
427
  abi: accountLensAbi,
@@ -190,6 +442,18 @@ export async function simulateTransactionPlan(ctx, chainId, account, transaction
190
442
  vaultInfosBySub.set(key, list);
191
443
  }
192
444
  }
445
+ // Stitch the three Securitize reads into vault entities. Skip any vault whose
446
+ // governor/supply-cap read failed (kept undefined) so a partial read degrades
447
+ // to "no entity" rather than throwing — mirrors the per-vault tolerance above.
448
+ for (const [vault, info] of securitizeInfos.entries()) {
449
+ const governor = securitizeGovernors.get(vault);
450
+ const supplyCap = securitizeSupplyCaps.get(vault);
451
+ if (governor === undefined || supplyCap === undefined)
452
+ continue;
453
+ const conversionErrors = [];
454
+ const entity = new SecuritizeCollateralVault(convertToISecuritizeCollateralVault(info, governor, supplyCap, chainId, conversionErrors));
455
+ vaultsByAddress.set(vault, entity);
456
+ }
193
457
  const simulatedVaults = Array.from(vaultsByAddress.values());
194
458
  const vaultFetchOptions = options?.vaultFetchOptions ??
195
459
  options?.accountFetchOptions?.vaultFetchOptions;
@@ -240,22 +504,7 @@ export async function simulateTransactionPlan(ctx, chainId, account, transaction
240
504
  if (accountFetchOptions?.populateUserRewards && ctx.rewardsService) {
241
505
  await populatedAccount.populateUserRewards(ctx.rewardsService);
242
506
  }
243
- const result = {
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;
507
+ return { account: populatedAccount, vaults: simulatedVaults, walletBalances };
259
508
  }
260
509
  export async function estimateGasForTransactionPlan(ctx, chainId, account, transactionPlan, options) {
261
510
  assertNoCowSwapPlanItems(transactionPlan, "estimateGasForTransactionPlan");
@@ -312,8 +561,47 @@ function getAccountAdapter(ctx, methodName) {
312
561
  const emptyPositionsAdapter = { fetchAccountVaults: async () => ({}) };
313
562
  return new AccountOnchainAdapter(ctx.providerService, ctx.deploymentService, emptyPositionsAdapter);
314
563
  }
315
- async function buildSimulationBatch(ctx, chainId, owner, batch) {
564
+ async function buildSimulationBatch(ctx, chainId, owner, operations) {
565
+ const batch = operations.flatMap((op) => op.items);
316
566
  const { candidateVaults, subAccountVaults } = collectCandidateVaults(ctx, owner, batch);
567
+ // Each touched sub-account's account-level liquidity (health factor, current
568
+ // LTV, collateral/liability values) rides on its *controller* vault's
569
+ // getVaultAccountInfo. An op that only touches collateral vaults (e.g. a
570
+ // collateral withdraw or supply) wouldn't otherwise read the controller, so
571
+ // the simulated liquidity would stay at the pre-batch (stale) state. Pull each
572
+ // touched sub-account's enabled controllers and read them too, so liquidity is
573
+ // recomputed post-op for every operation, not just borrows/repays.
574
+ const ctrlProvider = ctx.providerService?.getProvider(chainId);
575
+ const touchedSubs = Array.from(subAccountVaults.keys());
576
+ if (ctrlProvider && touchedSubs.length) {
577
+ const evc = ctx.deploymentService.getDeployment(chainId).addresses.coreAddrs
578
+ .evc;
579
+ try {
580
+ const controllerResults = await ctrlProvider.multicall({
581
+ allowFailure: true,
582
+ contracts: touchedSubs.map((sub) => ({
583
+ address: evc,
584
+ abi: ethereumVaultConnectorAbi,
585
+ functionName: "getControllers",
586
+ args: [sub],
587
+ })),
588
+ });
589
+ touchedSubs.forEach((sub, i) => {
590
+ const r = controllerResults[i];
591
+ if (r?.status !== "success")
592
+ return;
593
+ for (const controller of r.result ?? []) {
594
+ const c = getAddress(controller);
595
+ subAccountVaults.get(sub)?.add(c);
596
+ candidateVaults.add(c);
597
+ }
598
+ });
599
+ }
600
+ catch {
601
+ // Best-effort: if controllers can't be fetched, liquidity may stay stale
602
+ // for collateral-only ops, but the simulation itself is unaffected.
603
+ }
604
+ }
317
605
  const vaultCandidatesList = Array.from(candidateVaults);
318
606
  if (!ctx.vaultMetaService) {
319
607
  throw new Error("ExecutionService.simulateTransactionPlan requires a vaultMetaService. Pass it to the ExecutionService constructor or call setVaultMetaService().");
@@ -322,23 +610,28 @@ async function buildSimulationBatch(ctx, chainId, owner, batch) {
322
610
  const validVaults = new Set();
323
611
  const eVaults = [];
324
612
  const eulerEarnVaults = [];
613
+ const securitizeVaults = [];
325
614
  for (const vault of vaultCandidatesList) {
326
615
  const key = getAddress(vault);
327
616
  const type = vaultTypes[key];
328
617
  if (!type)
329
618
  continue;
330
- if (type === VaultType.SecuritizeCollateral)
331
- continue;
332
619
  validVaults.add(key);
333
620
  if (type === VaultType.EVault)
334
621
  eVaults.push(key);
335
622
  if (type === VaultType.EulerEarn)
336
623
  eulerEarnVaults.push(key);
624
+ // Securitize RWA collaterals are real Euler collateral vaults; keep them in
625
+ // validVaults so their per-account position (getVaultAccountInfo) is read,
626
+ // and queue the dedicated reads below so the vault entity is resolved too.
627
+ if (type === VaultType.SecuritizeCollateral)
628
+ securitizeVaults.push(key);
337
629
  }
338
630
  const deployment = ctx.deploymentService.getDeployment(chainId);
339
631
  const accountLensAddress = deployment.addresses.lensAddrs.accountLens;
340
632
  const vaultLensAddress = deployment.addresses.lensAddrs.vaultLens;
341
633
  const eulerEarnLensAddress = deployment.addresses.lensAddrs.eulerEarnVaultLens;
634
+ const utilsLensAddress = deployment.addresses.lensAddrs.utilsLens;
342
635
  const evcAddress = deployment.addresses.coreAddrs.evc;
343
636
  const lensItems = [];
344
637
  const lensMeta = [];
@@ -358,6 +651,20 @@ async function buildSimulationBatch(ctx, chainId, owner, batch) {
358
651
  vault,
359
652
  });
360
653
  }
654
+ for (const vault of securitizeVaults) {
655
+ pushLensItem(getVaultInfoERC4626LensBatchItem(utilsLensAddress, vault, owner), {
656
+ kind: "securitizeInfo",
657
+ vault,
658
+ });
659
+ pushLensItem(getSecuritizeGovernorAdminBatchItem(vault, owner), {
660
+ kind: "securitizeGovernor",
661
+ vault,
662
+ });
663
+ pushLensItem(getSecuritizeSupplyCapResolvedBatchItem(vault, owner), {
664
+ kind: "securitizeSupplyCap",
665
+ vault,
666
+ });
667
+ }
361
668
  for (const [subAccount, vaults] of subAccountVaults.entries()) {
362
669
  pushLensItem(getEVCAccountInfoLensBatchItem(accountLensAddress, evcAddress, subAccount, owner), {
363
670
  kind: "evcAccount",
@@ -373,9 +680,71 @@ async function buildSimulationBatch(ctx, chainId, owner, batch) {
373
680
  });
374
681
  }
375
682
  }
376
- const fullBatch = [...batch, ...lensItems];
377
- const totalValue = fullBatch.reduce((sum, item) => sum + item.value, 0n);
378
- return { fullBatch, lensMeta, evcAddress, totalValue };
683
+ // Wallet-balance reads: resolve the underlying asset of each touched vault,
684
+ // plus reward tokens from claim/unlock operations, and read balanceOf(owner)
685
+ // for each token. Interleaved per layer like the other lens reads, so wallet
686
+ // balances are captured after every operation. Balances are forged by state
687
+ // overrides, so consumers stitch using the delta vs the pre-batch layer.
688
+ const provider = ctx.providerService?.getProvider(chainId);
689
+ if (provider) {
690
+ const vaultsForAssets = [...eVaults, ...eulerEarnVaults, ...securitizeVaults];
691
+ const assets = await Promise.all(vaultsForAssets.map((vault) => provider
692
+ .readContract({
693
+ address: vault,
694
+ abi: walletBalanceAbi,
695
+ functionName: "asset",
696
+ })
697
+ .then((a) => getAddress(a))
698
+ .catch(() => null)));
699
+ const assetTokens = new Set();
700
+ for (const a of assets)
701
+ if (a)
702
+ assetTokens.add(getAddress(a));
703
+ for (const operation of operations) {
704
+ for (const token of operation.walletBalanceTokens ?? []) {
705
+ addWalletToken(assetTokens, token);
706
+ }
707
+ }
708
+ for (const token of collectClaimWalletBalanceTokens(batch)) {
709
+ addWalletToken(assetTokens, token);
710
+ }
711
+ const unlockItems = batch.filter((item) => getSelector(item.data) === REUL_UNLOCK_SELECTOR);
712
+ if (unlockItems.length > 0) {
713
+ const configuredEul = deployment.addresses.tokenAddrs?.EUL;
714
+ if (configuredEul) {
715
+ addWalletToken(assetTokens, configuredEul);
716
+ }
717
+ else {
718
+ const underlyingTokens = await Promise.all(unlockItems.map((item) => provider
719
+ .readContract({
720
+ address: item.targetContract,
721
+ abi: walletBalanceAbi,
722
+ functionName: "underlying",
723
+ })
724
+ .then((a) => getAddress(a))
725
+ .catch(() => null)));
726
+ for (const token of underlyingTokens) {
727
+ if (token)
728
+ addWalletToken(assetTokens, token);
729
+ }
730
+ }
731
+ }
732
+ for (const token of assetTokens) {
733
+ pushLensItem({
734
+ targetContract: token,
735
+ onBehalfOfAccount: owner,
736
+ value: 0n,
737
+ data: encodeFunctionData({
738
+ abi: walletBalanceAbi,
739
+ functionName: "balanceOf",
740
+ args: [owner],
741
+ }),
742
+ }, { kind: "walletBalance", token });
743
+ }
744
+ }
745
+ // The caller assembles the final batch (interleaving these lens reads per the
746
+ // chosen layers mode), so we just return the reusable lens block.
747
+ return { lensItems, lensMeta, evcAddress };
379
748
  }
380
749
  function collectCandidateVaults(ctx, owner, batch) {
381
750
  const candidateVaults = new Set();
@@ -399,9 +768,20 @@ function collectCandidateVaults(ctx, owner, batch) {
399
768
  for (const item of described) {
400
769
  const fn = item.functionName.toLowerCase();
401
770
  const target = getAddress(item.targetContract);
402
- if (fn === "transfer" || fn === "transferfrom") {
771
+ if (fn === "enablecollateral" ||
772
+ fn === "disablecollateral" ||
773
+ fn === "enablecontroller") {
774
+ const account = item.args.account;
775
+ const vault = item.args.vault;
776
+ if (vault)
777
+ addCandidateVault(vault);
778
+ if (account && vault)
779
+ addSubAccountVault(account, vault);
780
+ continue;
781
+ }
782
+ if (fn === "transfer" || fn === "transferfrom" || fn === "transferfrommax") {
403
783
  const to = item.args.to;
404
- const from = fn === "transferfrom"
784
+ const from = fn === "transferfrom" || fn === "transferfrommax"
405
785
  ? item.args.from
406
786
  : (item.args.from ?? item.onBehalfOfAccount);
407
787
  if (from)
@@ -411,12 +791,68 @@ function collectCandidateVaults(ctx, owner, batch) {
411
791
  addCandidateVault(target);
412
792
  continue;
413
793
  }
794
+ if (fn === "withdraw" || fn === "redeem") {
795
+ const owner = item.args.owner;
796
+ if (owner)
797
+ addSubAccountVault(owner, target);
798
+ addCandidateVault(target);
799
+ continue;
800
+ }
801
+ if (fn === "liquidate") {
802
+ const violator = item.args.violator;
803
+ const collateral = item.args.collateral;
804
+ if (violator)
805
+ addSubAccountVault(violator, target);
806
+ if (violator && collateral)
807
+ addSubAccountVault(violator, collateral);
808
+ if (collateral) {
809
+ addSubAccountVault(item.onBehalfOfAccount, collateral);
810
+ addCandidateVault(collateral);
811
+ }
812
+ addCandidateVault(target);
813
+ continue;
814
+ }
815
+ if (fn === "pulldebt") {
816
+ const from = item.args.from;
817
+ if (from)
818
+ addSubAccountVault(from, target);
819
+ addCandidateVault(target);
820
+ continue;
821
+ }
414
822
  if (fn === "deposit" || fn === "skim" || fn === "mint") {
415
823
  const receiver = item.args.receiver;
416
824
  if (receiver)
417
825
  addSubAccountVault(receiver, target);
418
826
  addCandidateVault(target);
419
827
  }
828
+ // repay(amount, receiver) / repayWithShares(amount, receiver): the *receiver*
829
+ // is the account whose debt is reduced, and it can differ from the batch
830
+ // item's onBehalfOfAccount (e.g. a repay-from-wallet runs on behalf of the
831
+ // owner but repays a sub-account's debt). Map the receiver so its post-repay
832
+ // debt position (and liquidity) is read — otherwise the simulated debt/health
833
+ // of the repaid sub-account stays at the pre-batch state.
834
+ if (fn === "repay" || fn === "repaywithshares") {
835
+ const receiver = item.args.receiver;
836
+ if (receiver)
837
+ addSubAccountVault(receiver, target);
838
+ addCandidateVault(target);
839
+ }
840
+ if (fn === "verifyamountminanddeposit" || fn === "verifyamountminandskim") {
841
+ const vault = item.args.vault;
842
+ const receiver = item.args.receiver;
843
+ if (vault)
844
+ addCandidateVault(vault);
845
+ if (vault && receiver)
846
+ addSubAccountVault(receiver, vault);
847
+ }
848
+ if (fn === "verifydebtmax") {
849
+ const vault = item.args.vault;
850
+ const account = item.args.account;
851
+ if (vault)
852
+ addCandidateVault(vault);
853
+ if (vault && account)
854
+ addSubAccountVault(account, vault);
855
+ }
420
856
  }
421
857
  if (!subAccountVaults.has(owner)) {
422
858
  subAccountVaults.set(owner, new Set());
@@ -476,6 +912,50 @@ async function runSimulation(ctx, chainId, account, evcAddress, fullBatch, total
476
912
  })));
477
913
  return { batchResults, accountStatusErrors, vaultStatusErrors };
478
914
  }
915
+ /**
916
+ * Wallet shortfall derived from the per-layer simulated balances. In-sim
917
+ * balances are forged by state overrides, so we anchor to the real on-chain
918
+ * balance and track the running balance across layers — real balance plus each
919
+ * layer's net delta vs the pre-batch layer. The shortfall per token is the worst
920
+ * dip below zero. This nets out intra-batch funding (e.g. withdraw then deposit
921
+ * the same asset → never dips) yet still flags a step that consumes more than is
922
+ * genuinely available at that point. Returns undefined when balances can't be
923
+ * resolved (no walletService / no touched tokens), so callers don't over-block.
924
+ */
925
+ async function computeWalletShortfall(ctx, chainId, owner, simulatedWalletBalances) {
926
+ if (!ctx.walletService || simulatedWalletBalances.length === 0)
927
+ return undefined;
928
+ const base = simulatedWalletBalances[0] ?? {};
929
+ const tokens = Object.keys(base);
930
+ if (tokens.length === 0)
931
+ return undefined;
932
+ let realByToken;
933
+ try {
934
+ const wallet = (await ctx.walletService.fetchWallet(chainId, owner, tokens.map((t) => ({ asset: getAddress(t), spenders: [] })))).result;
935
+ realByToken = {};
936
+ for (const t of tokens)
937
+ realByToken[t] = wallet.getBalance(getAddress(t));
938
+ }
939
+ catch {
940
+ return undefined;
941
+ }
942
+ const shortfalls = [];
943
+ for (const t of tokens) {
944
+ const baseBal = base[t] ?? 0n;
945
+ const real = realByToken[t] ?? 0n;
946
+ // Worst running balance across layers (layer 0's delta is 0 ⇒ starts at real).
947
+ let minAvailable = real;
948
+ for (let i = 1; i < simulatedWalletBalances.length; i++) {
949
+ const delta = (simulatedWalletBalances[i][t] ?? 0n) - baseBal;
950
+ const available = real + delta;
951
+ if (available < minAvailable)
952
+ minAvailable = available;
953
+ }
954
+ if (minAvailable < 0n)
955
+ shortfalls.push({ token: getAddress(t), amount: -minAvailable });
956
+ }
957
+ return shortfalls.length > 0 ? shortfalls : undefined;
958
+ }
479
959
  async function fetchSimulationDiagnostics(ctx, chainId, account, transactionPlan) {
480
960
  if (!ctx.walletService || !transactionPlan)
481
961
  return {};
@@ -549,20 +1029,25 @@ async function fetchSimulationDiagnostics(ctx, chainId, account, transactionPlan
549
1029
  : {}),
550
1030
  };
551
1031
  }
552
- function extractBalanceRequirements(transactionPlan, account) {
553
- const maxPerToken = new Map();
1032
+ // Sum the required amount per token across every approval the owner must fund.
1033
+ // Each requiredApproval is a wallet outflow, and several can draw on the same
1034
+ // token within one batch (e.g. supplying it into two vaults, or supply + repay),
1035
+ // so the wallet must cover their *total*, not the largest single one. Forging
1036
+ // the sum lets an underfunded batch simulate through to completion — so the
1037
+ // running-balance shortfall reports the true peak deficit instead of the batch
1038
+ // reverting partway with E_InsufficientBalance. Over-forging is harmless: the
1039
+ // shortfall is computed against the real balance, not this forged amount.
1040
+ export function extractBalanceRequirements(transactionPlan, account) {
1041
+ const totalPerToken = new Map();
554
1042
  for (const item of transactionPlan) {
555
1043
  if (item.type !== "requiredApproval")
556
1044
  continue;
557
1045
  if (getAddress(item.owner) !== getAddress(account))
558
1046
  continue;
559
1047
  const token = getAddress(item.token);
560
- const current = maxPerToken.get(token) ?? 0n;
561
- if (item.amount > current) {
562
- maxPerToken.set(token, item.amount);
563
- }
1048
+ totalPerToken.set(token, (totalPerToken.get(token) ?? 0n) + item.amount);
564
1049
  }
565
- return Array.from(maxPerToken.entries());
1050
+ return Array.from(totalPerToken.entries());
566
1051
  }
567
1052
  function extractApprovalRequirements(transactionPlan, account) {
568
1053
  const seen = new Set();