@eulerxyz/euler-v2-sdk 0.1.2-beta → 0.1.3-beta

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 (92) hide show
  1. package/README.md +1 -1
  2. package/dist/src/entities/Account.d.ts.map +1 -1
  3. package/dist/src/entities/Account.js +11 -4
  4. package/dist/src/entities/Account.js.map +1 -1
  5. package/dist/src/entities/Portfolio.d.ts +21 -1
  6. package/dist/src/entities/Portfolio.d.ts.map +1 -1
  7. package/dist/src/entities/Portfolio.js +48 -17
  8. package/dist/src/entities/Portfolio.js.map +1 -1
  9. package/dist/src/index.d.ts +1 -1
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/index.js +1 -1
  12. package/dist/src/index.js.map +1 -1
  13. package/dist/src/plugins/keyring/keyringPlugin.d.ts +1 -1
  14. package/dist/src/plugins/keyring/keyringPlugin.d.ts.map +1 -1
  15. package/dist/src/plugins/keyring/keyringPlugin.js +12 -3
  16. package/dist/src/plugins/keyring/keyringPlugin.js.map +1 -1
  17. package/dist/src/sdk/buildSDK.d.ts +1 -3
  18. package/dist/src/sdk/buildSDK.d.ts.map +1 -1
  19. package/dist/src/sdk/buildSDK.js +10 -6
  20. package/dist/src/sdk/buildSDK.js.map +1 -1
  21. package/dist/src/sdk/defaultConfig.js +7 -7
  22. package/dist/src/sdk/defaultConfig.js.map +1 -1
  23. package/dist/src/sdk/sdk.d.ts +2 -5
  24. package/dist/src/sdk/sdk.d.ts.map +1 -1
  25. package/dist/src/sdk/sdk.js +0 -2
  26. package/dist/src/sdk/sdk.js.map +1 -1
  27. package/dist/src/services/accountService/adapters/accountV3Adapter/accountV3Adapter.d.ts +7 -1
  28. package/dist/src/services/accountService/adapters/accountV3Adapter/accountV3Adapter.d.ts.map +1 -1
  29. package/dist/src/services/accountService/adapters/accountV3Adapter/accountV3Adapter.js +100 -18
  30. package/dist/src/services/accountService/adapters/accountV3Adapter/accountV3Adapter.js.map +1 -1
  31. package/dist/src/services/executionService/encode.d.ts +30 -0
  32. package/dist/src/services/executionService/encode.d.ts.map +1 -0
  33. package/dist/src/services/executionService/encode.js +841 -0
  34. package/dist/src/services/executionService/encode.js.map +1 -0
  35. package/dist/src/services/executionService/execute.d.ts +64 -0
  36. package/dist/src/services/executionService/execute.d.ts.map +1 -0
  37. package/dist/src/services/executionService/execute.js +181 -0
  38. package/dist/src/services/executionService/execute.js.map +1 -0
  39. package/dist/src/services/executionService/executionService.d.ts +163 -37
  40. package/dist/src/services/executionService/executionService.d.ts.map +1 -1
  41. package/dist/src/services/executionService/executionService.js +493 -893
  42. package/dist/src/services/executionService/executionService.js.map +1 -1
  43. package/dist/src/services/executionService/executionServiceTypes.d.ts +93 -9
  44. package/dist/src/services/executionService/executionServiceTypes.d.ts.map +1 -1
  45. package/dist/src/services/executionService/executionServiceTypes.js +6 -0
  46. package/dist/src/services/executionService/executionServiceTypes.js.map +1 -1
  47. package/dist/src/services/executionService/index.d.ts +5 -2
  48. package/dist/src/services/executionService/index.d.ts.map +1 -1
  49. package/dist/src/services/executionService/index.js +3 -1
  50. package/dist/src/services/executionService/index.js.map +1 -1
  51. package/dist/src/services/{simulationService/simulationService.d.ts → executionService/simulate.d.ts} +26 -40
  52. package/dist/src/services/executionService/simulate.d.ts.map +1 -0
  53. package/dist/src/services/executionService/simulate.js +531 -0
  54. package/dist/src/services/executionService/simulate.js.map +1 -0
  55. package/dist/src/services/intrinsicApyService/adapters/intrinsicApyV3Adapter/intrinsicApyV3AdapterTypes.d.ts +1 -1
  56. package/dist/src/services/intrinsicApyService/adapters/intrinsicApyV3Adapter/intrinsicApyV3AdapterTypes.d.ts.map +1 -1
  57. package/dist/src/services/priceService/backendClient.d.ts +0 -2
  58. package/dist/src/services/priceService/backendClient.d.ts.map +1 -1
  59. package/dist/src/services/priceService/backendClient.js.map +1 -1
  60. package/dist/src/services/rewardsService/adapters/rewardsV3Adapter/rewardsV3Adapter.d.ts.map +1 -1
  61. package/dist/src/services/rewardsService/adapters/rewardsV3Adapter/rewardsV3Adapter.js +26 -13
  62. package/dist/src/services/rewardsService/adapters/rewardsV3Adapter/rewardsV3Adapter.js.map +1 -1
  63. package/dist/src/services/rewardsService/adapters/rewardsV3Adapter/rewardsV3AdapterTypes.d.ts +10 -10
  64. package/dist/src/services/rewardsService/adapters/rewardsV3Adapter/rewardsV3AdapterTypes.d.ts.map +1 -1
  65. package/dist/src/services/rewardsService/rewardsServiceTypes.d.ts +1 -1
  66. package/dist/src/services/rewardsService/rewardsServiceTypes.d.ts.map +1 -1
  67. package/dist/src/services/vaults/eVaultService/adapters/eVaultV3Adapter/eVaultV3Adapter.d.ts +1 -0
  68. package/dist/src/services/vaults/eVaultService/adapters/eVaultV3Adapter/eVaultV3Adapter.d.ts.map +1 -1
  69. package/dist/src/services/vaults/eVaultService/adapters/eVaultV3Adapter/eVaultV3Adapter.js +20 -3
  70. package/dist/src/services/vaults/eVaultService/adapters/eVaultV3Adapter/eVaultV3Adapter.js.map +1 -1
  71. package/dist/src/services/vaults/eVaultService/eVaultServiceConfig.d.ts +1 -1
  72. package/dist/src/services/vaults/eVaultService/eVaultServiceConfig.d.ts.map +1 -1
  73. package/dist/src/services/vaults/eulerEarnService/eulerEarnServiceConfig.d.ts +1 -1
  74. package/dist/src/services/vaults/eulerEarnService/eulerEarnServiceConfig.d.ts.map +1 -1
  75. package/dist/src/services/vaults/vaultMetaService/adapters/VaultTypeV3Adapter.d.ts +1 -1
  76. package/dist/src/services/vaults/vaultMetaService/adapters/VaultTypeV3Adapter.d.ts.map +1 -1
  77. package/dist/src/utils/accountComputations.d.ts +29 -0
  78. package/dist/src/utils/accountComputations.d.ts.map +1 -1
  79. package/dist/src/utils/accountComputations.js +84 -4
  80. package/dist/src/utils/accountComputations.js.map +1 -1
  81. package/dist/src/utils/accountPositionClassification.d.ts +15 -0
  82. package/dist/src/utils/accountPositionClassification.d.ts.map +1 -0
  83. package/dist/src/utils/accountPositionClassification.js +27 -0
  84. package/dist/src/utils/accountPositionClassification.js.map +1 -0
  85. package/package.json +2 -2
  86. package/dist/src/services/simulationService/index.d.ts +0 -3
  87. package/dist/src/services/simulationService/index.d.ts.map +0 -1
  88. package/dist/src/services/simulationService/index.js +0 -2
  89. package/dist/src/services/simulationService/index.js.map +0 -1
  90. package/dist/src/services/simulationService/simulationService.d.ts.map +0 -1
  91. package/dist/src/services/simulationService/simulationService.js +0 -533
  92. package/dist/src/services/simulationService/simulationService.js.map +0 -1
@@ -1,34 +1,120 @@
1
- import { encodeFunctionData, getAddress, maxUint256, zeroAddress, maxUint160, maxUint48, erc20Abi, decodeFunctionData, } from "viem";
1
+ import { decodeFunctionData, encodeFunctionData, erc20Abi, getAddress, maxUint256, maxUint160, } from "viem";
2
+ import { resolveBorrowCollateralPositions } from "../../utils/accountPositionClassification.js";
2
3
  import { ethereumVaultConnectorAbi } from "./abis/ethereumVaultConnectorAbi.js";
3
4
  import { eVaultAbi } from "./abis/eVaultAbi.js";
4
5
  import { permit2PermitAbi } from "./abis/permit2PermitAbi.js";
5
6
  import { swapperAbi } from "./abis/swapperAbi.js";
6
7
  import { swapVerifierAbi } from "./abis/swapVerifierAbi.js";
