@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.
- 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 +523 -38
- 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/services/vaults/securitizeVaultService/adapters/securitizeVaultOnchainAdapter.d.ts +3 -1
- package/dist/src/services/vaults/securitizeVaultService/adapters/securitizeVaultOnchainAdapter.d.ts.map +1 -1
- package/dist/src/services/vaults/securitizeVaultService/adapters/securitizeVaultOnchainAdapter.js +20 -2
- package/dist/src/services/vaults/securitizeVaultService/adapters/securitizeVaultOnchainAdapter.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 +40 -36
- 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";
|
|
@@ -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
|
-
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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 === "
|
|
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
|
-
|
|
553
|
-
|
|
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
|
-
|
|
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(
|
|
1050
|
+
return Array.from(totalPerToken.entries());
|
|
566
1051
|
}
|
|
567
1052
|
function extractApprovalRequirements(transactionPlan, account) {
|
|
568
1053
|
const seen = new Set();
|