@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.
Files changed (54) 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 +454 -36
  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/utils/stateOverrides/approvalOverrides.d.ts +1 -1
  44. package/dist/src/utils/stateOverrides/approvalOverrides.d.ts.map +1 -1
  45. package/dist/src/utils/stateOverrides/approvalOverrides.js +3 -9
  46. package/dist/src/utils/stateOverrides/approvalOverrides.js.map +1 -1
  47. package/dist/src/utils/stateOverrides/balanceOverrides.d.ts +8 -6
  48. package/dist/src/utils/stateOverrides/balanceOverrides.d.ts.map +1 -1
  49. package/dist/src/utils/stateOverrides/balanceOverrides.js +62 -55
  50. package/dist/src/utils/stateOverrides/balanceOverrides.js.map +1 -1
  51. package/dist/src/utils/stateOverrides/getStateOverrides.d.ts.map +1 -1
  52. package/dist/src/utils/stateOverrides/getStateOverrides.js +9 -8
  53. package/dist/src/utils/stateOverrides/getStateOverrides.js.map +1 -1
  54. 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
- const batch = transactionPlan.flatMap((item) => item.type === "evcBatch" ? flattenBatchEntries(item.items) : []);
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 { fullBatch, lensMeta, evcAddress, totalValue } = await buildSimulationBatch(ctx, chainId, owner, batch);
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 = batchResults.slice(0, batch.length);
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 = batchResults[batch.length + i];
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
- 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;
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, batch) {
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
- const fullBatch = [...batch, ...lensItems];
377
- const totalValue = fullBatch.reduce((sum, item) => sum + item.value, 0n);
378
- return { fullBatch, lensMeta, evcAddress, totalValue };
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 === "transfer" || fn === "transferfrom") {
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
- function extractBalanceRequirements(transactionPlan, account) {
553
- const maxPerToken = new Map();
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
- const current = maxPerToken.get(token) ?? 0n;
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(maxPerToken.entries());
983
+ return Array.from(totalPerToken.entries());
566
984
  }
567
985
  function extractApprovalRequirements(transactionPlan, account) {
568
986
  const seen = new Set();