7
- import { PERMIT2_TYPES, } from "./executionServiceTypes.js";
8
+ import * as encodeHelpers from "./encode.js";
9
+ import { isEVCBatchOperation } from "./executionServiceTypes.js";
10
+ import { executeTransactionPlan, } from "./execute.js";
11
+ import { deriveStateOverrides, estimateGasForTransactionPlan, simulateTransactionPlan, } from "./simulate.js";
8
12
  const TOKENS_REQUIRING_ZERO_APPROVAL_RESET = {
9
13
  1: [getAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7")],
10
14
  };
11
15
  function requiresZeroApprovalReset(chainId, token) {
12
16
  return (TOKENS_REQUIRING_ZERO_APPROVAL_RESET[chainId]?.some((resetToken) => resetToken === getAddress(token)) ?? false);
13
17
  }
14
- const PERMIT2_SIG_WINDOW = 60n * 60n;
18
+ function isSavingsCollateral(collateral) {
19
+ return collateral?.source === "savings";
20
+ }
21
+ function isWalletCollateral(collateral) {
22
+ return !!collateral && !isSavingsCollateral(collateral);
23
+ }
24
+ function cloneBatchEntries(entries) {
25
+ return entries.map((entry) => "type" in entry && entry.type === "operation"
26
+ ? { type: "operation", name: entry.name, items: [...entry.items] }
27
+ : entry);
28
+ }
15
29
  const WAD = 10n ** 18n;
16
30
  // TODO explain how this service is coupled to the concrete abis of ERC4626, permit2 and EVK.
17
31
  // this is a helper service, not a generic one.
18
32
  export class ExecutionService {
19
33
  deploymentService;
20
- walletService;
21
34
  plugins = [];
22
- constructor(deploymentService, walletService) {
35
+ walletService;
36
+ providerService;
37
+ vaultMetaService;
38
+ priceService;
39
+ rewardsService;
40
+ intrinsicApyService;
41
+ eulerLabelsService;
42
+ constructor(deploymentService, walletService, providerService, vaultMetaService, priceService, rewardsService, intrinsicApyService, eulerLabelsService) {
23
43
  this.deploymentService = deploymentService;
24
44
  this.walletService = walletService;
45
+ this.providerService = providerService;
46
+ this.vaultMetaService = vaultMetaService;
47
+ this.priceService = priceService;
48
+ this.rewardsService = rewardsService;
49
+ this.intrinsicApyService = intrinsicApyService;
50
+ this.eulerLabelsService = eulerLabelsService;
25
51
  }
26
52
  setWalletService(walletService) {
27
53
  this.walletService = walletService;
28
54
  }
55
+ setProviderService(providerService) {
56
+ this.providerService = providerService;
57
+ }
58
+ setVaultMetaService(vaultMetaService) {
59
+ this.vaultMetaService = vaultMetaService;
60
+ }
61
+ setPriceService(priceService) {
62
+ this.priceService = priceService;
63
+ }
64
+ setRewardsService(rewardsService) {
65
+ this.rewardsService = rewardsService;
66
+ }
67
+ setIntrinsicApyService(intrinsicApyService) {
68
+ this.intrinsicApyService = intrinsicApyService;
69
+ }
70
+ setEulerLabelsService(eulerLabelsService) {
71
+ this.eulerLabelsService = eulerLabelsService;
72
+ }
29
73
  setPlugins(plugins) {
30
74
  this.plugins = plugins;
31
75
  }
76
+ /** Derive storage overrides needed to simulate the plan against the current account state. */
77
+ async deriveStateOverrides(chainId, account, transactionPlan, options) {
78
+ return deriveStateOverrides(this.getSimulationContext(), chainId, account, transactionPlan, options);
79
+ }
80
+ /** Simulate the full transaction plan, including approval resolution and plugin-aware batch execution. */
81
+ async simulateTransactionPlan(chainId, account, transactionPlan, options) {
82
+ return simulateTransactionPlan(this.getSimulationContext(), chainId, account, transactionPlan, options);
83
+ }
84
+ /** Estimate gas for the full transaction plan after applying the same simulation pipeline used for execution. */
85
+ async estimateGasForTransactionPlan(chainId, account, transactionPlan, options) {
86
+ return estimateGasForTransactionPlan(this.getSimulationContext(), chainId, account, transactionPlan, options);
87
+ }
88
+ /** Execute a transaction plan using caller-provided signing and send callbacks. */
89
+ async executeTransactionPlan(args) {
90
+ const { providerService } = this;
91
+ if (!providerService) {
92
+ throw new Error("ExecutionService.executeTransactionPlan requires a providerService. Pass it to the ExecutionService constructor or call setProviderService().");
93
+ }
94
+ const helperArgs = {
95
+ ...args,
96
+ executionService: this,
97
+ deploymentService: this.deploymentService,
98
+ providerService,
99
+ };
100
+ return executeTransactionPlan(helperArgs);
101
+ }
102
+ getSimulationContext() {
103
+ return {
104
+ deploymentService: this.deploymentService,
105
+ walletService: this.walletService,
106
+ providerService: this.providerService,
107
+ vaultMetaService: this.vaultMetaService,
108
+ priceService: this.priceService,
109
+ rewardsService: this.rewardsService,
110
+ intrinsicApyService: this.intrinsicApyService,
111
+ eulerLabelsService: this.eulerLabelsService,
112
+ describeBatch: (batch) => this.describeBatch(batch),
113
+ };
114
+ }
115
+ getCoreAddresses(chainId) {
116
+ return this.deploymentService.getDeployment(chainId).addresses.coreAddrs;
117
+ }
32
118
  /**
33
119
  * Encodes an array of EVC batch items into a single calldata hex for `EVC.batch()`.
34
120
  *
@@ -36,11 +122,7 @@ export class ExecutionService {
36
122
  * @returns Encoded calldata hex for the EVC batch call
37
123
  */
38
124
  encodeBatch(items) {
39
- return encodeFunctionData({
40
- abi: ethereumVaultConnectorAbi,
41
- functionName: "batch",
42
- args: [items],
43
- });
125
+ return encodeHelpers.encodeBatch(items);
44
126
  }
45
127
  /**
46
128
  * Encodes EVC batch items for depositing underlying assets into a vault (mints shares to receiver).
@@ -55,33 +137,9 @@ export class ExecutionService {
55
137
  * @param args.permit2 - Optional Permit2 message + signature; if set, prepends a permit2 permit call so transferFrom can be used
56
138
  * @returns Array of EVC batch items (optional permit2, optional enableCollateral, deposit)
57
139
  */
58
- encodeDeposit({ chainId, vault, amount, receiver, owner, enableCollateral, permit2, }) {
59
- const items = [];
60
- if (permit2) {
61
- const permit2Call = this.encodePermit2Call({
62
- chainId,
63
- owner,
64
- message: permit2.message,
65
- signature: permit2.signature,
66
- });
67
- items.push(permit2Call);
68
- }
69
- // Add enable collateral if flag is set
70
- if (enableCollateral) {
71
- items.push(this.encodeEnableCollateral(chainId, receiver, vault));
72
- }
73
- // Add deposit operation
74
- items.push({
75
- targetContract: vault,
76
- onBehalfOfAccount: owner,
77
- value: 0n,
78
- data: encodeFunctionData({
79
- abi: eVaultAbi,
80
- functionName: "deposit",
81
- args: [amount, receiver],
82
- }),
83
- });
84
- return items;
140
+ encodeDeposit({ chainId, ...args }) {
141
+ const { evc, permit2 } = this.getCoreAddresses(chainId);
142
+ return encodeHelpers.encodeDeposit(evc, permit2, { chainId, ...args });
85
143
  }
86
144
  /**
87
145
  * Encodes EVC batch items for minting vault shares by depositing underlying assets.
@@ -96,33 +154,9 @@ export class ExecutionService {
96
154
  * @param args.permit2 - Optional Permit2 message + signature for transferFrom
97
155
  * @returns Array of EVC batch items (optional permit2, optional enableCollateral, mint)
98
156
  */
99
- encodeMint({ chainId, vault, shares, receiver, owner, enableCollateral, permit2, }) {
100
- const items = [];
101
- if (permit2) {
102
- const permit2Call = this.encodePermit2Call({
103
- chainId,
104
- owner,
105
- message: permit2.message,
106
- signature: permit2.signature,
107
- });
108
- items.push(permit2Call);
109
- }
110
- // Add enable collateral if flag is set
111
- if (enableCollateral) {
112
- items.push(this.encodeEnableCollateral(chainId, receiver, vault));
113
- }
114
- // Add mint operation
115
- items.push({
116
- targetContract: vault,
117
- onBehalfOfAccount: owner,
118
- value: 0n,
119
- data: encodeFunctionData({
120
- abi: eVaultAbi,
121
- functionName: "mint",
122
- args: [shares, receiver],
123
- }),
124
- });
125
- return items;
157
+ encodeMint({ chainId, ...args }) {
158
+ const { evc, permit2 } = this.getCoreAddresses(chainId);
159
+ return encodeHelpers.encodeMint(evc, permit2, { chainId, ...args });
126
160
  }
127
161
  /**
128
162
  * Encodes EVC batch items for withdrawing underlying assets from a vault (burns shares).
@@ -136,24 +170,9 @@ export class ExecutionService {
136
170
  * @param args.disableCollateral - If true, appends disableCollateral( owner, vault ) via EVC before withdraw
137
171
  * @returns Array of EVC batch items (optional disableCollateral, withdraw)
138
172
  */
139
- encodeWithdraw({ chainId, vault, assets, receiver, owner, disableCollateral, }) {
140
- const items = [];
141
- // Add disable collateral if flag is set
142
- if (disableCollateral) {
143
- items.push(this.encodeDisableCollateral(chainId, owner, vault));
144
- }
145
- // Add withdraw operation
146
- items.push({
147
- targetContract: vault,
148
- onBehalfOfAccount: owner,
149
- value: 0n,
150
- data: encodeFunctionData({
151
- abi: eVaultAbi,
152
- functionName: "withdraw",
153
- args: [assets, receiver, owner],
154
- }),
155
- });
156
- return items;
173
+ encodeWithdraw({ chainId, ...args }) {
174
+ const { evc } = this.getCoreAddresses(chainId);
175
+ return encodeHelpers.encodeWithdraw(evc, { chainId, ...args });
157
176
  }
158
177
  /**
159
178
  * Encodes EVC batch items for redeeming vault shares for underlying assets.
@@ -167,24 +186,9 @@ export class ExecutionService {
167
186
  * @param args.disableCollateral - If true, prepends disableCollateral( owner, vault ) via EVC
168
187
  * @returns Array of EVC batch items (optional disableCollateral, redeem)
169
188
  */
170
- encodeRedeem({ chainId, vault, shares, receiver, owner, disableCollateral, }) {
171
- const items = [];
172
- // Add disable collateral if flag is set
173
- if (disableCollateral) {
174
- items.push(this.encodeDisableCollateral(chainId, owner, vault));
175
- }
176
- // Add redeem operation
177
- items.push({
178
- targetContract: vault,
179
- onBehalfOfAccount: owner,
180
- value: 0n,
181
- data: encodeFunctionData({
182
- abi: eVaultAbi,
183
- functionName: "redeem",
184
- args: [shares, receiver, owner],
185
- }),
186
- });
187
- return items;
189
+ encodeRedeem({ chainId, ...args }) {
190
+ const { evc } = this.getCoreAddresses(chainId);
191
+ return encodeHelpers.encodeRedeem(evc, { chainId, ...args });
188
192
  }
189
193
  /**
190
194
  * Encodes EVC batch items for borrowing from a liability vault, optionally depositing collateral in the same batch.
@@ -205,42 +209,8 @@ export class ExecutionService {
205
209
  * @returns Array of EVC batch items (optional deposit, optional disableController, optional enableController, borrow)
206
210
  */
207
211
  encodeBorrow(args) {
208
- const { chainId, vault, amount, owner, borrowAccount, receiver, enableController = true, currentController, collateralVault, collateralAmount, enableCollateral = true, collateralPermit2, } = args;
209
- const items = [];
210
- // Add collateral if provided
211
- if (collateralVault &&
212
- collateralAmount !== undefined &&
213
- collateralAmount > 0n) {
214
- const depositItems = this.encodeDeposit({
215
- chainId,
216
- vault: collateralVault,
217
- amount: collateralAmount,
218
- receiver: borrowAccount,
219
- enableCollateral,
220
- permit2: collateralPermit2,
221
- owner: owner,
222
- });
223
- items.push(...depositItems);
224
- }
225
- // Add disable controller if there's a different controller enabled
226
- if (currentController && currentController !== vault) {
227
- items.push(this.encodeDisableController(currentController, borrowAccount));
228
- }
229
- if (enableController) {
230
- items.push(this.encodeEnableController(chainId, borrowAccount, vault));
231
- }
232
- // Add borrow operation
233
- items.push({
234
- targetContract: vault,
235
- onBehalfOfAccount: borrowAccount,
236
- value: 0n,
237
- data: encodeFunctionData({
238
- abi: eVaultAbi,
239
- functionName: "borrow",
240
- args: [amount, receiver],
241
- }),
242
- });
243
- return items;
212
+ const { evc, permit2 } = this.getCoreAddresses(args.chainId);
213
+ return encodeHelpers.encodeBorrow(evc, permit2, args);
244
214
  }
245
215
  /**
246
216
  * Encodes EVC batch items for liquidating an undercollateralized account (repay debt, seize collateral).
@@ -257,28 +227,9 @@ export class ExecutionService {
257
227
  * @param args.enableCollateral - If true, enables collateral vault for liquidator sub-account after liquidate (default true)
258
228
  * @returns Array of EVC batch items (optional enableController, liquidate, optional enableCollateral)
259
229
  */
260
- encodeLiquidation({ chainId, vault, violator, collateral, repayAssets, minYieldBalance, liquidatorSubAccountAddress, enableCollateral = true, enableController = true, }) {
261
- const items = [];
262
- // Optionally enable controller for the liquidator account on the liability vault
263
- if (enableController) {
264
- items.push(this.encodeEnableController(chainId, liquidatorSubAccountAddress, vault));
265
- }
266
- // Perform the liquidation
267
- items.push({
268
- targetContract: vault,
269
- onBehalfOfAccount: liquidatorSubAccountAddress,
270
- value: 0n,
271
- data: encodeFunctionData({
272
- abi: eVaultAbi,
273
- functionName: "liquidate",
274
- args: [violator, collateral, repayAssets, minYieldBalance],
275
- }),
276
- });
277
- // Optionally enable collateral for the seized collateral vault on the liquidator account
278
- if (enableCollateral) {
279
- items.push(this.encodeEnableCollateral(chainId, liquidatorSubAccountAddress, collateral));
280
- }
281
- return items;
230
+ encodeLiquidation({ chainId, ...args }) {
231
+ const { evc } = this.getCoreAddresses(chainId);
232
+ return encodeHelpers.encodeLiquidation(evc, { chainId, ...args });
282
233
  }
283
234
  /**
284
235
  * Encodes EVC batch items for pulling debt from one sub-account to another on the same liability vault.
@@ -292,24 +243,9 @@ export class ExecutionService {
292
243
  * @param args.enableController - If true, enables vault as controller for `to` via EVC before pullDebt (default true)
293
244
  * @returns Array of EVC batch items (optional enableController, pullDebt)
294
245
  */
295
- encodePullDebt({ chainId, vault, amount, from, to, enableController = true, }) {
296
- const items = [];
297
- // Add enable controller if flag is set
298
- if (enableController) {
299
- items.push(this.encodeEnableController(chainId, to, vault));
300
- }
301
- // Add pullDebt operation
302
- items.push({
303
- targetContract: vault,
304
- onBehalfOfAccount: to,
305
- value: 0n,
306
- data: encodeFunctionData({
307
- abi: eVaultAbi,
308
- functionName: "pullDebt",
309
- args: [amount, from],
310
- }),
311
- });
312
- return items;
246
+ encodePullDebt({ chainId, ...args }) {
247
+ const { evc } = this.getCoreAddresses(chainId);
248
+ return encodeHelpers.encodePullDebt(evc, { chainId, ...args });
313
249
  }
314
250
  /**
315
251
  * Encodes EVC batch items for a multiply/leverage operation when liability and long asset differ (swap required).
@@ -333,77 +269,8 @@ export class ExecutionService {
333
269
  * @returns Array of EVC batch items
334
270
  */
335
271
  encodeMultiplyWithSwap(args) {
336
- const { chainId, collateralVault, collateralAmount, liabilityVault, liabilityAmount, longVault, owner, receiver, enableCollateral = true, enableCollateralLong = true, currentController, enableController = true, collateralPermit2, swapQuote, } = args;
337
- const items = [];
338
- // 1. Add permit2 for collateral if provided
339
- if (collateralPermit2) {
340
- const permit2Call = this.encodePermit2Call({
341
- chainId,
342
- owner,
343
- message: collateralPermit2.message,
344
- signature: collateralPermit2.signature,
345
- });
346
- items.push(permit2Call);
347
- }
348
- // 2. Deposit initial collateral if amount > 0
349
- if (collateralAmount > 0n) {
350
- // Enable collateral for collateral vault
351
- if (enableCollateral) {
352
- items.push(this.encodeEnableCollateral(chainId, receiver, collateralVault));
353
- }
354
- // Deposit collateral
355
- items.push({
356
- targetContract: collateralVault,
357
- onBehalfOfAccount: owner,
358
- value: 0n,
359
- data: encodeFunctionData({
360
- abi: eVaultAbi,
361
- functionName: "deposit",
362
- args: [collateralAmount, receiver],
363
- }),
364
- });
365
- }
366
- // 3. Disable current controller if there's a different one enabled
367
- if (currentController && currentController !== liabilityVault) {
368
- items.push(this.encodeDisableController(currentController, receiver));
369
- }
370
- // 4. Enable controller for liability vault
371
- if (enableController) {
372
- items.push(this.encodeEnableController(chainId, receiver, liabilityVault));
373
- }
374
- // 5. Borrow from liability vault to swapper
375
- items.push({
376
- targetContract: liabilityVault,
377
- onBehalfOfAccount: receiver,
378
- value: 0n,
379
- data: encodeFunctionData({
380
- abi: eVaultAbi,
381
- functionName: "borrow",
382
- args: [liabilityAmount, swapQuote.swap.swapperAddress],
383
- }),
384
- });
385
- // 6. Execute swap multicall
386
- items.push({
387
- targetContract: swapQuote.swap.swapperAddress,
388
- onBehalfOfAccount: receiver,
389
- value: 0n,
390
- data: swapQuote.swap.swapperData,
391
- });
392
- // 7. Verify swap and skim to long vault
393
- if (swapQuote.verify.type !== "skimMin") {
394
- throw new Error("Invalid swap quote type for multiply - must be skimMin");
395
- }
396
- items.push({
397
- targetContract: swapQuote.verify.verifierAddress,
398
- onBehalfOfAccount: receiver,
399
- value: 0n,
400
- data: swapQuote.verify.verifierData,
401
- });
402
- // 8. Enable collateral on long vault
403
- if (enableCollateralLong && collateralVault !== longVault) {
404
- items.push(this.encodeEnableCollateral(chainId, receiver, longVault));
405
- }
406
- return items;
272
+ const { evc, permit2 } = this.getCoreAddresses(args.chainId);
273
+ return encodeHelpers.encodeMultiplyWithSwap(evc, permit2, args);
407
274
  }
408
275
  /**
409
276
  * Encodes EVC batch items for a multiply/leverage operation when liability and long asset are the same (no swap).
@@ -426,71 +293,8 @@ export class ExecutionService {
426
293
  * @returns Array of EVC batch items
427
294
  */
428
295
  encodeMultiplySameAsset(args) {
429
- const { chainId, collateralVault, collateralAmount, liabilityVault, liabilityAmount, longVault, owner, receiver, enableCollateral = true, enableCollateralLong = true, enableController = true, currentController, collateralPermit2, } = args;
430
- const items = [];
431
- // 1. Add permit2 for collateral if provided
432
- if (collateralPermit2) {
433
- const permit2Call = this.encodePermit2Call({
434
- chainId,
435
- owner,
436
- message: collateralPermit2.message,
437
- signature: collateralPermit2.signature,
438
- });
439
- items.push(permit2Call);
440
- }
441
- // 2. Deposit initial collateral if amount > 0
442
- if (collateralAmount > 0n) {
443
- // Enable collateral for collateral vault
444
- if (enableCollateral) {
445
- items.push(this.encodeEnableCollateral(chainId, receiver, collateralVault));
446
- }
447
- // Deposit collateral
448
- items.push({
449
- targetContract: collateralVault,
450
- onBehalfOfAccount: owner,
451
- value: 0n,
452
- data: encodeFunctionData({
453
- abi: eVaultAbi,
454
- functionName: "deposit",
455
- args: [collateralAmount, receiver],
456
- }),
457
- });
458
- }
459
- // 3. Disable current controller if there's a different one enabled
460
- if (currentController && currentController !== liabilityVault) {
461
- items.push(this.encodeDisableController(currentController, receiver));
462
- }
463
- // 4. Enable controller for liability vault
464
- if (enableController) {
465
- items.push(this.encodeEnableController(chainId, receiver, liabilityVault));
466
- }
467
- // 5. Borrow from liability vault directly to long vault
468
- items.push({
469
- targetContract: liabilityVault,
470
- onBehalfOfAccount: receiver,
471
- value: 0n,
472
- data: encodeFunctionData({
473
- abi: eVaultAbi,
474
- functionName: "borrow",
475
- args: [liabilityAmount, longVault],
476
- }),
477
- });
478
- // 6. Skim borrowed assets to position
479
- items.push({
480
- targetContract: longVault,
481
- onBehalfOfAccount: receiver,
482
- value: 0n,
483
- data: encodeFunctionData({
484
- abi: eVaultAbi,
485
- functionName: "skim",
486
- args: [liabilityAmount, receiver],
487
- }),
488
- });
489
- // 7. Enable collateral on long vault
490
- if (enableCollateralLong) {
491
- items.push(this.encodeEnableCollateral(chainId, receiver, longVault));
492
- }
493
- return items;
296
+ const { evc, permit2 } = this.getCoreAddresses(args.chainId);
297
+ return encodeHelpers.encodeMultiplySameAsset(evc, permit2, args);
494
298
  }
495
299
  /**
496
300
  * Encodes EVC batch items for repaying debt using assets from the sender's wallet (transferFrom to vault then repay).
@@ -507,41 +311,16 @@ export class ExecutionService {
507
311
  * @returns Array of EVC batch items (optional permit2, repay, optional disableController)
508
312
  */
509
313
  encodeRepayFromWallet(args) {
510
- const { chainId, sender, liabilityVault, liabilityAmount, receiver, disableControllerOnMax = true, isMax = false, permit2, } = args;
511
- const items = [];
512
- if (permit2) {
513
- const permit2Call = this.encodePermit2Call({
514
- chainId,
515
- owner: sender,
516
- message: permit2.message,
517
- signature: permit2.signature,
518
- });
519
- items.push(permit2Call);
520
- }
521
- // Repay operation
522
- items.push({
523
- targetContract: liabilityVault,
524
- onBehalfOfAccount: sender,
525
- value: 0n,
526
- data: encodeFunctionData({
527
- abi: eVaultAbi,
528
- functionName: "repay",
529
- args: [isMax ? maxUint256 : liabilityAmount, receiver],
530
- }),
531
- });
532
- // Disable controller if needed (for max repay)
533
- // Sender must be allowed to act on behalf of receiver (sender is subaccount of receiver or is an operator)
534
- if (disableControllerOnMax && isMax) {
535
- items.push(this.encodeDisableController(liabilityVault, receiver));
536
- }
537
- return items;
314
+ const { permit2 } = this.getCoreAddresses(args.chainId);
315
+ return encodeHelpers.encodeRepayFromWallet(permit2, args);
538
316
  }
539
317
  /**
540
318
  * Encodes EVC batch items for repaying debt from a deposit (same-asset only).
541
- * Path 1: same asset and same vault → repayWithShares. Path 2: same asset, different vault → withdraw then repay/skim/repayWithShares as needed.
319
+ * Path 1: same asset and same vault → repayWithShares.
320
+ * Path 2: same asset, different vault → withdraw to liability vault, skim, then repayWithShares.
542
321
  *
543
322
  * @param args - Repay-from-deposit encoding arguments
544
- * @param args.chainId - Chain ID (used for EVC and optional permit2)
323
+ * @param args.chainId - Chain ID used for EVC controller calls
545
324
  * @param args.liabilityVault - Vault (liability) to repay debt to
546
325
  * @param args.liabilityAsset - Underlying asset address of the liability vault
547
326
  * @param args.liabilityAmount - Amount of liability to repay (maxUint256 with isMax for full repay)
@@ -554,31 +333,7 @@ export class ExecutionService {
554
333
  * @returns Array of EVC batch items. Throws if fromAsset !== liabilityAsset.
555
334
  */
556
335
  encodeRepayFromDeposit(args) {
557
- const { chainId, liabilityVault, liabilityAsset, liabilityAmount, from, receiver, fromVault, fromAsset, disableControllerOnMax = true, isMax = false, } = args;
558
- // PATH 1: Same asset, same vault - use repayWithShares
559
- if (fromAsset === liabilityAsset && fromVault === liabilityVault) {
560
- return this.encodeRepayWithSharesSameAssetAndVault({
561
- chainId,
562
- vault: liabilityVault,
563
- amount: liabilityAmount,
564
- receiver,
565
- from,
566
- disableController: isMax && disableControllerOnMax,
567
- });
568
- }
569
- // PATH 2: Same asset, different vault
570
- if (fromAsset === liabilityAsset) {
571
- return this.encodeRepayWithSharesSameAssetDifferentVault({
572
- fromVault,
573
- toVault: liabilityVault,
574
- amount: liabilityAmount,
575
- receiver,
576
- from,
577
- isMax,
578
- disableControllerOnMax,
579
- });
580
- }
581
- throw new Error("encodeRepayFromDeposit only supports same-asset paths");
336
+ return encodeHelpers.encodeRepayFromDeposit(args);
582
337
  }
583
338
  /**
584
339
  * Encodes EVC batch items for repaying debt by swapping collateral (withdraw from vaultIn → swap → verify/repay debtMax).
@@ -593,50 +348,7 @@ export class ExecutionService {
593
348
  * @returns Array of EVC batch items (withdraw, swap, verify/repay, optional disableController)
594
349
  */
595
350
  encodeRepayWithSwap(args) {
596
- const { swapQuote, maxWithdraw, isMax = false, disableControllerOnMax = true, } = args;
597
- const items = [];
598
- // Determine withdraw amount (cap to available amount if provided)
599
- const withdrawAmount = maxWithdraw &&
600
- maxWithdraw < BigInt(swapQuote.amountInMax || swapQuote.amountIn)
601
- ? maxWithdraw
602
- : BigInt(swapQuote.amountInMax || swapQuote.amountIn);
603
- // 1. Withdraw collateral from vault to swapper
604
- items.push({
605
- targetContract: swapQuote.vaultIn,
606
- onBehalfOfAccount: swapQuote.accountIn,
607
- value: 0n,
608
- data: encodeFunctionData({
609
- abi: eVaultAbi,
610
- functionName: "withdraw",
611
- args: [
612
- withdrawAmount,
613
- swapQuote.swap.swapperAddress,
614
- swapQuote.accountIn,
615
- ],
616
- }),
617
- });
618
- // 2. Execute swap multicall
619
- items.push({
620
- targetContract: swapQuote.swap.swapperAddress,
621
- onBehalfOfAccount: swapQuote.accountIn,
622
- value: 0n,
623
- data: swapQuote.swap.swapperData,
624
- });
625
- // 3. Verify swap and repay (verifyDebtMax handles the repay)
626
- if (swapQuote.verify.type !== "debtMax") {
627
- throw new Error("Invalid swap quote type for repay - must be debtMax");
628
- }
629
- items.push({
630
- targetContract: swapQuote.verify.verifierAddress,
631
- onBehalfOfAccount: swapQuote.verify.account,
632
- value: 0n,
633
- data: swapQuote.verify.verifierData,
634
- });
635
- // 4. Disable controller if needed (for max repay)
636
- if (isMax && disableControllerOnMax) {
637
- items.push(this.encodeDisableController(swapQuote.receiver, swapQuote.accountOut));
638
- }
639
- return items;
351
+ return encodeHelpers.encodeRepayWithSwap(args);
640
352
  }
641
353
  /**
642
354
  * Encodes EVC batch items for depositing into a vault using tokens from the user's wallet, going through a swap.
@@ -651,42 +363,8 @@ export class ExecutionService {
651
363
  * @returns Array of EVC batch items (transferFromSender, swap, verify, optional enableCollateral)
652
364
  */
653
365
  encodeDepositWithSwapFromWallet(args) {
654
- const { chainId, swapQuote, amount, sender, enableCollateral = true, } = args;
655
- const items = [];
656
- // 1. Transfer tokens from sender's wallet to swapper via SwapVerifier.transferFromSender
657
- items.push({
658
- targetContract: swapQuote.verify.verifierAddress,
659
- onBehalfOfAccount: sender,
660
- value: 0n,
661
- data: encodeFunctionData({
662
- abi: swapVerifierAbi,
663
- functionName: "transferFromSender",
664
- args: [
665
- swapQuote.tokenIn.address,
666
- amount,
667
- swapQuote.swap.swapperAddress,
668
- ],
669
- }),
670
- });
671
- // 2. Execute swap multicall
672
- items.push({
673
- targetContract: swapQuote.swap.swapperAddress,
674
- onBehalfOfAccount: sender,
675
- value: 0n,
676
- data: swapQuote.swap.swapperData,
677
- });
678
- // 3. Verify swap
679
- items.push({
680
- targetContract: swapQuote.verify.verifierAddress,
681
- onBehalfOfAccount: swapQuote.accountOut || sender,
682
- value: 0n,
683
- data: swapQuote.verify.verifierData,
684
- });
685
- // 4. Enable collateral if needed
686
- if (enableCollateral && swapQuote.receiver) {
687
- items.push(this.encodeEnableCollateral(chainId, swapQuote.accountOut || sender, swapQuote.receiver));
688
- }
689
- return items;
366
+ const { evc } = this.getCoreAddresses(args.chainId);
367
+ return encodeHelpers.encodeDepositWithSwapFromWallet(evc, args);
690
368
  }
691
369
  /**
692
370
  * Encodes EVC batch items for swapping a token from the sender's wallet into another token
@@ -702,38 +380,7 @@ export class ExecutionService {
702
380
  * @returns Array of EVC batch items (transferFromSender, swap, verify)
703
381
  */
704
382
  encodeSwapFromWallet(args) {
705
- const { swapQuote, amount, sender } = args;
706
- const items = [];
707
- if (swapQuote.verify.type !== "transferMin") {
708
- throw new Error("Invalid swap quote type for wallet swap - must be transferMin");
709
- }
710
- items.push({
711
- targetContract: swapQuote.verify.verifierAddress,
712
- onBehalfOfAccount: sender,
713
- value: 0n,
714
- data: encodeFunctionData({
715
- abi: swapVerifierAbi,
716
- functionName: "transferFromSender",
717
- args: [
718
- swapQuote.tokenIn.address,
719
- amount,
720
- swapQuote.swap.swapperAddress,
721
- ],
722
- }),
723
- });
724
- items.push({
725
- targetContract: swapQuote.swap.swapperAddress,
726
- onBehalfOfAccount: sender,
727
- value: 0n,
728
- data: swapQuote.swap.swapperData,
729
- });
730
- items.push({
731
- targetContract: swapQuote.verify.verifierAddress,
732
- onBehalfOfAccount: sender,
733
- value: 0n,
734
- data: swapQuote.verify.verifierData,
735
- });
736
- return items;
383
+ return encodeHelpers.encodeSwapFromWallet(args);
737
384
  }
738
385
  /**
739
386
  * Encodes EVC batch items for swapping collateral: withdraw from vaultIn → swap → verify/skim to receiver; optional enable/disable collateral.
@@ -748,50 +395,8 @@ export class ExecutionService {
748
395
  * @returns Array of EVC batch items (withdraw, swap, verify/skim, optional disableCollateral, optional enableCollateral)
749
396
  */
750
397
  encodeSwapCollateral(args) {
751
- const { chainId, swapQuote, enableCollateral = true, disableCollateralOnMax = true, isMax = false, } = args;
752
- const items = [];
753
- // 1. Withdraw from source vault to swapper
754
- const withdrawAmount = BigInt(swapQuote.amountInMax || swapQuote.amountIn);
755
- items.push({
756
- targetContract: swapQuote.vaultIn,
757
- onBehalfOfAccount: swapQuote.accountIn,
758
- value: 0n,
759
- data: encodeFunctionData({
760
- abi: eVaultAbi,
761
- functionName: "withdraw",
762
- args: [
763
- withdrawAmount,
764
- swapQuote.swap.swapperAddress,
765
- swapQuote.accountIn,
766
- ],
767
- }),
768
- });
769
- // 2. Execute swap multicall
770
- items.push({
771
- targetContract: swapQuote.swap.swapperAddress,
772
- onBehalfOfAccount: swapQuote.accountIn,
773
- value: 0n,
774
- data: swapQuote.swap.swapperData,
775
- });
776
- // 3. Verify swap and skim
777
- if (swapQuote.verify.type !== "skimMin") {
778
- throw new Error("Invalid swap quote type for swap collateral - must be skimMin");
779
- }
780
- items.push({
781
- targetContract: swapQuote.verify.verifierAddress,
782
- onBehalfOfAccount: swapQuote.accountOut,
783
- value: 0n,
784
- data: swapQuote.verify.verifierData,
785
- });
786
- // 4. Disable collateral if needed (for max swap)
787
- if (isMax && disableCollateralOnMax) {
788
- items.push(this.encodeDisableCollateral(chainId, swapQuote.accountIn, swapQuote.vaultIn));
789
- }
790
- // 5. Enable collateral if needed
791
- if (enableCollateral) {
792
- items.push(this.encodeEnableCollateral(chainId, swapQuote.accountOut, swapQuote.receiver));
793
- }
794
- return items;
398
+ const { evc } = this.getCoreAddresses(args.chainId);
399
+ return encodeHelpers.encodeSwapCollateral(evc, args);
795
400
  }
796
401
  /**
797
402
  * Encodes EVC batch items for swapping debt: enableController → borrow from vaultIn → swap → verify/repay (debtMax).
@@ -805,46 +410,51 @@ export class ExecutionService {
805
410
  * @param args.isMax - If true, treats as full debt swap (can trigger disableControllerOnMax)
806
411
  * @returns Array of EVC batch items (optional enableController, borrow, swap, verify/repay, optional disableController)
807
412
  */
808
- encodeSwapDebt({ chainId, swapQuote, enableController = true, disableControllerOnMax = true, isMax = false, }) {
809
- const items = [];
810
- // Enable controller if needed
811
- if (enableController) {
812
- items.push(this.encodeEnableController(chainId, swapQuote.accountOut, swapQuote.vaultIn));
813
- }
814
- // Borrow from source vault
815
- const borrowAmount = BigInt(swapQuote.amountInMax);
816
- items.push({
817
- targetContract: swapQuote.vaultIn,
818
- onBehalfOfAccount: swapQuote.accountIn,
819
- value: 0n,
820
- data: encodeFunctionData({
821
- abi: eVaultAbi,
822
- functionName: "borrow",
823
- args: [borrowAmount, swapQuote.swap.swapperAddress],
824
- }),
825
- });
826
- // Execute swap multicall
827
- items.push({
828
- targetContract: swapQuote.swap.swapperAddress,
829
- onBehalfOfAccount: swapQuote.accountIn,
830
- value: 0n,
831
- data: swapQuote.swap.swapperData,
832
- });
833
- // Verify swap and skim
834
- if (swapQuote.verify.type !== "debtMax") {
835
- throw new Error("Invalid swap quote type for repay - must be debtMax");
836
- }
837
- items.push({
838
- targetContract: swapQuote.verify.verifierAddress,
839
- onBehalfOfAccount: swapQuote.accountOut,
840
- value: 0n,
841
- data: swapQuote.verify.verifierData,
842
- });
843
- // Disable controller if needed (for max swap)
844
- if (isMax && disableControllerOnMax) {
845
- items.push(this.encodeDisableController(swapQuote.receiver, swapQuote.accountIn));
846
- }
847
- return items;
413
+ encodeSwapDebt(args) {
414
+ const { evc } = this.getCoreAddresses(args.chainId);
415
+ return encodeHelpers.encodeSwapDebt(evc, args);
416
+ }
417
+ /**
418
+ * Encodes EVC batch items for migrating a supplied/collateral position between two same-asset vaults.
419
+ * Partial migration uses withdraw(amount, toVault, account) then skim(amount, account).
420
+ * Max migration uses redeem(maxShares || maxUint256, toVault, account) then skim(amount, account).
421
+ *
422
+ * @param args - Same-asset collateral migration encoding arguments
423
+ * @param args.chainId - Chain ID (used for EVC enable/disable collateral)
424
+ * @param args.fromVault - Source vault holding the supplied shares
425
+ * @param args.toVault - Destination vault with the same underlying asset
426
+ * @param args.amount - Asset amount expected to arrive at the destination vault and be skimmed
427
+ * @param args.account - Sub-account that owns the source shares and receives destination shares
428
+ * @param args.isMax - If true, redeems shares instead of withdrawing assets
429
+ * @param args.maxShares - Optional exact share amount for max migration; defaults to maxUint256
430
+ * @param args.enableCollateralTo - If true, enables the destination vault as collateral after skim
431
+ * @param args.disableCollateralFrom - If true, disables the source vault as collateral after enabling the destination
432
+ * @returns Array of EVC batch items
433
+ */
434
+ encodeMigrateSameAssetCollateral(args) {
435
+ const { evc } = this.getCoreAddresses(args.chainId);
436
+ return encodeHelpers.encodeMigrateSameAssetCollateral(evc, args);
437
+ }
438
+ /**
439
+ * Encodes EVC batch items for migrating a full debt position between two same-asset liability vaults.
440
+ * Flow: enable new controller → borrow with interest cushion to old vault → skim → repay old debt with shares
441
+ * → disable old controller → optionally sweep cushion to the new vault → optionally transfer remaining new-vault shares.
442
+ *
443
+ * @param args - Same-asset debt migration encoding arguments
444
+ * @param args.chainId - Chain ID (used for EVC enable controller)
445
+ * @param args.oldLiabilityVault - Existing debt vault to fully repay and disable
446
+ * @param args.newLiabilityVault - New same-asset debt vault to borrow from
447
+ * @param args.amount - Current debt amount before applying the 0.01% interest cushion
448
+ * @param args.account - Sub-account that owns the debt position
449
+ * @param args.enableController - If true, enables the new liability vault as controller first
450
+ * @param args.disableController - If true, disables the old liability vault after repayment
451
+ * @param args.sweepExcess - If true, redeems any old-vault cushion shares back to the new vault and skims them
452
+ * @param args.transferRemainingSharesTo - If set, transfers all new-vault shares from the sub-account to this address
453
+ * @returns Array of EVC batch items
454
+ */
455
+ encodeMigrateSameAssetDebt(args) {
456
+ const { evc } = this.getCoreAddresses(args.chainId);
457
+ return encodeHelpers.encodeMigrateSameAssetDebt(evc, args);
848
458
  }
849
459
  /**
850
460
  * Encodes EVC batch items for transferring vault shares between sub-accounts.
@@ -859,28 +469,9 @@ export class ExecutionService {
859
469
  * @param args.disableCollateralFrom - If true, prepends disableCollateral( from, vault ) via EVC before transfer
860
470
  * @returns Array of EVC batch items (optional disableCollateralFrom, transfer, optional enableCollateralTo)
861
471
  */
862
- encodeTransfer({ chainId, vault, to, amount, from, enableCollateralTo, disableCollateralFrom, }) {
863
- const items = [];
864
- // Add disable collateral from sender if flag is set
865
- if (disableCollateralFrom) {
866
- items.push(this.encodeDisableCollateral(chainId, from, vault));
867
- }
868
- // Add transfer operation
869
- items.push({
870
- targetContract: vault,
871
- onBehalfOfAccount: from,
872
- value: 0n,
873
- data: encodeFunctionData({
874
- abi: eVaultAbi,
875
- functionName: "transfer",
876
- args: [to, amount],
877
- }),
878
- });
879
- // Add enable collateral to receiver if flag is set
880
- if (enableCollateralTo) {
881
- items.push(this.encodeEnableCollateral(chainId, to, vault));
882
- }
883
- return items;
472
+ encodeTransfer({ chainId, ...args }) {
473
+ const { evc } = this.getCoreAddresses(chainId);
474
+ return encodeHelpers.encodeTransfer(evc, { chainId, ...args });
884
475
  }
885
476
  /**
886
477
  * Encodes a single EVC batch item that calls Permit2's `permit` with the given message and signature.
@@ -894,73 +485,23 @@ export class ExecutionService {
894
485
  * @returns Single EVC batch item (targetContract = Permit2, permit call)
895
486
  */
896
487
  encodePermit2Call(args) {
897
- const { chainId, owner, message, signature } = args;
898
- const deployment = this.deploymentService.getDeployment(chainId);
899
- const permit2 = deployment.addresses.coreAddrs.permit2;
900
- return {
901
- targetContract: permit2,
902
- onBehalfOfAccount: owner,
903
- value: 0n,
904
- data: encodeFunctionData({
905
- abi: permit2PermitAbi,
906
- functionName: "permit",
907
- args: [owner, message, signature],
908
- }),
909
- };
488
+ const { permit2 } = this.getCoreAddresses(args.chainId);
489
+ return encodeHelpers.encodePermit2Call(permit2, args);
910
490
  }
911
491
  encodeEnableCollateral(chainId, account, vault) {
912
- const deployment = this.deploymentService.getDeployment(chainId);
913
- const evc = deployment.addresses.coreAddrs.evc;
914
- return {
915
- targetContract: evc,
916
- onBehalfOfAccount: zeroAddress,
917
- value: 0n,
918
- data: encodeFunctionData({
919
- abi: ethereumVaultConnectorAbi,
920
- functionName: "enableCollateral",
921
- args: [account, vault],
922
- }),
923
- };
492
+ const { evc } = this.getCoreAddresses(chainId);
493
+ return encodeHelpers.encodeEnableCollateral(evc, account, vault);
924
494
  }
925
495
  encodeDisableCollateral(chainId, account, vault) {
926
- const deployment = this.deploymentService.getDeployment(chainId);
927
- const evc = deployment.addresses.coreAddrs.evc;
928
- return {
929
- targetContract: evc,
930
- onBehalfOfAccount: zeroAddress,
931
- value: 0n,
932
- data: encodeFunctionData({
933
- abi: ethereumVaultConnectorAbi,
934
- functionName: "disableCollateral",
935
- args: [account, vault],
936
- }),
937
- };
496
+ const { evc } = this.getCoreAddresses(chainId);
497
+ return encodeHelpers.encodeDisableCollateral(evc, account, vault);
938
498
  }
939
499
  encodeEnableController(chainId, account, vault) {
940
- const deployment = this.deploymentService.getDeployment(chainId);
941
- const evc = deployment.addresses.coreAddrs.evc;
942
- return {
943
- targetContract: evc,
944
- onBehalfOfAccount: zeroAddress,
945
- value: 0n,
946
- data: encodeFunctionData({
947
- abi: ethereumVaultConnectorAbi,
948
- functionName: "enableController",
949
- args: [account, vault],
950
- }),
951
- };
500
+ const { evc } = this.getCoreAddresses(chainId);
501
+ return encodeHelpers.encodeEnableController(evc, account, vault);
952
502
  }
953
503
  encodeDisableController(vault, account) {
954
- return {
955
- targetContract: vault,
956
- onBehalfOfAccount: account,
957
- value: 0n,
958
- data: encodeFunctionData({
959
- abi: eVaultAbi,
960
- functionName: "disableController",
961
- args: [],
962
- }),
963
- };
504
+ return encodeHelpers.encodeDisableController(vault, account);
964
505
  }
965
506
  /**
966
507
  * Builds EIP-712 typed data for a Permit2 PermitSingle signature (token approval for a spender).
@@ -977,46 +518,10 @@ export class ExecutionService {
977
518
  * @returns EIP-712 typed data (domain, types, primaryType, message) for signing
978
519
  */
979
520
  getPermit2TypedData(args) {
980
- const nowInSeconds = () => BigInt(Math.floor(Date.now() / 1000));
981
- const { chainId, token, amount, spender, nonce, sigDeadline, expiration } = args;
982
- const deployment = this.deploymentService.getDeployment(chainId);
983
- const permit2 = deployment.addresses.coreAddrs.permit2;
984
- const permitSingle = {
985
- details: {
986
- token,
987
- amount: amount > maxUint160 ? maxUint160 : amount,
988
- expiration: expiration ?? Number(maxUint48),
989
- nonce,
990
- },
991
- spender,
992
- sigDeadline: sigDeadline ?? nowInSeconds() + PERMIT2_SIG_WINDOW,
993
- };
994
- return {
995
- domain: {
996
- name: "Permit2",
997
- chainId,
998
- verifyingContract: permit2,
999
- },
1000
- types: PERMIT2_TYPES,
1001
- primaryType: "PermitSingle",
1002
- message: permitSingle,
1003
- };
521
+ const { permit2 } = this.getCoreAddresses(args.chainId);
522
+ return encodeHelpers.getPermit2TypedData(permit2, args);
1004
523
  }
1005
- /**
1006
- * Decodes EVC batch items into human-readable function names and named arguments.
1007
- * Tries known ABIs (EVC, eVault, Permit2, swapper, swapVerifier) to decode each item's data.
1008
- *
1009
- * @param batch - Array of EVC batch items (targetContract, onBehalfOfAccount, value, data) to decode
1010
- * @param extraAbis - Optional extra ABIs to try first when decoding unknown batch items.
1011
- * @returns Array of decoded items with targetContract, onBehalfOfAccount, functionName, and args (record of param name to value). Throws if any item cannot be decoded.
1012
- * @example
1013
- * const batchItems = executionService.encodeDeposit({ ... });
1014
- * const described = executionService.describeBatch(batchItems);
1015
- * console.log(described[0].functionName); // "deposit"
1016
- * console.log(described[0].args); // { amount: 1000n, receiver: "0x..." }
1017
- */
1018
524
  describeBatch(batch, extraAbis) {
1019
- const decodedBatchItems = [];
1020
525
  const executionDecodeAbis = [
1021
526
  ...(extraAbis ?? []),
1022
527
  ethereumVaultConnectorAbi,
@@ -1025,7 +530,7 @@ export class ExecutionService {
1025
530
  swapperAbi,
1026
531
  swapVerifierAbi,
1027
532
  ];
1028
- for (const item of batch) {
533
+ const decodeBatchItem = (item) => {
1029
534
  let decoded = false;
1030
535
  for (const abi of executionDecodeAbis) {
1031
536
  try {
@@ -1050,14 +555,13 @@ export class ExecutionService {
1050
555
  }
1051
556
  });
1052
557
  }
1053
- decodedBatchItems.push({
558
+ decoded = true;
559
+ return {
1054
560
  targetContract: item.targetContract,
1055
561
  onBehalfOfAccount: item.onBehalfOfAccount,
1056
562
  functionName: decodedData.functionName,
1057
563
  args: namedArgs,
1058
- });
1059
- decoded = true;
1060
- break;
564
+ };
1061
565
  }
1062
566
  catch { }
1063
567
  }
@@ -1069,29 +573,35 @@ export class ExecutionService {
1069
573
  try {
1070
574
  const result = plugin.decodeBatchItem(item);
1071
575
  if (result) {
1072
- decodedBatchItems.push(result);
1073
576
  decoded = true;
1074
- break;
577
+ return result;
1075
578
  }
1076
579
  }
1077
580
  catch { }
1078
581
  }
1079
582
  }
1080
583
  if (!decoded) {
1081
- decodedBatchItems.push({
584
+ return {
1082
585
  targetContract: item.targetContract,
1083
586
  onBehalfOfAccount: item.onBehalfOfAccount,
1084
587
  functionName: "Unknown",
1085
588
  args: {},
1086
- });
589
+ };
1087
590
  }
1088
- }
1089
- return decodedBatchItems;
591
+ throw new Error("unreachable batch item decode state");
592
+ };
593
+ return batch.map((entry) => isEVCBatchOperation(entry)
594
+ ? {
595
+ type: "operation",
596
+ name: entry.name,
597
+ items: entry.items.map(decodeBatchItem),
598
+ }
599
+ : decodeBatchItem(entry));
1090
600
  }
1091
601
  /**
1092
602
  * Merges multiple transaction plans into a single plan.
1093
603
  * Required approvals for the same (token, owner, spender) are summed.
1094
- * Executable items are preserved in order; adjacent EVC batch items are concatenated.
604
+ * Executable items are preserved in order; adjacent EVC batches are concatenated without flattening operation groupings.
1095
605
  * Can be used to construct a transaction queue.
1096
606
  *
1097
607
  * @param plans - Array of transaction plans to merge
@@ -1117,19 +627,37 @@ export class ExecutionService {
1117
627
  else if (item.type === "evcBatch") {
1118
628
  const previous = executableItems[executableItems.length - 1];
1119
629
  if (previous?.type === "evcBatch") {
1120
- previous.items.push(...item.items);
630
+ previous.items.push(...cloneBatchEntries(item.items));
1121
631
  }
1122
632
  else {
1123
- executableItems.push({ type: "evcBatch", items: [...item.items] });
633
+ executableItems.push({
634
+ type: "evcBatch",
635
+ items: cloneBatchEntries(item.items),
636
+ });
1124
637
  }
1125
638
  }
1126
639
  else {
1127
- executableItems.push(item);
640
+ throw new Error("ExecutionService.mergePlans cannot merge contractCall plan items. Merge these plans manually to preserve call boundaries.");
1128
641
  }
1129
642
  }
1130
643
  }
1131
644
  return [...approvalByKey.values(), ...executableItems];
1132
645
  }
646
+ /**
647
+ * Appends a raw batch item to the last EVC batch in the plan, or creates one.
648
+ * Mutates and returns the provided plan.
649
+ */
650
+ addBatchItemToPlan(plan, item) {
651
+ for (let index = plan.length - 1; index >= 0; index--) {
652
+ const planItem = plan[index];
653
+ if (planItem?.type === "evcBatch") {
654
+ planItem.items.push(item);
655
+ return plan;
656
+ }
657
+ }
658
+ plan.push({ type: "evcBatch", items: [item] });
659
+ return plan;
660
+ }
1133
661
  /**
1134
662
  * Converts EVC batch items into a transaction plan.
1135
663
  * Returns a plan with a single evcBatch containing the given items (no required approvals).
@@ -1138,127 +666,50 @@ export class ExecutionService {
1138
666
  * @param items - EVC batch items to wrap in a plan
1139
667
  * @returns Transaction plan containing one evcBatch with the items
1140
668
  */
1141
- convertBatchItemsToPlan(items) {
669
+ convertBatchItemsToPlan(items, operationName) {
1142
670
  if (items.length === 0)
1143
671
  return [];
672
+ if (operationName) {
673
+ return [
674
+ {
675
+ type: "evcBatch",
676
+ items: [{ type: "operation", name: operationName, items }],
677
+ },
678
+ ];
679
+ }
1144
680
  return [{ type: "evcBatch", items }];
1145
681
  }
1146
- /**
1147
- * Encodes batch items for repaying with shares from the same asset and vault
1148
- */
1149
- encodeRepayWithSharesSameAssetAndVault({ chainId: _chainId, vault, amount, from, receiver, disableController, }) {
1150
- const items = [];
1151
- // Repay with shares
1152
- items.push({
1153
- targetContract: vault,
1154
- onBehalfOfAccount: from,
1155
- value: 0n,
1156
- data: encodeFunctionData({
1157
- abi: eVaultAbi,
1158
- functionName: "repayWithShares",
1159
- args: [amount, receiver],
1160
- }),
1161
- });
1162
- // Disable controller if needed (for max repay)
1163
- if (disableController) {
1164
- items.push(this.encodeDisableController(vault, receiver));
1165
- }
1166
- return items;
682
+ encodeTransferFromMax(vault, from, to) {
683
+ return encodeHelpers.encodeTransferFromMax(vault, from, to);
1167
684
  }
1168
685
  /**
1169
- * Encodes batch items for repaying with shares from same asset but different vault
686
+ * Appends post-full-repay cleanup calls owned by plan builders:
687
+ * disable each active collateral used by the repaid borrow, transfer those collateral shares to the owner,
688
+ * and, when a source deposit funded the repay, transfer any remaining source-vault shares to the owner.
1170
689
  */
1171
- encodeRepayWithSharesSameAssetDifferentVault({ fromVault, toVault, amount, // if isMax, this should be the total current debt
1172
- receiver, from, isMax, disableControllerOnMax, }) {
1173
- const items = [];
1174
- if (isMax) {
1175
- // if amount was max uint, skim and repay with shares would not revert if after withdraw funds were skimmed
1176
- // by other party
1177
- if (amount == maxUint256) {
1178
- throw new Error("Amount is maxUint256, cannot be used for max repay");
1179
- }
1180
- // For max repay: withdraw full debt amount +1 BPS to cover interest, then skim, then repayWithShares max
1181
- const amountWithExtra = (amount * 10001n) / 10000n;
1182
- if (amountWithExtra >= maxUint256) {
1183
- throw new Error("Amount with extra exceeds maxUint256");
1184
- }
1185
- // 1. Withdraw from collateral vault
1186
- items.push({
1187
- targetContract: fromVault,
1188
- onBehalfOfAccount: from,
1189
- value: 0n,
1190
- data: encodeFunctionData({
1191
- abi: eVaultAbi,
1192
- functionName: "withdraw",
1193
- args: [amountWithExtra, toVault, from],
1194
- }),
1195
- });
1196
- // 2. Skim exact withdrawal amount to liability vault
1197
- items.push({
1198
- targetContract: toVault,
1199
- onBehalfOfAccount: receiver,
1200
- value: 0n,
1201
- data: encodeFunctionData({
1202
- abi: eVaultAbi,
1203
- functionName: "skim",
1204
- args: [amountWithExtra, receiver],
1205
- }),
1206
- });
1207
- // 3. Repay with shares (max)
1208
- items.push({
1209
- targetContract: toVault,
1210
- onBehalfOfAccount: receiver,
1211
- value: 0n,
1212
- data: encodeFunctionData({
1213
- abi: eVaultAbi,
1214
- functionName: "repayWithShares",
1215
- // max is ok now, because skim deposited exact amount and it is the full debt,
1216
- // so pre-existing balance will not be consumed
1217
- args: [maxUint256, receiver],
1218
- }),
1219
- });
1220
- // 4. Disable controller if needed
1221
- if (disableControllerOnMax) {
1222
- items.push(this.encodeDisableController(toVault, receiver));
690
+ appendMaxRepayCleanup(args) {
691
+ const { account, liabilityPosition, receiver, batchItems, sourceAccount, sourceVault, } = args;
692
+ const receiverSubAccount = typeof account.getSubAccount === "function"
693
+ ? account.getSubAccount(receiver)
694
+ : undefined;
695
+ const activeCollaterals = receiverSubAccount
696
+ ? resolveBorrowCollateralPositions(receiverSubAccount, liabilityPosition)
697
+ : [];
698
+ const transferredPositions = new Set();
699
+ for (const collateral of activeCollaterals) {
700
+ batchItems.push(this.encodeDisableCollateral(account.chainId, receiver, collateral.vaultAddress));
701
+ if (getAddress(receiver) !== getAddress(account.owner)) {
702
+ batchItems.push(this.encodeTransferFromMax(collateral.vaultAddress, receiver, account.owner));
1223
703
  }
704
+ transferredPositions.add(`${getAddress(receiver)}:${getAddress(collateral.vaultAddress)}`);
1224
705
  }
1225
- else {
1226
- // For partial repay: withdraw, then repay exact amount
1227
- // 1. Withdraw from source vault directly to the liability vault.
1228
- items.push({
1229
- targetContract: fromVault,
1230
- onBehalfOfAccount: from,
1231
- value: 0n,
1232
- data: encodeFunctionData({
1233
- abi: eVaultAbi,
1234
- functionName: "withdraw",
1235
- args: [amount, toVault, from],
1236
- }),
1237
- });
1238
- // 2. Skim the received assets into the borrow sub-account.
1239
- items.push({
1240
- targetContract: toVault,
1241
- onBehalfOfAccount: receiver,
1242
- value: 0n,
1243
- data: encodeFunctionData({
1244
- abi: eVaultAbi,
1245
- functionName: "skim",
1246
- args: [amount, receiver],
1247
- }),
1248
- });
1249
- // 3. Burn the skimmed shares to repay debt.
1250
- items.push({
1251
- targetContract: toVault,
1252
- onBehalfOfAccount: receiver,
1253
- value: 0n,
1254
- data: encodeFunctionData({
1255
- abi: eVaultAbi,
1256
- functionName: "repayWithShares",
1257
- args: [amount > 0n ? amount - 1n : 0n, receiver],
1258
- }),
1259
- });
706
+ if (sourceAccount && sourceVault) {
707
+ const sourcePositionKey = `${getAddress(sourceAccount)}:${getAddress(sourceVault)}`;
708
+ if (getAddress(sourceAccount) !== getAddress(account.owner) &&
709
+ !transferredPositions.has(sourcePositionKey)) {
710
+ batchItems.push(this.encodeTransferFromMax(sourceVault, sourceAccount, account.owner));
711
+ }
1260
712
  }
1261
- return items;
1262
713
  }
1263
714
  /**
1264
715
  * Resolves RequiredApproval items in a transaction plan by filling in each item's `resolved` field.
@@ -1381,6 +832,9 @@ export class ExecutionService {
1381
832
  */
1382
833
  async resolveRequiredApprovals(args) {
1383
834
  const { plan, chainId, account, usePermit2 = true, unlimitedApproval = false, } = args;
835
+ if (!this.walletService) {
836
+ throw new Error("ExecutionService.resolveRequiredApprovals requires a walletService. Pass it to the ExecutionService constructor or call setWalletService().");
837
+ }
1384
838
  // Filter transaction plan for only RequiredApproval items
1385
839
  const requiredApprovals = plan.filter((item) => item.type === "requiredApproval");
1386
840
  // Transform RequiredApprovals into AssetWithSpenders
@@ -1445,10 +899,7 @@ export class ExecutionService {
1445
899
  enableCollateral: !isCollateralEnabled && enableCollateral,
1446
900
  // Permit2 is handled separately in the plan
1447
901
  });
1448
- plan.push({
1449
- type: "evcBatch",
1450
- items: batchItems,
1451
- });
902
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "deposit"));
1452
903
  return plan;
1453
904
  }
1454
905
  /**
@@ -1490,10 +941,7 @@ export class ExecutionService {
1490
941
  enableCollateral: !isCollateralEnabled && enableCollateral,
1491
942
  // Permit2 is handled separately in the plan
1492
943
  });
1493
- plan.push({
1494
- type: "evcBatch",
1495
- items: batchItems,
1496
- });
944
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "mint"));
1497
945
  return plan;
1498
946
  }
1499
947
  /**
@@ -1522,10 +970,7 @@ export class ExecutionService {
1522
970
  owner,
1523
971
  disableCollateral: disableCollateral && (!position || position.isCollateral),
1524
972
  });
1525
- plan.push({
1526
- type: "evcBatch",
1527
- items: batchItems,
1528
- });
973
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "withdraw"));
1529
974
  return plan;
1530
975
  }
1531
976
  /**
@@ -1555,10 +1000,7 @@ export class ExecutionService {
1555
1000
  owner,
1556
1001
  disableCollateral: disableCollateral && (!position || position.isCollateral),
1557
1002
  });
1558
- plan.push({
1559
- type: "evcBatch",
1560
- items: batchItems,
1561
- });
1003
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "redeem"));
1562
1004
  return plan;
1563
1005
  }
1564
1006
  /**
@@ -1580,6 +1022,12 @@ export class ExecutionService {
1580
1022
  planBorrow(args) {
1581
1023
  const { vault, amount, receiver, borrowAccount, account, collateral } = args;
1582
1024
  const plan = [];
1025
+ const savingsCollateral = isSavingsCollateral(collateral)
1026
+ ? collateral
1027
+ : undefined;
1028
+ const walletCollateral = isWalletCollateral(collateral)
1029
+ ? collateral
1030
+ : undefined;
1583
1031
  const enableCollateral = collateral && collateral.amount > 0n
1584
1032
  ? !(account?.isCollateralEnabled(borrowAccount, collateral.vault) ??
1585
1033
  false)
@@ -1588,15 +1036,15 @@ export class ExecutionService {
1588
1036
  // Default: controller is not enabled when account/sub-account is not available
1589
1037
  const currentController = account?.getCurrentController(borrowAccount);
1590
1038
  const enableController = !(account?.isControllerEnabled(borrowAccount, vault) ?? false);
1591
- if (collateral && collateral.amount > 0n) {
1039
+ if (walletCollateral && walletCollateral.amount > 0n) {
1592
1040
  // Approval is needed from the account owner (who owns the wallet tokens)
1593
1041
  // Add approval requirement (will be resolved later with Wallet data)
1594
1042
  plan.push({
1595
1043
  type: "requiredApproval",
1596
- token: collateral.asset,
1044
+ token: walletCollateral.asset,
1597
1045
  owner: account.owner,
1598
- spender: collateral.vault,
1599
- amount: collateral.amount,
1046
+ spender: walletCollateral.vault,
1047
+ amount: walletCollateral.amount,
1600
1048
  });
1601
1049
  }
1602
1050
  const batchItems = this.encodeBorrow({
@@ -1610,12 +1058,18 @@ export class ExecutionService {
1610
1058
  currentController: currentController || undefined,
1611
1059
  enableCollateral,
1612
1060
  collateralVault: collateral?.vault,
1613
- collateralAmount: collateral?.amount,
1614
- });
1615
- plan.push({
1616
- type: "evcBatch",
1617
- items: batchItems,
1061
+ collateralAmount: walletCollateral?.amount,
1062
+ collateralShareSource: savingsCollateral
1063
+ ? {
1064
+ from: savingsCollateral.from,
1065
+ shares: savingsCollateral.amount,
1066
+ disableCollateralFrom: savingsCollateral.disableCollateralFrom &&
1067
+ (account?.isCollateralEnabled(savingsCollateral.from, savingsCollateral.vault) ??
1068
+ false),
1069
+ }
1070
+ : undefined,
1618
1071
  });
1072
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "borrow"));
1619
1073
  return plan;
1620
1074
  }
1621
1075
  /**
@@ -1659,10 +1113,7 @@ export class ExecutionService {
1659
1113
  enableController,
1660
1114
  enableCollateral,
1661
1115
  });
1662
- plan.push({
1663
- type: "evcBatch",
1664
- items: batchItems,
1665
- });
1116
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "liquidation"));
1666
1117
  return plan;
1667
1118
  }
1668
1119
  /**
@@ -1674,10 +1125,11 @@ export class ExecutionService {
1674
1125
  * @param args.liabilityAmount - Amount of liability asset to repay (use maxUint256 for "repay all")
1675
1126
  * @param args.receiver - Sub-account address whose debt is being repaid
1676
1127
  * @param args.account - Account entity; used for chainId, owner, and position (to resolve liability asset)
1128
+ * @param args.cleanupOnMax - When true and liabilityAmount is maxUint256, the batch disables active collaterals on the repaid sub-account and transfers their shares to the owner account (default false)
1677
1129
  * @returns Array of transaction plan items (approval + EVC batch)
1678
1130
  */
1679
1131
  planRepayFromWallet(args) {
1680
- const { liabilityVault, liabilityAmount, receiver, account } = args;
1132
+ const { liabilityVault, liabilityAmount, receiver, account, cleanupOnMax = false, } = args;
1681
1133
  const plan = [];
1682
1134
  // Get position to determine asset
1683
1135
  const position = account?.getPosition(receiver, liabilityVault);
@@ -1692,6 +1144,7 @@ export class ExecutionService {
1692
1144
  spender: liabilityVault,
1693
1145
  amount: liabilityAmount,
1694
1146
  });
1147
+ const isMax = liabilityAmount === maxUint256;
1695
1148
  // Build EVC batch items
1696
1149
  const batchItems = this.encodeRepayFromWallet({
1697
1150
  chainId: account.chainId,
@@ -1700,18 +1153,28 @@ export class ExecutionService {
1700
1153
  liabilityAmount,
1701
1154
  receiver,
1702
1155
  disableControllerOnMax: true,
1703
- isMax: liabilityAmount === maxUint256,
1156
+ isMax,
1704
1157
  // Permit2 is handled separately in the plan
1705
1158
  });
1706
- plan.push({
1707
- type: "evcBatch",
1708
- items: batchItems,
1709
- });
1159
+ if (cleanupOnMax && isMax) {
1160
+ this.appendMaxRepayCleanup({
1161
+ account,
1162
+ liabilityPosition: position,
1163
+ receiver,
1164
+ batchItems,
1165
+ });
1166
+ }
1167
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "repayFromWallet"));
1710
1168
  return plan;
1711
1169
  }
1712
1170
  /**
1713
1171
  * Builds a transaction plan for repaying debt using assets from another vault deposit (same asset only).
1714
- * Use `maxUint256` for `liabilityAmount` to repay all available debt or up to the available deposit.
1172
+ * Use `maxUint256` for `liabilityAmount` to perform a full repay.
1173
+ * Full repay can opt into cleanup. For different-vault full repays, cleanup only redeems
1174
+ * liability-vault shares left from the repay cushion when the account snapshot shows no pre-existing liability-vault deposit;
1175
+ * otherwise those shares are preserved in the liability vault instead of being migrated to the source vault.
1176
+ * Cleanup also disables active collaterals on the repaid sub-account, transfers those collateral shares to the owner
1177
+ * account, and transfers any remaining source-vault shares from the source sub-account to the owner account.
1715
1178
  *
1716
1179
  * @param args - Repay-from-deposit plan arguments
1717
1180
  * @param args.liabilityVault - Address of the liability vault (debt is repaid to this vault)
@@ -1720,10 +1183,11 @@ export class ExecutionService {
1720
1183
  * @param args.fromVault - Vault to withdraw assets from (must be same underlying asset as liability for this plan)
1721
1184
  * @param args.fromAccount - Sub-account that holds the deposit in `fromVault`
1722
1185
  * @param args.account - Account entity; used for chainId, owner, and positions (to resolve assets and eligibility)
1723
- * @returns Array of transaction plan items (optional approval + EVC batch). Throws if asset differs between fromVault and liabilityVault; use planRepayWithSwap for cross-asset.
1186
+ * @param args.cleanupOnMax - Whether max repay should append repay-cushion, collateral, and source-share cleanup (default false)
1187
+ * @returns Array of transaction plan items (EVC batch only). Throws if asset differs between fromVault and liabilityVault; use planRepayWithSwap for cross-asset.
1724
1188
  */
1725
1189
  planRepayFromDeposit(args) {
1726
- const { liabilityVault, liabilityAmount, receiver, fromVault, fromAccount, account, } = args;
1190
+ const { liabilityVault, liabilityAmount, receiver, fromVault, fromAccount, account, cleanupOnMax = false, } = args;
1727
1191
  const plan = [];
1728
1192
  // Get positions
1729
1193
  const liabilityPosition = account?.getPosition(receiver, liabilityVault);
@@ -1735,7 +1199,7 @@ export class ExecutionService {
1735
1199
  }
1736
1200
  const liabilityAsset = liabilityPosition.asset;
1737
1201
  const fromAsset = fromPosition.asset;
1738
- // Check if approval is needed (only if different assets and we need to swap/withdraw)
1202
+ // Cross-asset repay requires a swap path; this planner only builds same-asset batches.
1739
1203
  if (fromAsset !== liabilityAsset) {
1740
1204
  // This path requires a swap, which is handled by planRepayWithSwap
1741
1205
  throw new Error("planRepayFromDeposit only supports same-asset paths. Use planRepayWithSwap for different assets.");
@@ -1744,6 +1208,7 @@ export class ExecutionService {
1744
1208
  const amount = isMax && fromVault !== liabilityVault
1745
1209
  ? liabilityPosition.borrowed
1746
1210
  : liabilityAmount;
1211
+ const hasPreExistingLiabilityDeposit = (liabilityPosition.assets ?? 0n) > 0n;
1747
1212
  // Build EVC batch items
1748
1213
  const batchItems = this.encodeRepayFromDeposit({
1749
1214
  chainId: account.chainId,
@@ -1758,10 +1223,41 @@ export class ExecutionService {
1758
1223
  isMax,
1759
1224
  // Permit2 is handled separately in the plan
1760
1225
  });
1761
- plan.push({
1762
- type: "evcBatch",
1763
- items: batchItems,
1764
- });
1226
+ if (cleanupOnMax && isMax) {
1227
+ if (fromVault !== liabilityVault && !hasPreExistingLiabilityDeposit) {
1228
+ // Sweep only the repay cushion. If liability shares existed before this batch,
1229
+ // redeem(max) would also migrate that unrelated deposit into the source vault.
1230
+ batchItems.push({
1231
+ targetContract: liabilityVault,
1232
+ onBehalfOfAccount: receiver,
1233
+ value: 0n,
1234
+ data: encodeFunctionData({
1235
+ abi: eVaultAbi,
1236
+ functionName: "redeem",
1237
+ args: [maxUint256, fromVault, receiver],
1238
+ }),
1239
+ });
1240
+ batchItems.push({
1241
+ targetContract: fromVault,
1242
+ onBehalfOfAccount: fromAccount,
1243
+ value: 0n,
1244
+ data: encodeFunctionData({
1245
+ abi: eVaultAbi,
1246
+ functionName: "skim",
1247
+ args: [maxUint256, fromAccount],
1248
+ }),
1249
+ });
1250
+ }
1251
+ this.appendMaxRepayCleanup({
1252
+ account,
1253
+ liabilityPosition,
1254
+ receiver,
1255
+ batchItems,
1256
+ sourceAccount: fromAccount,
1257
+ sourceVault: fromVault,
1258
+ });
1259
+ }
1260
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "repayFromDeposit"));
1765
1261
  return plan;
1766
1262
  }
1767
1263
  /**
@@ -1771,10 +1267,11 @@ export class ExecutionService {
1771
1267
  * @param args - Repay-with-swap plan arguments
1772
1268
  * @param args.swapQuote - Quote from swap service (e.g. fetchRepayQuotes); defines vaultIn, accountIn, accountOut, receiver, swap and verify steps
1773
1269
  * @param args.account - Account entity; used for chainId and positions (to compute isMax and maxWithdraw)
1270
+ * @param args.cleanupOnMax - When true and the quote repays the full debt, the batch disables active collaterals on the repaid sub-account, transfers their shares to the owner account, and transfers remaining source-vault shares to the owner account (default false)
1774
1271
  * @returns Array of transaction plan items (EVC batch: withdraw, swap, verify/repay). Throws if positions not found or liability is zero.
1775
1272
  */
1776
1273
  planRepayWithSwap(args) {
1777
- const { swapQuote, account } = args;
1274
+ const { swapQuote, account, cleanupOnMax = false } = args;
1778
1275
  const plan = [];
1779
1276
  const liabilityPosition = account?.getPosition(swapQuote.accountOut, swapQuote.receiver);
1780
1277
  const fromPosition = account?.getPosition(swapQuote.accountIn, swapQuote.vaultIn);
@@ -1793,10 +1290,17 @@ export class ExecutionService {
1793
1290
  isMax,
1794
1291
  disableControllerOnMax: true,
1795
1292
  });
1796
- plan.push({
1797
- type: "evcBatch",
1798
- items: batchItems,
1799
- });
1293
+ if (cleanupOnMax && isMax) {
1294
+ this.appendMaxRepayCleanup({
1295
+ account,
1296
+ liabilityPosition,
1297
+ receiver: swapQuote.accountOut,
1298
+ batchItems,
1299
+ sourceAccount: swapQuote.accountIn,
1300
+ sourceVault: swapQuote.vaultIn,
1301
+ });
1302
+ }
1303
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "repayWithSwap"));
1800
1304
  return plan;
1801
1305
  }
1802
1306
  /**
@@ -1838,10 +1342,7 @@ export class ExecutionService {
1838
1342
  sender: account.owner,
1839
1343
  enableCollateral: shouldEnableCollateral,
1840
1344
  });
1841
- plan.push({
1842
- type: "evcBatch",
1843
- items: batchItems,
1844
- });
1345
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "depositWithSwapFromWallet"));
1845
1346
  return plan;
1846
1347
  }
1847
1348
  /**
@@ -1871,10 +1372,7 @@ export class ExecutionService {
1871
1372
  amount,
1872
1373
  sender: account.owner,
1873
1374
  });
1874
- plan.push({
1875
- type: "evcBatch",
1876
- items: batchItems,
1877
- });
1375
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "swapFromWallet"));
1878
1376
  return plan;
1879
1377
  }
1880
1378
  /**
@@ -1906,10 +1404,7 @@ export class ExecutionService {
1906
1404
  disableCollateralOnMax: true,
1907
1405
  isMax,
1908
1406
  });
1909
- plan.push({
1910
- type: "evcBatch",
1911
- items: batchItems,
1912
- });
1407
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "swapCollateral"));
1913
1408
  return plan;
1914
1409
  }
1915
1410
  /**
@@ -1937,10 +1432,103 @@ export class ExecutionService {
1937
1432
  disableControllerOnMax: true,
1938
1433
  isMax,
1939
1434
  });
1940
- plan.push({
1941
- type: "evcBatch",
1942
- items: batchItems,
1435
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "swapDebt"));
1436
+ return plan;
1437
+ }
1438
+ /**
1439
+ * Builds a transaction plan for migrating a supplied/collateral position between two same-asset vaults.
1440
+ * This is the no-swap path for moving shares from one vault to another vault with the same underlying asset.
1441
+ *
1442
+ * @param args - Same-asset collateral migration plan arguments
1443
+ * @param args.fromVault - Source vault holding the supplied shares
1444
+ * @param args.toVault - Destination vault with the same underlying asset
1445
+ * @param args.amount - Asset amount to withdraw/redeem and skim into the destination vault
1446
+ * @param args.positionAccount - Sub-account that owns the source shares and receives destination shares
1447
+ * @param args.fromAsset - Optional source underlying asset; defaults to the account position asset
1448
+ * @param args.toAsset - Destination underlying asset, used to verify this is a same-asset migration
1449
+ * @param args.isMax - If true, redeems shares instead of withdrawing assets
1450
+ * @param args.maxShares - Optional exact share amount for max migration
1451
+ * @param args.enableCollateralTo - Optional override for enabling destination collateral
1452
+ * @param args.disableCollateralFrom - Optional override for disabling source collateral
1453
+ * @returns Array of transaction plan items (EVC batch; no token approvals)
1454
+ */
1455
+ planMigrateSameAssetCollateral(args) {
1456
+ const { account, fromVault, toVault, amount, positionAccount, fromAsset, toAsset, isMax = false, maxShares, enableCollateralTo, disableCollateralFrom, } = args;
1457
+ const plan = [];
1458
+ const sourcePosition = account?.getPosition(positionAccount, fromVault);
1459
+ const resolvedFromAsset = fromAsset ?? sourcePosition?.asset;
1460
+ if (!resolvedFromAsset) {
1461
+ throw new Error(`Position not found. From vault: ${fromVault}, Account: ${positionAccount}. Source asset is required when position is not available.`);
1462
+ }
1463
+ if (getAddress(resolvedFromAsset) !== getAddress(toAsset)) {
1464
+ throw new Error("planMigrateSameAssetCollateral only supports same-asset vault migrations. Use planSwapCollateral for cross-asset migrations.");
1465
+ }
1466
+ const shouldEnableCollateralTo = enableCollateralTo ??
1467
+ !account.isCollateralEnabled(positionAccount, toVault);
1468
+ const shouldDisableCollateralFrom = disableCollateralFrom ??
1469
+ (isMax && account.isCollateralEnabled(positionAccount, fromVault));
1470
+ const batchItems = this.encodeMigrateSameAssetCollateral({
1471
+ chainId: account.chainId,
1472
+ fromVault,
1473
+ toVault,
1474
+ amount,
1475
+ account: positionAccount,
1476
+ isMax,
1477
+ maxShares: maxShares ?? (isMax ? sourcePosition?.shares : undefined),
1478
+ enableCollateralTo: shouldEnableCollateralTo,
1479
+ disableCollateralFrom: shouldDisableCollateralFrom,
1480
+ });
1481
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "migrateSameAssetCollateral"));
1482
+ return plan;
1483
+ }
1484
+ /**
1485
+ * Builds a transaction plan for migrating a full same-asset debt position from one liability vault to another.
1486
+ * This is the no-swap path for debt vault migration and repays the old vault with shares after borrowing
1487
+ * slightly more from the new vault to cover interest accrual.
1488
+ *
1489
+ * @param args - Same-asset debt migration plan arguments
1490
+ * @param args.oldLiabilityVault - Existing debt vault to fully repay and disable
1491
+ * @param args.newLiabilityVault - New same-asset debt vault to borrow from
1492
+ * @param args.liabilityAccount - Sub-account that owns the debt position
1493
+ * @param args.liabilityAmount - Current debt amount; defaults to the old-vault borrowed amount in account data
1494
+ * @param args.oldLiabilityAsset - Optional old liability asset; defaults to the old-vault account position asset
1495
+ * @param args.newLiabilityAsset - New liability underlying asset, used to verify this is a same-asset migration
1496
+ * @param args.sweepExcess - Whether to redeem and skim the migration cushion back into the new vault (default true)
1497
+ * @param args.transferRemainingSharesToOwner - Whether to transfer new-vault shares to the owner when liabilityAccount differs from owner (default true)
1498
+ * @returns Array of transaction plan items (EVC batch; no token approvals)
1499
+ */
1500
+ planMigrateSameAssetDebt(args) {
1501
+ const { account, oldLiabilityVault, newLiabilityVault, liabilityAccount, liabilityAmount, oldLiabilityAsset, newLiabilityAsset, sweepExcess = true, transferRemainingSharesToOwner = true, } = args;
1502
+ const plan = [];
1503
+ const oldPosition = account?.getPosition(liabilityAccount, oldLiabilityVault);
1504
+ const resolvedOldAsset = oldLiabilityAsset ?? oldPosition?.asset;
1505
+ if (!resolvedOldAsset) {
1506
+ throw new Error(`Position not found. Old liability vault: ${oldLiabilityVault}, Account: ${liabilityAccount}. Old liability asset is required when position is not available.`);
1507
+ }
1508
+ if (getAddress(resolvedOldAsset) !== getAddress(newLiabilityAsset)) {
1509
+ throw new Error("planMigrateSameAssetDebt only supports same-asset debt migrations. Use planSwapDebt for cross-asset migrations.");
1510
+ }
1511
+ const amount = liabilityAmount ?? oldPosition?.borrowed;
1512
+ if (!amount || amount <= 0n) {
1513
+ throw new Error(`Debt position not found or liability is 0. Old liability vault: ${oldLiabilityVault}, Account: ${liabilityAccount}`);
1514
+ }
1515
+ const enableController = !account.isControllerEnabled(liabilityAccount, newLiabilityVault);
1516
+ const transferRemainingSharesTo = transferRemainingSharesToOwner &&
1517
+ getAddress(liabilityAccount) !== getAddress(account.owner)
1518
+ ? account.owner
1519
+ : undefined;
1520
+ const batchItems = this.encodeMigrateSameAssetDebt({
1521
+ chainId: account.chainId,
1522
+ oldLiabilityVault,
1523
+ newLiabilityVault,
1524
+ amount,
1525
+ account: liabilityAccount,
1526
+ enableController,
1527
+ disableController: true,
1528
+ sweepExcess,
1529
+ transferRemainingSharesTo,
1943
1530
  });
1531
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "migrateSameAssetDebt"));
1944
1532
  return plan;
1945
1533
  }
1946
1534
  /**
@@ -1971,10 +1559,7 @@ export class ExecutionService {
1971
1559
  disableCollateralFrom: disableCollateralFrom &&
1972
1560
  (account?.isCollateralEnabled(from, vault) ?? false),
1973
1561
  });
1974
- plan.push({
1975
- type: "evcBatch",
1976
- items: batchItems,
1977
- });
1562
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "transfer"));
1978
1563
  return plan;
1979
1564
  }
1980
1565
  /**
@@ -2001,10 +1586,7 @@ export class ExecutionService {
2001
1586
  to,
2002
1587
  enableController,
2003
1588
  });
2004
- plan.push({
2005
- type: "evcBatch",
2006
- items: batchItems,
2007
- });
1589
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "pullDebt"));
2008
1590
  return plan;
2009
1591
  }
2010
1592
  /**
@@ -2020,10 +1602,10 @@ export class ExecutionService {
2020
1602
  * @returns Array of transaction plan items (optional approval + EVC batch). Throws if swapQuote.accountIn !== swapQuote.accountOut.
2021
1603
  */
2022
1604
  planMultiplyWithSwap(args) {
2023
- const { collateralVault, collateralAmount, collateralAsset, account, swapQuote, } = args;
1605
+ const { collateralVault, collateralAmount, collateralAsset, collateralShareSource, account, swapQuote, } = args;
2024
1606
  const plan = [];
2025
1607
  // 1. Check if collateral approval is needed (only if depositing collateral)
2026
- if (collateralAmount > 0n) {
1608
+ if (!collateralShareSource && collateralAmount > 0n) {
2027
1609
  // Add approval requirement (will be resolved later with Wallet data)
2028
1610
  plan.push({
2029
1611
  type: "requiredApproval",
@@ -2041,8 +1623,19 @@ export class ExecutionService {
2041
1623
  const longVault = swapQuote.receiver;
2042
1624
  const liabilityAmount = BigInt(swapQuote.amountIn);
2043
1625
  // 2. Determine if collateral needs to be enabled
2044
- const enableCollateral = collateralAmount > 0n &&
1626
+ const hasCollateralInput = collateralShareSource
1627
+ ? collateralShareSource.shares > 0n
1628
+ : collateralAmount > 0n;
1629
+ const enableCollateral = hasCollateralInput &&
2045
1630
  !(account?.isCollateralEnabled(receiver, collateralVault) ?? false);
1631
+ const resolvedCollateralShareSource = collateralShareSource
1632
+ ? {
1633
+ ...collateralShareSource,
1634
+ disableCollateralFrom: collateralShareSource.disableCollateralFrom &&
1635
+ (account?.isCollateralEnabled(collateralShareSource.from, collateralVault) ??
1636
+ false),
1637
+ }
1638
+ : undefined;
2046
1639
  // 3. Determine if controller needs to be enabled
2047
1640
  const enableController = !(account?.isControllerEnabled(receiver, liabilityVault) ?? false);
2048
1641
  // 4. Get current controller (may need to disable if different)
@@ -2060,13 +1653,11 @@ export class ExecutionService {
2060
1653
  enableCollateral,
2061
1654
  currentController: currentController || undefined,
2062
1655
  enableController,
1656
+ collateralShareSource: resolvedCollateralShareSource,
2063
1657
  swapQuote,
2064
1658
  // Permit2 is handled separately in the plan
2065
1659
  });
2066
- plan.push({
2067
- type: "evcBatch",
2068
- items: batchItems,
2069
- });
1660
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "multiplyWithSwap"));
2070
1661
  return plan;
2071
1662
  }
2072
1663
  /**
@@ -2085,10 +1676,10 @@ export class ExecutionService {
2085
1676
  * @returns Array of transaction plan items (optional approval + EVC batch)
2086
1677
  */
2087
1678
  planMultiplySameAsset(args) {
2088
- const { collateralVault, collateralAmount, collateralAsset, liabilityVault, liabilityAmount, longVault, receiver, account, } = args;
1679
+ const { collateralVault, collateralAmount, collateralAsset, collateralShareSource, liabilityVault, liabilityAmount, longVault, receiver, account, } = args;
2089
1680
  const plan = [];
2090
1681
  // 1. Check if collateral approval is needed (only if depositing collateral)
2091
- if (collateralAmount > 0n) {
1682
+ if (!collateralShareSource && collateralAmount > 0n) {
2092
1683
  // Add approval requirement (will be resolved later with Wallet data)
2093
1684
  plan.push({
2094
1685
  type: "requiredApproval",
@@ -2099,8 +1690,19 @@ export class ExecutionService {
2099
1690
  });
2100
1691
  }
2101
1692
  // 2. Determine if collateral needs to be enabled
2102
- const enableCollateral = collateralAmount > 0n &&
1693
+ const hasCollateralInput = collateralShareSource
1694
+ ? collateralShareSource.shares > 0n
1695
+ : collateralAmount > 0n;
1696
+ const enableCollateral = hasCollateralInput &&
2103
1697
  !(account?.isCollateralEnabled(receiver, collateralVault) ?? false);
1698
+ const resolvedCollateralShareSource = collateralShareSource
1699
+ ? {
1700
+ ...collateralShareSource,
1701
+ disableCollateralFrom: collateralShareSource.disableCollateralFrom &&
1702
+ (account?.isCollateralEnabled(collateralShareSource.from, collateralVault) ??
1703
+ false),
1704
+ }
1705
+ : undefined;
2104
1706
  // 3. Determine if controller needs to be enabled
2105
1707
  const enableController = !(account?.isControllerEnabled(receiver, liabilityVault) ?? false);
2106
1708
  // 4. Get current controller (may need to disable if different)
@@ -2118,12 +1720,10 @@ export class ExecutionService {
2118
1720
  enableCollateral,
2119
1721
  currentController: currentController || undefined,
2120
1722
  enableController,
1723
+ collateralShareSource: resolvedCollateralShareSource,
2121
1724
  // Permit2 is handled separately in the plan
2122
1725
  });
2123
- plan.push({
2124
- type: "evcBatch",
2125
- items: batchItems,
2126
- });
1726
+ plan.push(...this.convertBatchItemsToPlan(batchItems, "multiplySameAsset"));
2127
1727
  return plan;
2128
1728
  }
2129
1729
  }