@agentlayer.tech/wallet 0.1.0

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 (96) hide show
  1. package/.openclaw/AGENTS.md +98 -0
  2. package/.openclaw/extensions/agent-wallet/README.md +127 -0
  3. package/.openclaw/extensions/agent-wallet/index.ts +1520 -0
  4. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +184 -0
  5. package/.openclaw/extensions/agent-wallet/package.json +11 -0
  6. package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +20 -0
  7. package/CHANGELOG.md +42 -0
  8. package/LICENSE +104 -0
  9. package/README.md +332 -0
  10. package/RELEASING.md +204 -0
  11. package/agent-wallet/.env.example +62 -0
  12. package/agent-wallet/AGENTS.md +129 -0
  13. package/agent-wallet/README.md +527 -0
  14. package/agent-wallet/agent_wallet/__init__.py +11 -0
  15. package/agent-wallet/agent_wallet/approval.py +161 -0
  16. package/agent-wallet/agent_wallet/bootstrap.py +178 -0
  17. package/agent-wallet/agent_wallet/btc_user_wallets.py +217 -0
  18. package/agent-wallet/agent_wallet/config.py +382 -0
  19. package/agent-wallet/agent_wallet/encrypted_storage.py +161 -0
  20. package/agent-wallet/agent_wallet/evm_user_wallets.py +370 -0
  21. package/agent-wallet/agent_wallet/exceptions.py +9 -0
  22. package/agent-wallet/agent_wallet/file_ops.py +34 -0
  23. package/agent-wallet/agent_wallet/http_client.py +25 -0
  24. package/agent-wallet/agent_wallet/models.py +66 -0
  25. package/agent-wallet/agent_wallet/nonce_registry.py +59 -0
  26. package/agent-wallet/agent_wallet/openclaw_adapter.py +5128 -0
  27. package/agent-wallet/agent_wallet/openclaw_cli.py +626 -0
  28. package/agent-wallet/agent_wallet/openclaw_runtime.py +272 -0
  29. package/agent-wallet/agent_wallet/plugin_bundle.py +42 -0
  30. package/agent-wallet/agent_wallet/providers/__init__.py +1 -0
  31. package/agent-wallet/agent_wallet/providers/bags.py +259 -0
  32. package/agent-wallet/agent_wallet/providers/evm_portfolio.py +470 -0
  33. package/agent-wallet/agent_wallet/providers/jupiter.py +567 -0
  34. package/agent-wallet/agent_wallet/providers/kamino.py +215 -0
  35. package/agent-wallet/agent_wallet/providers/lifi.py +277 -0
  36. package/agent-wallet/agent_wallet/providers/solana_rpc.py +470 -0
  37. package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +114 -0
  38. package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +205 -0
  39. package/agent-wallet/agent_wallet/sealed_keys.py +61 -0
  40. package/agent-wallet/agent_wallet/solana_stake.py +103 -0
  41. package/agent-wallet/agent_wallet/solana_tx.py +93 -0
  42. package/agent-wallet/agent_wallet/spending_limits.py +101 -0
  43. package/agent-wallet/agent_wallet/transaction_policy.py +518 -0
  44. package/agent-wallet/agent_wallet/user_wallets.py +355 -0
  45. package/agent-wallet/agent_wallet/validation.py +31 -0
  46. package/agent-wallet/agent_wallet/wallet_layer/__init__.py +1 -0
  47. package/agent-wallet/agent_wallet/wallet_layer/base.py +808 -0
  48. package/agent-wallet/agent_wallet/wallet_layer/base58.py +44 -0
  49. package/agent-wallet/agent_wallet/wallet_layer/factory.py +102 -0
  50. package/agent-wallet/agent_wallet/wallet_layer/solana.py +4252 -0
  51. package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +272 -0
  52. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +1628 -0
  53. package/agent-wallet/examples/bootstrap_wallet.py +21 -0
  54. package/agent-wallet/examples/openclaw_runtime_onboarding.py +28 -0
  55. package/agent-wallet/examples/openclaw_user_wallet_example.py +31 -0
  56. package/agent-wallet/examples/openclaw_wallet_adapter_example.py +33 -0
  57. package/agent-wallet/openclaw.plugin.json +138 -0
  58. package/agent-wallet/pyproject.toml +31 -0
  59. package/agent-wallet/scripts/bootstrap_openclaw_btc.py +278 -0
  60. package/agent-wallet/scripts/build_release_bundle.py +188 -0
  61. package/agent-wallet/scripts/finalize_openclaw_local_wallet_config.py +121 -0
  62. package/agent-wallet/scripts/install_agent_wallet.py +505 -0
  63. package/agent-wallet/scripts/install_openclaw_local_config.py +226 -0
  64. package/agent-wallet/scripts/install_openclaw_sealed_keys.py +105 -0
  65. package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +244 -0
  66. package/agent-wallet/scripts/reveal_btc_seed.sh +130 -0
  67. package/agent-wallet/scripts/security_utils.py +37 -0
  68. package/agent-wallet/scripts/setup_btc_wallet.sh +146 -0
  69. package/agent-wallet/scripts/switch_openclaw_wallet_network.py +106 -0
  70. package/agent-wallet/skills/wallet-operator/SKILL.md +128 -0
  71. package/bin/openclaw-agent-wallet.mjs +487 -0
  72. package/install-from-github.sh +134 -0
  73. package/package.json +61 -0
  74. package/setup.sh +40 -0
  75. package/wdk-btc-wallet/README.md +325 -0
  76. package/wdk-btc-wallet/bootstrap.sh +22 -0
  77. package/wdk-btc-wallet/package-lock.json +1839 -0
  78. package/wdk-btc-wallet/package.json +18 -0
  79. package/wdk-btc-wallet/run-local.sh +21 -0
  80. package/wdk-btc-wallet/src/config.js +160 -0
  81. package/wdk-btc-wallet/src/json.js +35 -0
  82. package/wdk-btc-wallet/src/local_vault.js +432 -0
  83. package/wdk-btc-wallet/src/network_state.js +84 -0
  84. package/wdk-btc-wallet/src/server.js +257 -0
  85. package/wdk-btc-wallet/src/wdk_btc_wallet.js +332 -0
  86. package/wdk-evm-wallet/README.md +183 -0
  87. package/wdk-evm-wallet/bootstrap.sh +8 -0
  88. package/wdk-evm-wallet/package-lock.json +2340 -0
  89. package/wdk-evm-wallet/package.json +23 -0
  90. package/wdk-evm-wallet/run-local.sh +12 -0
  91. package/wdk-evm-wallet/src/config.js +274 -0
  92. package/wdk-evm-wallet/src/json.js +35 -0
  93. package/wdk-evm-wallet/src/local_vault.js +430 -0
  94. package/wdk-evm-wallet/src/network_state.js +92 -0
  95. package/wdk-evm-wallet/src/server.js +575 -0
  96. package/wdk-evm-wallet/src/wdk_evm_wallet.js +4981 -0
@@ -0,0 +1,4981 @@
1
+ import crypto from "node:crypto";
2
+
3
+ import { Contract, Interface } from "ethers";
4
+ import WDK from "@tetherto/wdk";
5
+ import { AaveV3Base, AaveV3Ethereum } from "@bgd-labs/aave-address-book";
6
+ import AaveProtocolEvm from "@tetherto/wdk-protocol-lending-aave-evm";
7
+ import VeloraProtocolEvm from "@tetherto/wdk-protocol-swap-velora-evm";
8
+ import WalletManagerEvm, { WalletAccountReadOnlyEvm } from "@tetherto/wdk-wallet-evm";
9
+
10
+ const ERC20_NAME_SELECTOR = "0x06fdde03";
11
+ const ERC20_SYMBOL_SELECTOR = "0x95d89b41";
12
+ const ERC20_DECIMALS_SELECTOR = "0x313ce567";
13
+ const ERC20_BALANCE_OF_SELECTOR = "0x70a08231";
14
+ const ERC20_APPROVE_SELECTOR = "0x095ea7b3";
15
+ const USDT_MAINNET_ADDRESS = "0xdac17f958d2ee523a2206206994597c13d831ec7";
16
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
17
+ const VELORA_NATIVE_TOKEN_ADDRESS = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
18
+ const LIFI_SOLANA_NATIVE_TOKEN_ADDRESS = "11111111111111111111111111111111";
19
+ const DEFAULT_SWAP_SLIPPAGE_BPS = 100;
20
+ const DEFAULT_LIFI_SLIPPAGE = 0.005;
21
+ const ALWAYS_DENIED_LIFI_BRIDGES = ["mayan"];
22
+ const AAVE_RAY = 10n ** 27n;
23
+ const LIDO_STETH_DECIMALS = 18;
24
+ const LIDO_MIN_STETH_WITHDRAWAL_AMOUNT = 100n;
25
+ const LIDO_MAX_STETH_WITHDRAWAL_AMOUNT = 1000n * 10n ** 18n;
26
+ const LIDO_CONTRACTS_BY_NETWORK = {
27
+ ethereum: {
28
+ steth: {
29
+ address: "0xae7ab96520de3a18e5e111b5eaab095312d7fe84",
30
+ name: "Liquid staked Ether 2.0",
31
+ symbol: "stETH",
32
+ decimals: LIDO_STETH_DECIMALS,
33
+ },
34
+ wsteth: {
35
+ address: "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0",
36
+ name: "Wrapped liquid staked Ether 2.0",
37
+ symbol: "wstETH",
38
+ decimals: LIDO_STETH_DECIMALS,
39
+ },
40
+ referralStaker: "0xa88f0329c2c4ce51ba3fc619bbf44efe7120dd0d",
41
+ withdrawalQueue: "0x889edc2edab5f40e902b864ad4d7ade8e412f9b1",
42
+ },
43
+ };
44
+ const AAVE_PROTOCOL_DATA_PROVIDER_BY_NETWORK = {
45
+ ethereum: AaveV3Ethereum.AAVE_PROTOCOL_DATA_PROVIDER,
46
+ base: AaveV3Base.AAVE_PROTOCOL_DATA_PROVIDER,
47
+ };
48
+ const AAVE_PROTOCOL_DATA_PROVIDER_ABI = [
49
+ "function getUserReserveData(address asset, address user) view returns (uint256 currentATokenBalance, uint256 currentStableDebt, uint256 currentVariableDebt, uint256 principalStableDebt, uint256 scaledVariableDebt, uint256 stableBorrowRate, uint256 liquidityRate, uint40 stableRateLastUpdated, bool usageAsCollateralEnabled)",
50
+ ];
51
+ const LIDO_WSTETH_ABI = [
52
+ "function getWstETHByStETH(uint256 _stETHAmount) view returns (uint256)",
53
+ "function getStETHByWstETH(uint256 _wstETHAmount) view returns (uint256)",
54
+ "function wrap(uint256 _stETHAmount) returns (uint256)",
55
+ "function unwrap(uint256 _wstETHAmount) returns (uint256)",
56
+ ];
57
+ const LIDO_REFERRAL_STAKER_ABI = [
58
+ "function stakeETH(address _referral) payable returns (uint256)",
59
+ ];
60
+ const LIDO_WITHDRAWAL_QUEUE_ABI = [
61
+ "function requestWithdrawals(uint256[] _amounts, address _owner) returns (uint256[] requestIds)",
62
+ "function requestWithdrawalsWstETH(uint256[] _amounts, address _owner) returns (uint256[] requestIds)",
63
+ "function getWithdrawalRequests(address _owner) view returns (uint256[] requestIds)",
64
+ "function getWithdrawalStatus(uint256[] _requestIds) view returns ((uint256 amountOfStETH,uint256 amountOfShares,address owner,uint256 timestamp,bool isFinalized,bool isClaimed)[] statuses)",
65
+ "function claimWithdrawal(uint256 _requestId)",
66
+ ];
67
+ const LIDO_WSTETH_INTERFACE = new Interface(LIDO_WSTETH_ABI);
68
+ const LIDO_REFERRAL_STAKER_INTERFACE = new Interface(LIDO_REFERRAL_STAKER_ABI);
69
+ const LIDO_WITHDRAWAL_QUEUE_INTERFACE = new Interface(LIDO_WITHDRAWAL_QUEUE_ABI);
70
+ const LIFI_CHAIN_IDS_BY_NETWORK = {
71
+ ethereum: "1",
72
+ base: "8453",
73
+ };
74
+ const LIFI_CHAIN_ALIASES = {
75
+ eth: "1",
76
+ ethereum: "1",
77
+ mainnet: "1",
78
+ "eth-mainnet": "1",
79
+ base: "8453",
80
+ "base-mainnet": "8453",
81
+ sol: "1151111081099710",
82
+ solana: "1151111081099710",
83
+ };
84
+
85
+ function createTaggedError(message, code, details = {}) {
86
+ const error = new Error(message);
87
+ if (typeof code === "string" && code.trim()) {
88
+ error.errorCode = code.trim();
89
+ }
90
+ if (details && typeof details === "object" && !Array.isArray(details)) {
91
+ error.errorDetails = details;
92
+ }
93
+ return error;
94
+ }
95
+
96
+ function assertNonEmptyString(value, fieldName) {
97
+ if (typeof value !== "string" || !value.trim()) {
98
+ throw new Error(`${fieldName} is required.`);
99
+ }
100
+ return value.trim();
101
+ }
102
+
103
+ function assertValidSeedPhrase(seedPhrase) {
104
+ const mnemonic = assertNonEmptyString(seedPhrase, "seedPhrase");
105
+ if (!WDK.isValidSeed(mnemonic)) {
106
+ throw new Error("seedPhrase must be a valid BIP-39 seed phrase.");
107
+ }
108
+ return mnemonic;
109
+ }
110
+
111
+ function assertValidNetwork(network, fieldName = "network") {
112
+ if (network === undefined || network === null || network === "") {
113
+ return null;
114
+ }
115
+ const normalized = String(network).trim().toLowerCase();
116
+ const aliases = {
117
+ mainnet: "ethereum",
118
+ eth: "ethereum",
119
+ "base-mainnet": "base",
120
+ base_sepolia: "base-sepolia",
121
+ };
122
+ const effective = aliases[normalized] || normalized;
123
+ if (!["ethereum", "sepolia", "base", "base-sepolia"].includes(effective)) {
124
+ throw new Error(`${fieldName} must be one of: ethereum, sepolia, base, base-sepolia.`);
125
+ }
126
+ return effective;
127
+ }
128
+
129
+ function assertNonNegativeInteger(value, fieldName) {
130
+ if (typeof value === "boolean") {
131
+ throw new Error(`${fieldName} must be a non-negative integer.`);
132
+ }
133
+ const parsed = Number(value);
134
+ if (!Number.isInteger(parsed) || parsed < 0) {
135
+ throw new Error(`${fieldName} must be a non-negative integer.`);
136
+ }
137
+ return parsed;
138
+ }
139
+
140
+ function assertPositiveBigIntString(value, fieldName) {
141
+ const normalized = String(value ?? "").trim();
142
+ if (!/^[0-9]+$/.test(normalized)) {
143
+ throw new Error(`${fieldName} must be a positive base-10 integer string.`);
144
+ }
145
+ const parsed = BigInt(normalized);
146
+ if (parsed <= 0n) {
147
+ throw new Error(`${fieldName} must be greater than zero.`);
148
+ }
149
+ return parsed;
150
+ }
151
+
152
+ function normalizeAddress(value, fieldName) {
153
+ const address = assertNonEmptyString(value, fieldName);
154
+ if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
155
+ throw new Error(`${fieldName} must be a valid 20-byte hex address.`);
156
+ }
157
+ if (address.toLowerCase() === "0x0000000000000000000000000000000000000000") {
158
+ throw new Error(`${fieldName} must not be the zero address.`);
159
+ }
160
+ return address;
161
+ }
162
+
163
+ function assertDistinctAddresses(left, leftName, right, rightName) {
164
+ if (left.toLowerCase() === right.toLowerCase()) {
165
+ throw new Error(`${leftName} and ${rightName} must be different addresses.`);
166
+ }
167
+ }
168
+
169
+ function assertVeloraSupportedNetwork(network) {
170
+ if (!["ethereum", "base"].includes(network)) {
171
+ throw new Error(
172
+ "Velora swap quotes are currently supported only on ethereum and base mainnet."
173
+ );
174
+ }
175
+ }
176
+
177
+ function assertLifiSupportedNetwork(network) {
178
+ if (!Object.hasOwn(LIFI_CHAIN_IDS_BY_NETWORK, network)) {
179
+ throw new Error(
180
+ "LI.FI EVM-origin swaps are currently supported only on ethereum and base mainnet."
181
+ );
182
+ }
183
+ }
184
+
185
+ function assertAaveSupportedNetwork(network) {
186
+ if (!["ethereum", "base"].includes(network)) {
187
+ throw new Error("Aave V3 is currently supported only on ethereum and base mainnet.");
188
+ }
189
+ }
190
+
191
+ function assertLidoSupportedNetwork(network) {
192
+ if (network !== "ethereum") {
193
+ throw new Error("Lido staking is currently supported only on ethereum mainnet.");
194
+ }
195
+ }
196
+
197
+ function normalizeAaveOperation(value) {
198
+ const operation = assertNonEmptyString(value, "operation").toLowerCase();
199
+ if (!["supply", "withdraw", "borrow", "repay"].includes(operation)) {
200
+ throw new Error("operation must be one of: supply, withdraw, borrow, repay.");
201
+ }
202
+ return operation;
203
+ }
204
+
205
+ function normalizeLidoOperation(value) {
206
+ const operation = assertNonEmptyString(value, "operation").toLowerCase();
207
+ if (!["stake_eth_for_wsteth", "wrap_steth", "unwrap_wsteth"].includes(operation)) {
208
+ throw new Error("operation must be one of: stake_eth_for_wsteth, wrap_steth, unwrap_wsteth.");
209
+ }
210
+ return operation;
211
+ }
212
+
213
+ function normalizeLidoWithdrawalOperation(value) {
214
+ const operation = assertNonEmptyString(value, "operation").toLowerCase();
215
+ if (!["request_withdrawal_steth", "request_withdrawal_wsteth", "claim_withdrawal"].includes(operation)) {
216
+ throw new Error(
217
+ "operation must be one of: request_withdrawal_steth, request_withdrawal_wsteth, claim_withdrawal."
218
+ );
219
+ }
220
+ return operation;
221
+ }
222
+
223
+ function isVeloraNativeTokenAddress(value) {
224
+ return String(value || "").trim().toLowerCase() === VELORA_NATIVE_TOKEN_ADDRESS;
225
+ }
226
+
227
+ function isZeroAddress(value) {
228
+ return String(value || "").trim().toLowerCase() === ZERO_ADDRESS;
229
+ }
230
+
231
+ function normalizeEvmTokenAddressAllowingNative(value, fieldName) {
232
+ const raw = assertNonEmptyString(value, fieldName);
233
+ const alias = raw.toLowerCase();
234
+ const address = alias === "native" || alias === "eth" ? ZERO_ADDRESS : raw;
235
+ if (isZeroAddress(address)) {
236
+ return ZERO_ADDRESS;
237
+ }
238
+ return normalizeAddress(address, fieldName).toLowerCase();
239
+ }
240
+
241
+ function normalizeLifiOutputTokenAddress(value, destinationChainId, fieldName) {
242
+ const raw = assertNonEmptyString(value, fieldName);
243
+ const alias = raw.toLowerCase();
244
+ if (["1", "8453"].includes(destinationChainId)) {
245
+ return normalizeEvmTokenAddressAllowingNative(raw, fieldName);
246
+ }
247
+ if (destinationChainId === "1151111081099710" && ["native", "sol", "solana"].includes(alias)) {
248
+ return LIFI_SOLANA_NATIVE_TOKEN_ADDRESS;
249
+ }
250
+ return raw;
251
+ }
252
+
253
+ function normalizeLifiChainId(value, fieldName) {
254
+ const normalized = assertNonEmptyString(value, fieldName).toLowerCase();
255
+ const effective = LIFI_CHAIN_ALIASES[normalized] || normalized;
256
+ if (!["1", "8453", "1151111081099710"].includes(effective)) {
257
+ throw new Error(`${fieldName} must be one of: ethereum, base, solana, 1, 8453, 1151111081099710.`);
258
+ }
259
+ return effective;
260
+ }
261
+
262
+ function parseLifiSlippage(value, fallback = DEFAULT_LIFI_SLIPPAGE) {
263
+ if (value === undefined || value === null || value === "") {
264
+ return fallback;
265
+ }
266
+ if (typeof value === "boolean") {
267
+ throw new Error("slippage must be a number between 0 and 1.");
268
+ }
269
+ const parsed = Number(value);
270
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
271
+ throw new Error("slippage must be a number between 0 and 1.");
272
+ }
273
+ return parsed;
274
+ }
275
+
276
+ function normalizeBridgeList(value, fieldName) {
277
+ if (value === undefined || value === null || value === "") {
278
+ return null;
279
+ }
280
+ if (typeof value === "string") {
281
+ const items = value.split(",").map((item) => item.trim()).filter(Boolean);
282
+ return items.length > 0 ? items.join(",") : null;
283
+ }
284
+ if (Array.isArray(value)) {
285
+ const items = value.map((item) => assertNonEmptyString(item, fieldName));
286
+ return items.length > 0 ? items.join(",") : null;
287
+ }
288
+ throw new Error(`${fieldName} must be a string or array of strings.`);
289
+ }
290
+
291
+ function mergeBridgeLists(...values) {
292
+ const items = [];
293
+ for (const value of values) {
294
+ const normalized = normalizeBridgeList(value, "denyBridges");
295
+ if (!normalized) {
296
+ continue;
297
+ }
298
+ for (const item of normalized.split(",")) {
299
+ const bridge = item.trim();
300
+ if (bridge && !items.some((existing) => existing.toLowerCase() === bridge.toLowerCase())) {
301
+ items.push(bridge);
302
+ }
303
+ }
304
+ }
305
+ return items.length > 0 ? items.join(",") : null;
306
+ }
307
+
308
+ function buildSwapRequest({ tokenIn, tokenOut, tokenInAmount }) {
309
+ const swapRequest = {
310
+ tokenIn: normalizeAddress(tokenIn, "tokenIn"),
311
+ tokenOut: normalizeAddress(tokenOut, "tokenOut"),
312
+ tokenInAmount: assertPositiveBigIntString(tokenInAmount, "tokenInAmount"),
313
+ };
314
+ assertDistinctAddresses(swapRequest.tokenIn, "tokenIn", swapRequest.tokenOut, "tokenOut");
315
+ return swapRequest;
316
+ }
317
+
318
+ function buildLifiEvmSwapRequest({
319
+ tokenIn,
320
+ destinationChain,
321
+ outputToken,
322
+ destinationAddress,
323
+ tokenInAmount,
324
+ slippage,
325
+ allowBridges,
326
+ denyBridges,
327
+ preferBridges,
328
+ }) {
329
+ const destinationChainId = normalizeLifiChainId(destinationChain, "destinationChain");
330
+ return {
331
+ tokenIn: normalizeEvmTokenAddressAllowingNative(tokenIn, "tokenIn"),
332
+ destinationChainId,
333
+ outputToken: normalizeLifiOutputTokenAddress(outputToken, destinationChainId, "outputToken"),
334
+ destinationAddress: assertNonEmptyString(destinationAddress, "destinationAddress"),
335
+ tokenInAmount: assertPositiveBigIntString(tokenInAmount, "tokenInAmount"),
336
+ slippage: parseLifiSlippage(slippage),
337
+ allowBridges: normalizeBridgeList(allowBridges, "allowBridges"),
338
+ denyBridges: normalizeBridgeList(denyBridges, "denyBridges"),
339
+ preferBridges: normalizeBridgeList(preferBridges, "preferBridges"),
340
+ };
341
+ }
342
+
343
+ function buildAaveOperationRequest({ operation, token, tokenAddress, amount, onBehalfOf, to }) {
344
+ if (onBehalfOf !== undefined && onBehalfOf !== null && String(onBehalfOf).trim()) {
345
+ throw new Error("Aave delegated onBehalfOf operations are not exposed by this local wallet runtime.");
346
+ }
347
+ if (to !== undefined && to !== null && String(to).trim()) {
348
+ throw new Error("Aave third-party withdraw destinations are not exposed by this local wallet runtime.");
349
+ }
350
+ const preferredToken = tokenAddress ?? token;
351
+ if (tokenAddress && token && String(tokenAddress).toLowerCase() !== String(token).toLowerCase()) {
352
+ throw new Error("tokenAddress and token must refer to the same address.");
353
+ }
354
+ return {
355
+ operation: normalizeAaveOperation(operation),
356
+ token: normalizeAddress(preferredToken, "tokenAddress"),
357
+ amount: assertPositiveBigIntString(amount, "amount"),
358
+ };
359
+ }
360
+
361
+ function buildLidoOperationRequest({ operation, amount }) {
362
+ return {
363
+ operation: normalizeLidoOperation(operation),
364
+ amount: assertPositiveBigIntString(amount, "amount"),
365
+ };
366
+ }
367
+
368
+ function buildLidoWithdrawalRequest({ operation, amount, requestId }) {
369
+ const normalizedOperation = normalizeLidoWithdrawalOperation(operation);
370
+ if (normalizedOperation === "claim_withdrawal") {
371
+ const normalizedRequestId = String(requestId ?? "").trim();
372
+ if (!/^[0-9]+$/.test(normalizedRequestId) || BigInt(normalizedRequestId) <= 0n) {
373
+ throw new Error("requestId must be a positive base-10 integer string.");
374
+ }
375
+ return {
376
+ operation: normalizedOperation,
377
+ requestId: BigInt(normalizedRequestId),
378
+ };
379
+ }
380
+ return {
381
+ operation: normalizedOperation,
382
+ amount: assertPositiveBigIntString(amount, "amount"),
383
+ };
384
+ }
385
+
386
+ function parseOptionalDecimalBigInt(value) {
387
+ const normalized = String(value ?? "").trim();
388
+ if (!/^[0-9]+$/.test(normalized)) {
389
+ return null;
390
+ }
391
+ return BigInt(normalized);
392
+ }
393
+
394
+ function parseOptionalHexOrDecimalBigInt(value) {
395
+ const normalized = String(value ?? "").trim();
396
+ if (!normalized) {
397
+ return null;
398
+ }
399
+ if (/^0x[0-9a-fA-F]+$/.test(normalized) || /^[0-9]+$/.test(normalized)) {
400
+ return BigInt(normalized);
401
+ }
402
+ return null;
403
+ }
404
+
405
+ function computeMinimumOutputAmount(destAmount, slippageBps) {
406
+ const amount = BigInt(destAmount);
407
+ const bps = BigInt(slippageBps);
408
+ if (bps <= 0n) {
409
+ return amount;
410
+ }
411
+ return (amount * (10000n - bps)) / 10000n;
412
+ }
413
+
414
+ function assertValidHash(value, fieldName) {
415
+ const hash = assertNonEmptyString(value, fieldName);
416
+ if (!/^0x[a-fA-F0-9]{64}$/.test(hash)) {
417
+ throw new Error(`${fieldName} must be a valid 32-byte transaction hash.`);
418
+ }
419
+ return hash;
420
+ }
421
+
422
+ function stripHexPrefix(value) {
423
+ return String(value || "").startsWith("0x") ? String(value).slice(2) : String(value || "");
424
+ }
425
+
426
+ function toRpcHex(value) {
427
+ const numeric = BigInt(value || 0);
428
+ return `0x${numeric.toString(16)}`;
429
+ }
430
+
431
+ function parseHexOrDecimalBigInt(value, fieldName) {
432
+ const normalized = String(value ?? "0").trim();
433
+ if (/^0x[0-9a-fA-F]+$/.test(normalized)) {
434
+ return BigInt(normalized);
435
+ }
436
+ if (/^[0-9]+$/.test(normalized)) {
437
+ return BigInt(normalized);
438
+ }
439
+ throw new Error(`${fieldName} must be a hex or base-10 integer string.`);
440
+ }
441
+
442
+ function leftPadHex(value, length = 64) {
443
+ return stripHexPrefix(value).toLowerCase().padStart(length, "0");
444
+ }
445
+
446
+ function buildBalanceOfCallData(owner) {
447
+ return `${ERC20_BALANCE_OF_SELECTOR}${leftPadHex(normalizeAddress(owner, "owner"))}`;
448
+ }
449
+
450
+ function sha256Hex(value) {
451
+ return crypto.createHash("sha256").update(String(value || ""), "utf8").digest("hex");
452
+ }
453
+
454
+ function normalizeErrorCodeValue(error) {
455
+ if (!error || typeof error !== "object") {
456
+ return "";
457
+ }
458
+ return String(error.errorCode || error.code || "").trim().toLowerCase();
459
+ }
460
+
461
+ function decodeUint256Result(value, fieldName) {
462
+ const hex = stripHexPrefix(value);
463
+ if (!hex || !/^[0-9a-fA-F]+$/.test(hex)) {
464
+ throw new Error(`${fieldName} returned invalid hex data.`);
465
+ }
466
+ return BigInt(`0x${hex}`);
467
+ }
468
+
469
+ function decodeAbiStringResult(value, fieldName) {
470
+ const hex = stripHexPrefix(value);
471
+ if (!hex || !/^[0-9a-fA-F]+$/.test(hex) || hex.length % 2 !== 0) {
472
+ throw new Error(`${fieldName} returned invalid hex data.`);
473
+ }
474
+ if (hex.length === 64) {
475
+ const buffer = Buffer.from(hex, "hex");
476
+ const end = buffer.indexOf(0);
477
+ return buffer.slice(0, end >= 0 ? end : undefined).toString("utf8");
478
+ }
479
+ if (hex.length < 128) {
480
+ throw new Error(`${fieldName} returned an unsupported ABI payload.`);
481
+ }
482
+ const offset = Number(decodeUint256Result(`0x${hex.slice(0, 64)}`, fieldName));
483
+ const offsetHexIndex = offset * 2;
484
+ const lengthIndex = offsetHexIndex + 64;
485
+ if (offsetHexIndex + 64 > hex.length || lengthIndex > hex.length) {
486
+ throw new Error(`${fieldName} returned a truncated ABI payload.`);
487
+ }
488
+ const byteLength = Number(
489
+ decodeUint256Result(`0x${hex.slice(offsetHexIndex, offsetHexIndex + 64)}`, fieldName)
490
+ );
491
+ const dataStart = offsetHexIndex + 64;
492
+ const dataEnd = dataStart + byteLength * 2;
493
+ if (dataEnd > hex.length) {
494
+ throw new Error(`${fieldName} returned a truncated ABI string payload.`);
495
+ }
496
+ return Buffer.from(hex.slice(dataStart, dataEnd), "hex").toString("utf8");
497
+ }
498
+
499
+ function formatUnits(value, decimals = 18) {
500
+ const sign = value < 0n ? "-" : "";
501
+ const absolute = value < 0n ? value * -1n : value;
502
+ const base = 10n ** BigInt(decimals);
503
+ const whole = absolute / base;
504
+ const fraction = absolute % base;
505
+ if (fraction === 0n) {
506
+ return `${sign}${whole.toString()}`;
507
+ }
508
+ const fractionText = fraction.toString().padStart(decimals, "0").replace(/0+$/, "");
509
+ return `${sign}${whole.toString()}.${fractionText}`;
510
+ }
511
+
512
+ function rayMul(value, rayValue) {
513
+ return (BigInt(value || 0) * BigInt(rayValue || 0)) / AAVE_RAY;
514
+ }
515
+
516
+ function formatBasisPoints(value) {
517
+ return formatUnits(BigInt(value || 0), 2);
518
+ }
519
+
520
+ function formatRayAprPercent(value) {
521
+ return formatUnits(BigInt(value || 0), 25);
522
+ }
523
+
524
+ function computeAaveUsdPriceRaw(priceInMarketReferenceCurrency, baseCurrencyInfo) {
525
+ const marketReferenceCurrencyUnit = BigInt(baseCurrencyInfo?.marketReferenceCurrencyUnit || 0);
526
+ const marketReferenceCurrencyPriceInUsd = BigInt(baseCurrencyInfo?.marketReferenceCurrencyPriceInUsd || 0);
527
+ if (marketReferenceCurrencyUnit <= 0n || marketReferenceCurrencyPriceInUsd <= 0n) {
528
+ return null;
529
+ }
530
+ return (
531
+ (BigInt(priceInMarketReferenceCurrency || 0) * marketReferenceCurrencyPriceInUsd) /
532
+ marketReferenceCurrencyUnit
533
+ );
534
+ }
535
+
536
+ function computeAaveUsdValueRaw(amountRaw, decimals, priceUsdRaw) {
537
+ if (priceUsdRaw === null || priceUsdRaw === undefined) {
538
+ return null;
539
+ }
540
+ const scale = 10n ** BigInt(Number.isInteger(decimals) ? decimals : 18);
541
+ if (scale <= 0n) {
542
+ return null;
543
+ }
544
+ return (BigInt(amountRaw || 0) * BigInt(priceUsdRaw)) / scale;
545
+ }
546
+
547
+ function withLidoMetadataDefaults(metadata, defaults) {
548
+ const resolved = metadata && typeof metadata === "object" ? { ...metadata } : {};
549
+ return {
550
+ address: String(resolved.address || defaults.address).toLowerCase(),
551
+ name: resolved.name || defaults.name,
552
+ symbol: resolved.symbol || defaults.symbol,
553
+ decimals: Number.isInteger(resolved.decimals) ? resolved.decimals : defaults.decimals,
554
+ verified: resolved.verified === true,
555
+ source: resolved.source || "lido-catalog",
556
+ };
557
+ }
558
+
559
+ async function fetchJson(url, { headers = {} } = {}) {
560
+ let response;
561
+ try {
562
+ response = await fetch(url, {
563
+ headers: {
564
+ Accept: "application/json",
565
+ ...headers,
566
+ },
567
+ });
568
+ } catch (error) {
569
+ const message = error instanceof Error ? error.message : String(error);
570
+ throw createTaggedError(`HTTP network unavailable: ${message}`, "network_unavailable", {
571
+ url,
572
+ });
573
+ }
574
+ let payload;
575
+ try {
576
+ payload = await response.json();
577
+ } catch (error) {
578
+ const message = error instanceof Error ? error.message : String(error);
579
+ throw createTaggedError(`HTTP returned invalid JSON: ${message}`, "network_unavailable", {
580
+ url,
581
+ httpStatus: response.status,
582
+ });
583
+ }
584
+ if (!response.ok) {
585
+ throw createTaggedError(`HTTP request failed with status ${response.status}.`, "network_unavailable", {
586
+ url,
587
+ httpStatus: response.status,
588
+ payload,
589
+ });
590
+ }
591
+ return payload;
592
+ }
593
+
594
+ async function rpcRequest(providerUrl, method, params = []) {
595
+ let response;
596
+ try {
597
+ response = await fetch(providerUrl, {
598
+ method: "POST",
599
+ headers: {
600
+ "Content-Type": "application/json",
601
+ },
602
+ body: JSON.stringify({
603
+ jsonrpc: "2.0",
604
+ id: 1,
605
+ method,
606
+ params,
607
+ }),
608
+ });
609
+ } catch (error) {
610
+ const message = error instanceof Error ? error.message : String(error);
611
+ throw createTaggedError(`RPC network unavailable: ${message}`, "network_unavailable", {
612
+ providerUrl,
613
+ rpcMethod: method,
614
+ });
615
+ }
616
+ if (!response.ok) {
617
+ throw createTaggedError(`RPC request failed with HTTP ${response.status}.`, "network_unavailable", {
618
+ providerUrl,
619
+ rpcMethod: method,
620
+ httpStatus: response.status,
621
+ });
622
+ }
623
+ let payload;
624
+ try {
625
+ payload = await response.json();
626
+ } catch (error) {
627
+ const message = error instanceof Error ? error.message : String(error);
628
+ throw createTaggedError(`RPC returned invalid JSON: ${message}`, "network_unavailable", {
629
+ providerUrl,
630
+ rpcMethod: method,
631
+ });
632
+ }
633
+ if (payload?.error) {
634
+ const rpcMessage = payload.error.message || `RPC ${method} failed.`;
635
+ const error = new Error(rpcMessage);
636
+ if (payload.error.code !== undefined && payload.error.code !== null) {
637
+ error.code = String(payload.error.code);
638
+ }
639
+ error.errorDetails = {
640
+ providerUrl,
641
+ rpcMethod: method,
642
+ rpcCode: payload.error.code,
643
+ };
644
+ throw error;
645
+ }
646
+ return payload.result;
647
+ }
648
+
649
+ async function ethCall(providerUrl, to, data) {
650
+ return rpcRequest(providerUrl, "eth_call", [{ to, data }, "latest"]);
651
+ }
652
+
653
+ async function ethCallTransaction(providerUrl, tx) {
654
+ return rpcRequest(providerUrl, "eth_call", [tx, "latest"]);
655
+ }
656
+
657
+ async function callContract(providerUrl, to, contractInterface, functionName, args = [], txOverrides = {}) {
658
+ const data = contractInterface.encodeFunctionData(functionName, args);
659
+ const raw = await ethCallTransaction(providerUrl, {
660
+ to,
661
+ data,
662
+ ...txOverrides,
663
+ });
664
+ return contractInterface.decodeFunctionResult(functionName, raw);
665
+ }
666
+
667
+ function buildErc20ApproveTransaction(tokenAddress, spender, amount) {
668
+ return {
669
+ to: normalizeAddress(tokenAddress, "tokenAddress"),
670
+ value: 0n,
671
+ data: `${ERC20_APPROVE_SELECTOR}${leftPadHex(
672
+ normalizeAddress(spender, "spender")
673
+ )}${leftPadHex(BigInt(amount).toString(16))}`,
674
+ };
675
+ }
676
+
677
+ function isRecoverableSwapFeeEstimateFailure(error) {
678
+ const code = normalizeErrorCodeValue(error);
679
+ const message = error instanceof Error ? error.message : String(error || "");
680
+ const lower = message.toLowerCase();
681
+ if (
682
+ code === "insufficient_funds" ||
683
+ code === "call_exception" ||
684
+ code === "execution_reverted" ||
685
+ code === "bad_data"
686
+ ) {
687
+ return true;
688
+ }
689
+ return (
690
+ lower.includes("execution reverted") ||
691
+ lower.includes("insufficient funds") ||
692
+ lower.includes("estimategas") ||
693
+ lower.includes("missing revert data") ||
694
+ lower.includes("call_exception")
695
+ );
696
+ }
697
+
698
+ function parseInsufficientFundsHint(error) {
699
+ const message = error instanceof Error ? error.message : String(error || "");
700
+ const match = message.match(/have\s+([0-9]+)\s+want\s+([0-9]+)/i);
701
+ if (!match) {
702
+ return null;
703
+ }
704
+ const available = BigInt(match[1]);
705
+ const required = BigInt(match[2]);
706
+ return {
707
+ availableNativeBalanceWei: available.toString(),
708
+ requiredNativeBalanceWei: required.toString(),
709
+ missingNativeBalanceWei: (required > available ? required - available : 0n).toString(),
710
+ };
711
+ }
712
+
713
+ function isRecoverableAllowanceReadFailure(error) {
714
+ const code = normalizeErrorCodeValue(error);
715
+ const message = error instanceof Error ? error.message : String(error || "");
716
+ const lower = message.toLowerCase();
717
+ if (code === "bad_data" || code === "call_exception" || code === "buffer_overrun") {
718
+ return true;
719
+ }
720
+ return (
721
+ lower.includes("could not decode result data") ||
722
+ lower.includes("allowance(address,address)") ||
723
+ lower.includes('value="0x"') ||
724
+ lower.includes("bad data") ||
725
+ lower.includes("buffer overrun")
726
+ );
727
+ }
728
+
729
+ function isRecoverableTokenBalanceReadFailure(error) {
730
+ const code = normalizeErrorCodeValue(error);
731
+ const message = error instanceof Error ? error.message : String(error || "");
732
+ const lower = message.toLowerCase();
733
+ if (code === "bad_data" || code === "call_exception" || code === "buffer_overrun") {
734
+ return true;
735
+ }
736
+ return (
737
+ lower.includes("missing revert data") ||
738
+ lower.includes("could not decode result data") ||
739
+ lower.includes("balanceof(address)") ||
740
+ lower.includes('value="0x"') ||
741
+ lower.includes("bad data") ||
742
+ lower.includes("buffer overrun")
743
+ );
744
+ }
745
+
746
+ function isRecoverableTokenTransferSimulationFailure(error) {
747
+ const code = normalizeErrorCodeValue(error);
748
+ const message = error instanceof Error ? error.message : String(error || "");
749
+ const lower = message.toLowerCase();
750
+ if (code === "insufficient_funds" || lower.includes("insufficient funds")) {
751
+ return false;
752
+ }
753
+ if (code === "bad_data" || code === "call_exception" || code === "execution_reverted") {
754
+ return true;
755
+ }
756
+ return (
757
+ lower.includes("missing revert data") ||
758
+ lower.includes("execution reverted") ||
759
+ lower.includes("call exception") ||
760
+ lower.includes("call_exception") ||
761
+ lower.includes("could not decode result data")
762
+ );
763
+ }
764
+
765
+ async function maybeDispose(value) {
766
+ if (value && typeof value.dispose === "function") {
767
+ await value.dispose();
768
+ }
769
+ if (value && typeof value.close === "function") {
770
+ await value.close();
771
+ }
772
+ }
773
+
774
+ export class WdkEvmWalletService {
775
+ constructor(config) {
776
+ this.config = config;
777
+ this._tokenMetadataCache = new Map();
778
+ }
779
+
780
+ generateSeedPhrase(words = 12) {
781
+ const count = Number(words);
782
+ if (!Number.isInteger(count) || count !== 12) {
783
+ throw new Error(
784
+ "Only 12-word seed phrase generation is exposed by this service because that is the documented WDK helper surface."
785
+ );
786
+ }
787
+ return {
788
+ seedPhrase: WDK.getRandomSeedPhrase(),
789
+ wordCount: count,
790
+ source: "wdk",
791
+ };
792
+ }
793
+
794
+ async resolveAddress({ seedPhrase, accountIndex = 0, network }) {
795
+ return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => ({
796
+ network: runtimeConfig.network,
797
+ chainId: runtimeConfig.chainId,
798
+ accountIndex,
799
+ address: await account.getAddress(),
800
+ source: "wdk-wallet-evm",
801
+ }));
802
+ }
803
+
804
+ async getBalance({ seedPhrase, address, accountIndex = 0, network }) {
805
+ return this.#withReadableAccount(
806
+ { seedPhrase, address, accountIndex, network },
807
+ async (account, runtimeConfig) => {
808
+ const address = await account.getAddress();
809
+ const balance = await account.getBalance();
810
+ return {
811
+ network: runtimeConfig.network,
812
+ chainId: runtimeConfig.chainId,
813
+ nativeSymbol: runtimeConfig.nativeSymbol,
814
+ accountIndex,
815
+ address,
816
+ balance,
817
+ balanceFormatted: formatUnits(BigInt(balance), 18),
818
+ source: "wdk-wallet-evm",
819
+ };
820
+ }
821
+ );
822
+ }
823
+
824
+ async getTokenBalance({ seedPhrase, address, tokenAddress, accountIndex = 0, network }) {
825
+ return this.#withReadableAccount(
826
+ { seedPhrase, address, accountIndex, network },
827
+ async (account, runtimeConfig) => {
828
+ const address = await account.getAddress();
829
+ const token = normalizeAddress(tokenAddress, "tokenAddress");
830
+ const balance = await this.#readTokenBalanceWithFallback({
831
+ account,
832
+ runtimeConfig,
833
+ tokenAddress: token,
834
+ ownerAddress: address,
835
+ });
836
+ const tokenMetadata = await this.#getBestEffortTokenMetadata(runtimeConfig, token);
837
+ return {
838
+ network: runtimeConfig.network,
839
+ chainId: runtimeConfig.chainId,
840
+ accountIndex,
841
+ address,
842
+ tokenAddress: token,
843
+ balance,
844
+ balanceFormatted:
845
+ tokenMetadata && Number.isInteger(tokenMetadata.decimals)
846
+ ? formatUnits(BigInt(balance), tokenMetadata.decimals)
847
+ : null,
848
+ tokenMetadata,
849
+ source: "wdk-wallet-evm",
850
+ };
851
+ }
852
+ );
853
+ }
854
+
855
+ async getTokenMetadata({ tokenAddress, network }) {
856
+ const runtimeConfig = this.#resolveRuntimeConfig(network);
857
+ const token = normalizeAddress(tokenAddress, "tokenAddress");
858
+ return {
859
+ network: runtimeConfig.network,
860
+ chainId: runtimeConfig.chainId,
861
+ tokenAddress: token,
862
+ tokenMetadata: await this.#getTokenMetadata(runtimeConfig, token),
863
+ source: "erc20-rpc",
864
+ };
865
+ }
866
+
867
+ async getFeeRates({ network } = {}) {
868
+ const runtimeConfig = this.#resolveRuntimeConfig(network);
869
+ const gasPriceHex = await rpcRequest(runtimeConfig.providerUrl, "eth_gasPrice", []);
870
+ const priorityHex = await rpcRequest(
871
+ runtimeConfig.providerUrl,
872
+ "eth_maxPriorityFeePerGas",
873
+ []
874
+ );
875
+ const feeHistory = await rpcRequest(
876
+ runtimeConfig.providerUrl,
877
+ "eth_feeHistory",
878
+ ["0x1", "latest", []]
879
+ );
880
+ const baseFeeItems = Array.isArray(feeHistory?.baseFeePerGas) ? feeHistory.baseFeePerGas : [];
881
+ const latestBaseFeeHex = baseFeeItems.length ? baseFeeItems[baseFeeItems.length - 1] : "0x0";
882
+ const baseFeePerGas = BigInt(latestBaseFeeHex);
883
+ const priorityFeePerGas = BigInt(priorityHex || "0x0");
884
+ const gasPrice = BigInt(gasPriceHex || "0x0");
885
+ const normalMaxFeePerGas = baseFeePerGas + priorityFeePerGas;
886
+ const fastMaxFeePerGas = baseFeePerGas * 2n + priorityFeePerGas;
887
+ return {
888
+ network: runtimeConfig.network,
889
+ chainId: runtimeConfig.chainId,
890
+ gasPrice,
891
+ feeRates: {
892
+ slow: gasPrice,
893
+ normal: normalMaxFeePerGas,
894
+ fast: fastMaxFeePerGas,
895
+ baseFeePerGas,
896
+ maxPriorityFeePerGas: priorityFeePerGas,
897
+ },
898
+ source: "rpc",
899
+ };
900
+ }
901
+
902
+ async getTransactionReceipt({ txHash, network }) {
903
+ const runtimeConfig = this.#resolveRuntimeConfig(network);
904
+ const receipt = await rpcRequest(
905
+ runtimeConfig.providerUrl,
906
+ "eth_getTransactionReceipt",
907
+ [assertValidHash(txHash, "txHash")]
908
+ );
909
+ return {
910
+ network: runtimeConfig.network,
911
+ chainId: runtimeConfig.chainId,
912
+ txHash,
913
+ receipt,
914
+ found: receipt !== null,
915
+ source: "rpc",
916
+ };
917
+ }
918
+
919
+ async getAaveAccountData({ seedPhrase, address, accountIndex = 0, network }) {
920
+ return this.#withReadableAccount(
921
+ { seedPhrase, address, accountIndex, network },
922
+ async (account, runtimeConfig) => {
923
+ assertAaveSupportedNetwork(runtimeConfig.network);
924
+ const accountAddress = await account.getAddress();
925
+ const protocol = new AaveProtocolEvm(account);
926
+ try {
927
+ const accountData = await protocol.getAccountData(accountAddress);
928
+ return {
929
+ network: runtimeConfig.network,
930
+ chainId: runtimeConfig.chainId,
931
+ accountIndex,
932
+ address: accountAddress,
933
+ protocol: "aave-v3",
934
+ accountData: this.#formatAaveAccountData(accountData),
935
+ source: "wdk-protocol-lending-aave-evm",
936
+ };
937
+ } finally {
938
+ await maybeDispose(protocol);
939
+ }
940
+ }
941
+ );
942
+ }
943
+
944
+ async getAaveReserves({ seedPhrase, address, accountIndex = 0, network }) {
945
+ return this.#withReadableAccount(
946
+ { seedPhrase, address, accountIndex, network },
947
+ async (account, runtimeConfig) => {
948
+ assertAaveSupportedNetwork(runtimeConfig.network);
949
+ const protocol = new AaveProtocolEvm(account);
950
+ try {
951
+ const catalog = await this.#readAaveReserveCatalog(protocol);
952
+ return {
953
+ network: runtimeConfig.network,
954
+ chainId: runtimeConfig.chainId,
955
+ accountIndex,
956
+ protocol: "aave-v3",
957
+ pool: catalog.addresses.pool,
958
+ poolAddressesProvider: catalog.addresses.poolAddressesProvider,
959
+ uiPoolDataProvider: catalog.addresses.uiPoolDataProvider,
960
+ priceOracle: catalog.addresses.priceOracle,
961
+ baseCurrencyInfo: catalog.baseCurrencyInfo,
962
+ reserveCount: catalog.reserves.length,
963
+ reserves: catalog.reserves,
964
+ source: "wdk-protocol-lending-aave-evm",
965
+ };
966
+ } finally {
967
+ await maybeDispose(protocol);
968
+ }
969
+ }
970
+ );
971
+ }
972
+
973
+ async getAavePositions({ seedPhrase, address, accountIndex = 0, network }) {
974
+ return this.#withReadableAccount(
975
+ { seedPhrase, address, accountIndex, network },
976
+ async (account, runtimeConfig) => {
977
+ assertAaveSupportedNetwork(runtimeConfig.network);
978
+ const accountAddress = await account.getAddress();
979
+ const protocol = new AaveProtocolEvm(account);
980
+ try {
981
+ const catalog = await this.#readAaveReserveCatalog(protocol);
982
+ const poolContract = await protocol._getPoolContract();
983
+ const eModeCategoryIdRaw =
984
+ poolContract && typeof poolContract.getUserEMode === "function"
985
+ ? await poolContract.getUserEMode(accountAddress)
986
+ : 0n;
987
+ const protocolDataProviderContract = this.#getAaveProtocolDataProviderContract(
988
+ runtimeConfig.network,
989
+ protocol
990
+ );
991
+ const accountData = await protocol.getAccountData(accountAddress);
992
+ const userReserveEntries = await Promise.all(
993
+ catalog.reserves.map(async (reserve) => ({
994
+ reserve,
995
+ userReserve: await protocolDataProviderContract.getUserReserveData(
996
+ reserve.underlyingAsset,
997
+ accountAddress
998
+ ),
999
+ }))
1000
+ );
1001
+ const positions = [];
1002
+ for (const { reserve, userReserve } of userReserveEntries) {
1003
+ const liquidityIndexRaw = BigInt(reserve?.liquidityIndexRaw || 0);
1004
+ const suppliedBalance = BigInt(userReserve?.currentATokenBalance || 0);
1005
+ const currentStableDebt = BigInt(userReserve?.currentStableDebt || 0);
1006
+ const variableDebt = BigInt(userReserve?.currentVariableDebt || 0);
1007
+ const principalStableDebt = BigInt(userReserve?.principalStableDebt || 0);
1008
+ const scaledVariableDebt = BigInt(userReserve?.scaledVariableDebt || 0);
1009
+ const scaledATokenBalance =
1010
+ liquidityIndexRaw > 0n
1011
+ ? (suppliedBalance * AAVE_RAY) / liquidityIndexRaw
1012
+ : 0n;
1013
+ if (
1014
+ suppliedBalance <= 0n &&
1015
+ variableDebt <= 0n &&
1016
+ currentStableDebt <= 0n &&
1017
+ principalStableDebt <= 0n &&
1018
+ !Boolean(userReserve?.usageAsCollateralEnabled)
1019
+ ) {
1020
+ continue;
1021
+ }
1022
+ const suppliedValueUsdRaw = computeAaveUsdValueRaw(
1023
+ suppliedBalance,
1024
+ reserve.decimals,
1025
+ reserve.priceInUsdRaw
1026
+ );
1027
+ const variableDebtValueUsdRaw = computeAaveUsdValueRaw(
1028
+ variableDebt,
1029
+ reserve.decimals,
1030
+ reserve.priceInUsdRaw
1031
+ );
1032
+ const currentStableDebtValueUsdRaw = computeAaveUsdValueRaw(
1033
+ currentStableDebt,
1034
+ reserve.decimals,
1035
+ reserve.priceInUsdRaw
1036
+ );
1037
+ const principalStableDebtValueUsdRaw = computeAaveUsdValueRaw(
1038
+ principalStableDebt,
1039
+ reserve.decimals,
1040
+ reserve.priceInUsdRaw
1041
+ );
1042
+ positions.push({
1043
+ underlyingAsset: reserve.underlyingAsset,
1044
+ name: reserve.name,
1045
+ symbol: reserve.symbol,
1046
+ decimals: reserve.decimals,
1047
+ aTokenAddress: reserve.aTokenAddress,
1048
+ variableDebtTokenAddress: reserve.variableDebtTokenAddress,
1049
+ collateralEnabled: Boolean(userReserve?.usageAsCollateralEnabled),
1050
+ suppliedBalanceRaw: suppliedBalance.toString(),
1051
+ suppliedBalanceFormatted: formatUnits(suppliedBalance, reserve.decimals),
1052
+ suppliedValueUsdRaw: suppliedValueUsdRaw !== null ? suppliedValueUsdRaw.toString() : null,
1053
+ suppliedValueUsdFormatted:
1054
+ suppliedValueUsdRaw !== null
1055
+ ? formatUnits(suppliedValueUsdRaw, catalog.baseCurrencyInfo.usdDecimals)
1056
+ : null,
1057
+ scaledATokenBalanceRaw: scaledATokenBalance.toString(),
1058
+ variableDebtRaw: variableDebt.toString(),
1059
+ variableDebtFormatted: formatUnits(variableDebt, reserve.decimals),
1060
+ variableDebtValueUsdRaw:
1061
+ variableDebtValueUsdRaw !== null ? variableDebtValueUsdRaw.toString() : null,
1062
+ variableDebtValueUsdFormatted:
1063
+ variableDebtValueUsdRaw !== null
1064
+ ? formatUnits(variableDebtValueUsdRaw, catalog.baseCurrencyInfo.usdDecimals)
1065
+ : null,
1066
+ scaledVariableDebtRaw: scaledVariableDebt.toString(),
1067
+ currentStableDebtRaw: currentStableDebt.toString(),
1068
+ currentStableDebtFormatted: formatUnits(currentStableDebt, reserve.decimals),
1069
+ currentStableDebtValueUsdRaw:
1070
+ currentStableDebtValueUsdRaw !== null ? currentStableDebtValueUsdRaw.toString() : null,
1071
+ currentStableDebtValueUsdFormatted:
1072
+ currentStableDebtValueUsdRaw !== null
1073
+ ? formatUnits(currentStableDebtValueUsdRaw, catalog.baseCurrencyInfo.usdDecimals)
1074
+ : null,
1075
+ principalStableDebtRaw: principalStableDebt.toString(),
1076
+ principalStableDebtFormatted: formatUnits(principalStableDebt, reserve.decimals),
1077
+ principalStableDebtValueUsdRaw:
1078
+ principalStableDebtValueUsdRaw !== null
1079
+ ? principalStableDebtValueUsdRaw.toString()
1080
+ : null,
1081
+ principalStableDebtValueUsdFormatted:
1082
+ principalStableDebtValueUsdRaw !== null
1083
+ ? formatUnits(principalStableDebtValueUsdRaw, catalog.baseCurrencyInfo.usdDecimals)
1084
+ : null,
1085
+ stableBorrowRateRaw: BigInt(userReserve?.stableBorrowRate || 0).toString(),
1086
+ stableBorrowAprPercent: formatRayAprPercent(BigInt(userReserve?.stableBorrowRate || 0)),
1087
+ stableBorrowLastUpdateTimestamp: BigInt(
1088
+ userReserve?.stableRateLastUpdated || 0
1089
+ ).toString(),
1090
+ reserve: {
1091
+ priceInUsdRaw: reserve.priceInUsdRaw !== null ? reserve.priceInUsdRaw.toString() : null,
1092
+ priceInUsdFormatted: reserve.priceInUsdFormatted,
1093
+ priceInMarketReferenceCurrency: reserve.priceInMarketReferenceCurrency,
1094
+ usageAsCollateralEnabled: reserve.usageAsCollateralEnabled,
1095
+ borrowingEnabled: reserve.borrowingEnabled,
1096
+ isActive: reserve.isActive,
1097
+ isFrozen: reserve.isFrozen,
1098
+ isPaused: reserve.isPaused,
1099
+ flashLoanEnabled: reserve.flashLoanEnabled,
1100
+ },
1101
+ });
1102
+ }
1103
+ return {
1104
+ network: runtimeConfig.network,
1105
+ chainId: runtimeConfig.chainId,
1106
+ accountIndex,
1107
+ address: accountAddress,
1108
+ protocol: "aave-v3",
1109
+ eModeCategoryId: BigInt(eModeCategoryIdRaw || 0).toString(),
1110
+ accountData: this.#formatAaveAccountData(accountData),
1111
+ baseCurrencyInfo: catalog.baseCurrencyInfo,
1112
+ positionCount: positions.length,
1113
+ positions,
1114
+ source: "wdk-protocol-lending-aave-evm",
1115
+ };
1116
+ } finally {
1117
+ await maybeDispose(protocol);
1118
+ }
1119
+ }
1120
+ );
1121
+ }
1122
+
1123
+ async quoteAaveOperation({
1124
+ seedPhrase,
1125
+ address,
1126
+ operation,
1127
+ token,
1128
+ tokenAddress,
1129
+ amount,
1130
+ accountIndex = 0,
1131
+ network,
1132
+ }) {
1133
+ return this.#withReadableAccount(
1134
+ { seedPhrase, address, accountIndex, network },
1135
+ async (account, runtimeConfig) => {
1136
+ assertAaveSupportedNetwork(runtimeConfig.network);
1137
+ const request = buildAaveOperationRequest({
1138
+ operation,
1139
+ token,
1140
+ tokenAddress,
1141
+ amount,
1142
+ });
1143
+ const accountAddress = await account.getAddress();
1144
+ const plan = await this.#buildAaveOperationPlan({
1145
+ account,
1146
+ runtimeConfig,
1147
+ address: accountAddress,
1148
+ request,
1149
+ tolerateOperationFeeFailure: true,
1150
+ });
1151
+ return this.#formatAaveOperationResponse({
1152
+ runtimeConfig,
1153
+ accountIndex,
1154
+ address: accountAddress,
1155
+ request,
1156
+ plan,
1157
+ });
1158
+ }
1159
+ );
1160
+ }
1161
+
1162
+ async sendAaveOperation({
1163
+ seedPhrase,
1164
+ operation,
1165
+ token,
1166
+ tokenAddress,
1167
+ amount,
1168
+ accountIndex = 0,
1169
+ network,
1170
+ expectedQuoteFingerprint = null,
1171
+ }) {
1172
+ return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
1173
+ assertAaveSupportedNetwork(runtimeConfig.network);
1174
+ const request = buildAaveOperationRequest({
1175
+ operation,
1176
+ token,
1177
+ tokenAddress,
1178
+ amount,
1179
+ });
1180
+ const normalizedExpectedQuoteFingerprint =
1181
+ typeof expectedQuoteFingerprint === "string" && expectedQuoteFingerprint.trim()
1182
+ ? expectedQuoteFingerprint.trim()
1183
+ : null;
1184
+ const address = await account.getAddress();
1185
+ let initialPlan = await this.#buildAaveOperationPlan({
1186
+ account,
1187
+ runtimeConfig,
1188
+ address,
1189
+ request,
1190
+ tolerateOperationFeeFailure: true,
1191
+ });
1192
+ this.#assertExpectedAaveFingerprint(
1193
+ normalizedExpectedQuoteFingerprint,
1194
+ initialPlan.quoteFingerprint
1195
+ );
1196
+
1197
+ const approvalExecution = await this.#executeAaveApprovalsIfNeeded({
1198
+ account,
1199
+ runtimeConfig,
1200
+ request,
1201
+ plan: initialPlan,
1202
+ });
1203
+
1204
+ let finalPlan = initialPlan;
1205
+ try {
1206
+ if (approvalExecution.performed) {
1207
+ finalPlan = await this.#buildAaveOperationPlan({
1208
+ account,
1209
+ runtimeConfig,
1210
+ address,
1211
+ request,
1212
+ });
1213
+ this.#assertExpectedAaveFingerprint(
1214
+ normalizedExpectedQuoteFingerprint,
1215
+ finalPlan.quoteFingerprint
1216
+ );
1217
+ }
1218
+
1219
+ if (finalPlan.approval.required) {
1220
+ throw createTaggedError(
1221
+ "Aave operation still requires token approval after the approval step completed.",
1222
+ "aave_approval_required",
1223
+ {
1224
+ spender: finalPlan.spender,
1225
+ requiredAllowance: finalPlan.amount.toString(),
1226
+ currentAllowance: finalPlan.currentAllowance.toString(),
1227
+ }
1228
+ );
1229
+ }
1230
+
1231
+ if (finalPlan.operationFee === null) {
1232
+ throw createTaggedError(
1233
+ "Aave operation fee estimate was unavailable. Generate a new quote before sending.",
1234
+ "aave_fee_unavailable",
1235
+ {
1236
+ operation: request.operation,
1237
+ feeEstimateError: finalPlan.operationFeeError,
1238
+ }
1239
+ );
1240
+ }
1241
+
1242
+ const protocol = new AaveProtocolEvm(account);
1243
+ let result;
1244
+ try {
1245
+ result = await protocol[request.operation]({
1246
+ token: request.token,
1247
+ amount: request.amount,
1248
+ });
1249
+ } finally {
1250
+ await maybeDispose(protocol);
1251
+ }
1252
+ const resultFee = BigInt(result?.fee || 0);
1253
+ const totalFee = approvalExecution.totalFee + resultFee;
1254
+ return {
1255
+ ...this.#formatAaveOperationResponse({
1256
+ runtimeConfig,
1257
+ accountIndex,
1258
+ address,
1259
+ request,
1260
+ plan: {
1261
+ ...finalPlan,
1262
+ operationFee: resultFee,
1263
+ totalEstimatedFee: totalFee,
1264
+ approval: {
1265
+ ...finalPlan.approval,
1266
+ estimatedFee: approvalExecution.totalFee,
1267
+ },
1268
+ },
1269
+ }),
1270
+ result: {
1271
+ ...result,
1272
+ fee: resultFee.toString(),
1273
+ totalFee: totalFee.toString(),
1274
+ approvalFee: approvalExecution.totalFee.toString(),
1275
+ ...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
1276
+ ...(approvalExecution.resetAllowanceHash
1277
+ ? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
1278
+ : {}),
1279
+ },
1280
+ };
1281
+ } catch (error) {
1282
+ const cleanup = await this.#restoreAllowanceAfterFailedAaveOperation({
1283
+ account,
1284
+ runtimeConfig,
1285
+ tokenAddress: request.token,
1286
+ spender: initialPlan.spender,
1287
+ originalAllowance: initialPlan.currentAllowance,
1288
+ approvalExecution,
1289
+ });
1290
+ this.#throwAaveFailureWithCleanup(error, cleanup);
1291
+ }
1292
+ });
1293
+ }
1294
+
1295
+ async getLidoOverview({ seedPhrase, address, accountIndex = 0, network }) {
1296
+ return this.#withReadableAccount(
1297
+ { seedPhrase, address, accountIndex, network },
1298
+ async (account, runtimeConfig) => {
1299
+ assertLidoSupportedNetwork(runtimeConfig.network);
1300
+ const contracts = this.#getLidoContracts(runtimeConfig.network);
1301
+ const [stEthMetadata, wstEthMetadata, rates, stakingAprResult] = await Promise.all([
1302
+ this.#getLidoTokenMetadata(runtimeConfig, contracts.steth),
1303
+ this.#getLidoTokenMetadata(runtimeConfig, contracts.wsteth),
1304
+ this.#readLidoSampleRates(runtimeConfig),
1305
+ this.#readLidoStakingApr(runtimeConfig),
1306
+ ]);
1307
+ return {
1308
+ network: runtimeConfig.network,
1309
+ chainId: runtimeConfig.chainId,
1310
+ accountIndex,
1311
+ protocol: "lido",
1312
+ preferredPositionToken: "wstETH",
1313
+ stakingAsset: {
1314
+ type: "native",
1315
+ symbol: runtimeConfig.nativeSymbol,
1316
+ decimals: 18,
1317
+ },
1318
+ referralAddress: this.#getLidoReferralAddress(),
1319
+ contracts: {
1320
+ stETH: contracts.steth.address,
1321
+ wstETH: contracts.wsteth.address,
1322
+ referralStaker: contracts.referralStaker,
1323
+ withdrawalQueue: contracts.withdrawalQueue,
1324
+ },
1325
+ stEthMetadata,
1326
+ wstEthMetadata,
1327
+ sampleRates: rates,
1328
+ stakingApr: stakingAprResult.data,
1329
+ stakingAprError: stakingAprResult.error,
1330
+ withdrawalLimits: {
1331
+ minStEthAmountRaw: LIDO_MIN_STETH_WITHDRAWAL_AMOUNT.toString(),
1332
+ minStEthAmountFormatted: formatUnits(LIDO_MIN_STETH_WITHDRAWAL_AMOUNT, LIDO_STETH_DECIMALS),
1333
+ maxStEthAmountRaw: LIDO_MAX_STETH_WITHDRAWAL_AMOUNT.toString(),
1334
+ maxStEthAmountFormatted: formatUnits(LIDO_MAX_STETH_WITHDRAWAL_AMOUNT, LIDO_STETH_DECIMALS),
1335
+ },
1336
+ source: "lido-contracts",
1337
+ };
1338
+ }
1339
+ );
1340
+ }
1341
+
1342
+ async getLidoPositions({ seedPhrase, address, accountIndex = 0, network }) {
1343
+ return this.#withReadableAccount(
1344
+ { seedPhrase, address, accountIndex, network },
1345
+ async (account, runtimeConfig) => {
1346
+ assertLidoSupportedNetwork(runtimeConfig.network);
1347
+ const accountAddress = await account.getAddress();
1348
+ const contracts = this.#getLidoContracts(runtimeConfig.network);
1349
+ const [nativeBalance, stEthMetadata, wstEthMetadata, stEthBalance, wstEthBalance] = await Promise.all([
1350
+ account.getBalance(),
1351
+ this.#getLidoTokenMetadata(runtimeConfig, contracts.steth),
1352
+ this.#getLidoTokenMetadata(runtimeConfig, contracts.wsteth),
1353
+ this.#readTokenBalanceWithFallback({
1354
+ account,
1355
+ runtimeConfig,
1356
+ tokenAddress: contracts.steth.address,
1357
+ ownerAddress: accountAddress,
1358
+ }),
1359
+ this.#readTokenBalanceWithFallback({
1360
+ account,
1361
+ runtimeConfig,
1362
+ tokenAddress: contracts.wsteth.address,
1363
+ ownerAddress: accountAddress,
1364
+ }),
1365
+ ]);
1366
+ const wstEthAsStEth = await this.#quoteLidoOutputRaw({
1367
+ runtimeConfig,
1368
+ operation: "unwrap_wsteth",
1369
+ amount: wstEthBalance,
1370
+ fromAddress: accountAddress,
1371
+ });
1372
+ const stEthEquivalentTotal = stEthBalance + wstEthAsStEth;
1373
+ const positions = [];
1374
+ if (stEthBalance > 0n) {
1375
+ positions.push({
1376
+ asset: "stETH",
1377
+ tokenAddress: contracts.steth.address,
1378
+ tokenMetadata: stEthMetadata,
1379
+ balanceRaw: stEthBalance.toString(),
1380
+ balanceFormatted: formatUnits(stEthBalance, stEthMetadata.decimals),
1381
+ stEthEquivalentRaw: stEthBalance.toString(),
1382
+ stEthEquivalentFormatted: formatUnits(stEthBalance, stEthMetadata.decimals),
1383
+ });
1384
+ }
1385
+ if (wstEthBalance > 0n) {
1386
+ positions.push({
1387
+ asset: "wstETH",
1388
+ tokenAddress: contracts.wsteth.address,
1389
+ tokenMetadata: wstEthMetadata,
1390
+ balanceRaw: wstEthBalance.toString(),
1391
+ balanceFormatted: formatUnits(wstEthBalance, wstEthMetadata.decimals),
1392
+ stEthEquivalentRaw: wstEthAsStEth.toString(),
1393
+ stEthEquivalentFormatted: formatUnits(wstEthAsStEth, stEthMetadata.decimals),
1394
+ });
1395
+ }
1396
+ return {
1397
+ network: runtimeConfig.network,
1398
+ chainId: runtimeConfig.chainId,
1399
+ accountIndex,
1400
+ address: accountAddress,
1401
+ protocol: "lido",
1402
+ preferredPositionToken: "wstETH",
1403
+ contracts: {
1404
+ stETH: contracts.steth.address,
1405
+ wstETH: contracts.wsteth.address,
1406
+ referralStaker: contracts.referralStaker,
1407
+ withdrawalQueue: contracts.withdrawalQueue,
1408
+ },
1409
+ nativeBalanceWei: BigInt(nativeBalance || 0).toString(),
1410
+ nativeBalanceFormatted: formatUnits(BigInt(nativeBalance || 0), 18),
1411
+ stEthEquivalentTotalRaw: stEthEquivalentTotal.toString(),
1412
+ stEthEquivalentTotalFormatted: formatUnits(stEthEquivalentTotal, stEthMetadata.decimals),
1413
+ positionCount: positions.length,
1414
+ positions,
1415
+ source: "lido-contracts",
1416
+ };
1417
+ }
1418
+ );
1419
+ }
1420
+
1421
+ async getLidoWithdrawalRequests({ seedPhrase, address, accountIndex = 0, network }) {
1422
+ return this.#withReadableAccount(
1423
+ { seedPhrase, address, accountIndex, network },
1424
+ async (account, runtimeConfig) => {
1425
+ assertLidoSupportedNetwork(runtimeConfig.network);
1426
+ const accountAddress = await account.getAddress();
1427
+ const contracts = this.#getLidoContracts(runtimeConfig.network);
1428
+ const [stEthMetadata, wstEthMetadata, requestIds] = await Promise.all([
1429
+ this.#getLidoTokenMetadata(runtimeConfig, contracts.steth),
1430
+ this.#getLidoTokenMetadata(runtimeConfig, contracts.wsteth),
1431
+ this.#getLidoWithdrawalRequestIds(runtimeConfig, accountAddress),
1432
+ ]);
1433
+ const statuses = requestIds.length
1434
+ ? await this.#getLidoWithdrawalStatuses(runtimeConfig, requestIds)
1435
+ : [];
1436
+ const requests = statuses.map((status) =>
1437
+ this.#formatLidoWithdrawalStatus(status, stEthMetadata, wstEthMetadata)
1438
+ );
1439
+ const claimableCount = requests.filter((request) => request.claimable).length;
1440
+ return {
1441
+ network: runtimeConfig.network,
1442
+ chainId: runtimeConfig.chainId,
1443
+ accountIndex,
1444
+ address: accountAddress,
1445
+ protocol: "lido",
1446
+ withdrawalQueue: contracts.withdrawalQueue,
1447
+ requestCount: requests.length,
1448
+ claimableCount,
1449
+ requests,
1450
+ source: "lido-contracts",
1451
+ };
1452
+ }
1453
+ );
1454
+ }
1455
+
1456
+ async quoteLidoOperation({
1457
+ seedPhrase,
1458
+ address,
1459
+ operation,
1460
+ amount,
1461
+ accountIndex = 0,
1462
+ network,
1463
+ }) {
1464
+ return this.#withReadableAccount(
1465
+ { seedPhrase, address, accountIndex, network },
1466
+ async (account, runtimeConfig) => {
1467
+ assertLidoSupportedNetwork(runtimeConfig.network);
1468
+ const request = buildLidoOperationRequest({ operation, amount });
1469
+ const accountAddress = await account.getAddress();
1470
+ const plan = await this.#buildLidoOperationPlan({
1471
+ account,
1472
+ runtimeConfig,
1473
+ address: accountAddress,
1474
+ request,
1475
+ tolerateOperationFeeFailure: true,
1476
+ });
1477
+ return this.#formatLidoOperationResponse({
1478
+ runtimeConfig,
1479
+ accountIndex,
1480
+ address: accountAddress,
1481
+ request,
1482
+ plan,
1483
+ });
1484
+ }
1485
+ );
1486
+ }
1487
+
1488
+ async sendLidoOperation({
1489
+ seedPhrase,
1490
+ operation,
1491
+ amount,
1492
+ accountIndex = 0,
1493
+ network,
1494
+ expectedQuoteFingerprint = null,
1495
+ }) {
1496
+ return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
1497
+ assertLidoSupportedNetwork(runtimeConfig.network);
1498
+ const request = buildLidoOperationRequest({ operation, amount });
1499
+ const normalizedExpectedQuoteFingerprint =
1500
+ typeof expectedQuoteFingerprint === "string" && expectedQuoteFingerprint.trim()
1501
+ ? expectedQuoteFingerprint.trim()
1502
+ : null;
1503
+ const address = await account.getAddress();
1504
+ let initialPlan = await this.#buildLidoOperationPlan({
1505
+ account,
1506
+ runtimeConfig,
1507
+ address,
1508
+ request,
1509
+ tolerateOperationFeeFailure: true,
1510
+ });
1511
+ this.#assertExpectedLidoFingerprint(
1512
+ normalizedExpectedQuoteFingerprint,
1513
+ initialPlan.quoteFingerprint
1514
+ );
1515
+
1516
+ const approvalExecution = await this.#executeLidoApprovalsIfNeeded({
1517
+ account,
1518
+ runtimeConfig,
1519
+ request,
1520
+ plan: initialPlan,
1521
+ });
1522
+
1523
+ let finalPlan = initialPlan;
1524
+ try {
1525
+ if (approvalExecution.performed) {
1526
+ finalPlan = await this.#buildLidoOperationPlan({
1527
+ account,
1528
+ runtimeConfig,
1529
+ address,
1530
+ request,
1531
+ });
1532
+ this.#assertExpectedLidoFingerprint(
1533
+ normalizedExpectedQuoteFingerprint,
1534
+ finalPlan.quoteFingerprint
1535
+ );
1536
+ }
1537
+
1538
+ if (finalPlan.approval.required) {
1539
+ throw createTaggedError(
1540
+ "Lido operation still requires token approval after the approval step completed.",
1541
+ "lido_approval_required",
1542
+ {
1543
+ spender: finalPlan.spender,
1544
+ requiredAllowance: finalPlan.amount.toString(),
1545
+ currentAllowance: finalPlan.currentAllowance.toString(),
1546
+ }
1547
+ );
1548
+ }
1549
+
1550
+ if (finalPlan.operationFee === null) {
1551
+ throw createTaggedError(
1552
+ "Lido operation fee estimate was unavailable. Generate a new quote before sending.",
1553
+ "lido_fee_unavailable",
1554
+ {
1555
+ operation: request.operation,
1556
+ feeEstimateError: finalPlan.operationFeeError,
1557
+ }
1558
+ );
1559
+ }
1560
+
1561
+ const result = await account.sendTransaction(finalPlan.operationTx);
1562
+ const resultFee = BigInt(result?.fee || finalPlan.operationFee || 0);
1563
+ const totalFee = approvalExecution.totalFee + resultFee;
1564
+ return {
1565
+ ...this.#formatLidoOperationResponse({
1566
+ runtimeConfig,
1567
+ accountIndex,
1568
+ address,
1569
+ request,
1570
+ plan: {
1571
+ ...finalPlan,
1572
+ operationFee: resultFee,
1573
+ totalEstimatedFee: totalFee,
1574
+ approval: {
1575
+ ...finalPlan.approval,
1576
+ estimatedFee: approvalExecution.totalFee,
1577
+ },
1578
+ },
1579
+ }),
1580
+ result: {
1581
+ ...result,
1582
+ fee: resultFee.toString(),
1583
+ totalFee: totalFee.toString(),
1584
+ approvalFee: approvalExecution.totalFee.toString(),
1585
+ ...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
1586
+ ...(approvalExecution.resetAllowanceHash
1587
+ ? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
1588
+ : {}),
1589
+ },
1590
+ };
1591
+ } catch (error) {
1592
+ const cleanup = await this.#restoreAllowanceAfterFailedLidoOperation({
1593
+ account,
1594
+ runtimeConfig,
1595
+ tokenAddress: finalPlan.inputTokenAddress,
1596
+ spender: initialPlan.spender,
1597
+ originalAllowance: initialPlan.currentAllowance,
1598
+ approvalExecution,
1599
+ });
1600
+ this.#throwLidoFailureWithCleanup(error, cleanup);
1601
+ }
1602
+ });
1603
+ }
1604
+
1605
+ async quoteLidoWithdrawalOperation({
1606
+ seedPhrase,
1607
+ address,
1608
+ operation,
1609
+ amount,
1610
+ requestId,
1611
+ accountIndex = 0,
1612
+ network,
1613
+ }) {
1614
+ return this.#withReadableAccount(
1615
+ { seedPhrase, address, accountIndex, network },
1616
+ async (account, runtimeConfig) => {
1617
+ assertLidoSupportedNetwork(runtimeConfig.network);
1618
+ const request = buildLidoWithdrawalRequest({ operation, amount, requestId });
1619
+ const accountAddress = await account.getAddress();
1620
+ const plan = await this.#buildLidoWithdrawalPlan({
1621
+ account,
1622
+ runtimeConfig,
1623
+ address: accountAddress,
1624
+ request,
1625
+ tolerateOperationFeeFailure: true,
1626
+ });
1627
+ return this.#formatLidoWithdrawalResponse({
1628
+ runtimeConfig,
1629
+ accountIndex,
1630
+ address: accountAddress,
1631
+ request,
1632
+ plan,
1633
+ });
1634
+ }
1635
+ );
1636
+ }
1637
+
1638
+ async sendLidoWithdrawalOperation({
1639
+ seedPhrase,
1640
+ operation,
1641
+ amount,
1642
+ requestId,
1643
+ accountIndex = 0,
1644
+ network,
1645
+ expectedQuoteFingerprint = null,
1646
+ }) {
1647
+ return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
1648
+ assertLidoSupportedNetwork(runtimeConfig.network);
1649
+ const request = buildLidoWithdrawalRequest({ operation, amount, requestId });
1650
+ const normalizedExpectedQuoteFingerprint =
1651
+ typeof expectedQuoteFingerprint === "string" && expectedQuoteFingerprint.trim()
1652
+ ? expectedQuoteFingerprint.trim()
1653
+ : null;
1654
+ const address = await account.getAddress();
1655
+ let initialPlan = await this.#buildLidoWithdrawalPlan({
1656
+ account,
1657
+ runtimeConfig,
1658
+ address,
1659
+ request,
1660
+ tolerateOperationFeeFailure: true,
1661
+ });
1662
+ this.#assertExpectedLidoWithdrawalFingerprint(
1663
+ normalizedExpectedQuoteFingerprint,
1664
+ initialPlan.quoteFingerprint
1665
+ );
1666
+
1667
+ const approvalExecution = await this.#executeLidoWithdrawalApprovalsIfNeeded({
1668
+ account,
1669
+ runtimeConfig,
1670
+ request,
1671
+ plan: initialPlan,
1672
+ });
1673
+
1674
+ let finalPlan = initialPlan;
1675
+ try {
1676
+ if (approvalExecution.performed) {
1677
+ finalPlan = await this.#buildLidoWithdrawalPlan({
1678
+ account,
1679
+ runtimeConfig,
1680
+ address,
1681
+ request,
1682
+ });
1683
+ this.#assertExpectedLidoWithdrawalFingerprint(
1684
+ normalizedExpectedQuoteFingerprint,
1685
+ finalPlan.quoteFingerprint
1686
+ );
1687
+ }
1688
+
1689
+ if (finalPlan.approval.required) {
1690
+ throw createTaggedError(
1691
+ "Lido withdrawal still requires token approval after the approval step completed.",
1692
+ "lido_withdrawal_approval_required",
1693
+ {
1694
+ spender: finalPlan.spender,
1695
+ requiredAllowance: finalPlan.requiredAllowance.toString(),
1696
+ currentAllowance: finalPlan.currentAllowance.toString(),
1697
+ }
1698
+ );
1699
+ }
1700
+
1701
+ if (finalPlan.operationFee === null) {
1702
+ throw createTaggedError(
1703
+ "Lido withdrawal fee estimate was unavailable. Generate a new quote before sending.",
1704
+ "lido_withdrawal_fee_unavailable",
1705
+ {
1706
+ operation: request.operation,
1707
+ feeEstimateError: finalPlan.operationFeeError,
1708
+ }
1709
+ );
1710
+ }
1711
+
1712
+ const result = await account.sendTransaction(finalPlan.operationTx);
1713
+ const resultFee = BigInt(result?.fee || finalPlan.operationFee || 0);
1714
+ const totalFee = approvalExecution.totalFee + resultFee;
1715
+ return {
1716
+ ...this.#formatLidoWithdrawalResponse({
1717
+ runtimeConfig,
1718
+ accountIndex,
1719
+ address,
1720
+ request,
1721
+ plan: {
1722
+ ...finalPlan,
1723
+ operationFee: resultFee,
1724
+ totalEstimatedFee: totalFee,
1725
+ approval: {
1726
+ ...finalPlan.approval,
1727
+ estimatedFee: approvalExecution.totalFee,
1728
+ },
1729
+ },
1730
+ }),
1731
+ result: {
1732
+ ...result,
1733
+ fee: resultFee.toString(),
1734
+ totalFee: totalFee.toString(),
1735
+ approvalFee: approvalExecution.totalFee.toString(),
1736
+ ...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
1737
+ ...(approvalExecution.resetAllowanceHash
1738
+ ? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
1739
+ : {}),
1740
+ },
1741
+ };
1742
+ } catch (error) {
1743
+ const cleanup = await this.#restoreAllowanceAfterFailedLidoWithdrawal({
1744
+ account,
1745
+ runtimeConfig,
1746
+ tokenAddress: finalPlan.inputTokenAddress,
1747
+ spender: initialPlan.spender,
1748
+ originalAllowance: initialPlan.currentAllowance,
1749
+ approvalExecution,
1750
+ });
1751
+ this.#throwLidoWithdrawalFailureWithCleanup(error, cleanup);
1752
+ }
1753
+ });
1754
+ }
1755
+
1756
+ async quoteSwap({
1757
+ seedPhrase,
1758
+ address,
1759
+ tokenIn,
1760
+ tokenOut,
1761
+ tokenInAmount,
1762
+ accountIndex = 0,
1763
+ network,
1764
+ }) {
1765
+ return this.#withReadableAccount(
1766
+ { seedPhrase, address, accountIndex, network },
1767
+ async (account, runtimeConfig) => {
1768
+ assertVeloraSupportedNetwork(runtimeConfig.network);
1769
+ const swapRequest = buildSwapRequest({ tokenIn, tokenOut, tokenInAmount });
1770
+ const address = await account.getAddress();
1771
+ const readOnlyAccount =
1772
+ typeof account.toReadOnlyAccount === "function" ? await account.toReadOnlyAccount() : account;
1773
+ try {
1774
+ const plan = await this.#buildVeloraSwapPlan({
1775
+ account: readOnlyAccount,
1776
+ runtimeConfig,
1777
+ swapRequest,
1778
+ tolerateSwapFeeFailure: true,
1779
+ });
1780
+ const [tokenInMetadata, tokenOutMetadata] = await Promise.all([
1781
+ this.#getSwapTokenMetadata(runtimeConfig, swapRequest.tokenIn, plan.priceRoute?.srcDecimals),
1782
+ this.#getSwapTokenMetadata(runtimeConfig, swapRequest.tokenOut, plan.priceRoute?.destDecimals),
1783
+ ]);
1784
+ const quote = {
1785
+ fee: plan.swapFee !== null ? plan.swapFee.toString() : null,
1786
+ tokenInAmount: plan.tokenInAmount.toString(),
1787
+ tokenOutAmount: plan.tokenOutAmount.toString(),
1788
+ priceRoute: plan.priceRoute,
1789
+ };
1790
+ return {
1791
+ network: runtimeConfig.network,
1792
+ chainId: runtimeConfig.chainId,
1793
+ accountIndex,
1794
+ address,
1795
+ protocol: "velora",
1796
+ executionSupported: true,
1797
+ swapRequest,
1798
+ tokenInMetadata,
1799
+ tokenOutMetadata,
1800
+ inputAmountFormatted: formatUnits(swapRequest.tokenInAmount, tokenInMetadata.decimals),
1801
+ outputAmountFormatted: formatUnits(plan.tokenOutAmount, tokenOutMetadata.decimals),
1802
+ quoteFingerprint: plan.quoteFingerprint,
1803
+ estimatedFeeWei:
1804
+ plan.totalEstimatedFee !== null ? plan.totalEstimatedFee.toString() : null,
1805
+ estimatedSwapFeeWei: plan.swapFee !== null ? plan.swapFee.toString() : null,
1806
+ estimatedApprovalFeeWei: plan.approval.estimatedFee.toString(),
1807
+ feeEstimateAvailable: plan.swapFee !== null,
1808
+ feeEstimateError: plan.swapFeeError,
1809
+ slippageBps: plan.slippageBps,
1810
+ minimumOutputAmountRaw: plan.minimumTokenOutAmount.toString(),
1811
+ allowance: {
1812
+ spender: plan.spender,
1813
+ currentAllowance: plan.currentAllowance.toString(),
1814
+ requiredAllowance: plan.tokenInAmount.toString(),
1815
+ approvalRequired: plan.approval.required,
1816
+ approvalSequence: plan.approval.steps,
1817
+ readError: plan.allowanceReadError,
1818
+ },
1819
+ router: plan.router,
1820
+ simulation: plan.simulation,
1821
+ swapTransaction: plan.swapTransaction,
1822
+ quote,
1823
+ source: "wdk-protocol-swap-velora-evm",
1824
+ };
1825
+ } finally {
1826
+ if (readOnlyAccount !== account) {
1827
+ await maybeDispose(readOnlyAccount);
1828
+ }
1829
+ }
1830
+ }
1831
+ );
1832
+ }
1833
+
1834
+ async quoteLifiSwap({
1835
+ seedPhrase,
1836
+ address,
1837
+ tokenIn,
1838
+ destinationChain,
1839
+ outputToken,
1840
+ destinationAddress,
1841
+ tokenInAmount,
1842
+ slippage = DEFAULT_LIFI_SLIPPAGE,
1843
+ allowBridges = null,
1844
+ denyBridges = null,
1845
+ preferBridges = null,
1846
+ accountIndex = 0,
1847
+ network,
1848
+ }) {
1849
+ return this.#withReadableAccount(
1850
+ { seedPhrase, address, accountIndex, network },
1851
+ async (account, runtimeConfig) => {
1852
+ assertLifiSupportedNetwork(runtimeConfig.network);
1853
+ const swapRequest = buildLifiEvmSwapRequest({
1854
+ tokenIn,
1855
+ destinationChain,
1856
+ outputToken,
1857
+ destinationAddress,
1858
+ tokenInAmount,
1859
+ slippage,
1860
+ allowBridges,
1861
+ denyBridges,
1862
+ preferBridges,
1863
+ });
1864
+ const sourceAddress = await account.getAddress();
1865
+ const plan = await this.#buildLifiEvmSwapPlan({
1866
+ account,
1867
+ runtimeConfig,
1868
+ address: sourceAddress,
1869
+ swapRequest,
1870
+ tolerateSwapFeeFailure: true,
1871
+ });
1872
+ return this.#formatLifiSwapResponse({
1873
+ runtimeConfig,
1874
+ accountIndex,
1875
+ address: sourceAddress,
1876
+ swapRequest,
1877
+ plan,
1878
+ });
1879
+ }
1880
+ );
1881
+ }
1882
+
1883
+ async swap({
1884
+ seedPhrase,
1885
+ tokenIn,
1886
+ tokenOut,
1887
+ tokenInAmount,
1888
+ accountIndex = 0,
1889
+ network,
1890
+ expectedQuoteFingerprint = null,
1891
+ minimumTokenOutAmount = null,
1892
+ }) {
1893
+ return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
1894
+ assertVeloraSupportedNetwork(runtimeConfig.network);
1895
+ const swapRequest = buildSwapRequest({ tokenIn, tokenOut, tokenInAmount });
1896
+ const normalizedExpectedQuoteFingerprint =
1897
+ typeof expectedQuoteFingerprint === "string" && expectedQuoteFingerprint.trim()
1898
+ ? expectedQuoteFingerprint.trim()
1899
+ : null;
1900
+ const requestedMinimumTokenOutAmount =
1901
+ minimumTokenOutAmount !== null && minimumTokenOutAmount !== undefined
1902
+ ? assertPositiveBigIntString(minimumTokenOutAmount, "minimumTokenOutAmount")
1903
+ : null;
1904
+ const address = await account.getAddress();
1905
+ let initialPlan = await this.#buildVeloraSwapPlan({
1906
+ account,
1907
+ runtimeConfig,
1908
+ swapRequest,
1909
+ });
1910
+ const [tokenInMetadata, tokenOutMetadata] = await Promise.all([
1911
+ this.#getSwapTokenMetadata(runtimeConfig, swapRequest.tokenIn, initialPlan.priceRoute?.srcDecimals),
1912
+ this.#getSwapTokenMetadata(runtimeConfig, swapRequest.tokenOut, initialPlan.priceRoute?.destDecimals),
1913
+ ]);
1914
+ this.#assertExpectedSwapFingerprint(
1915
+ normalizedExpectedQuoteFingerprint,
1916
+ initialPlan.quoteFingerprint
1917
+ );
1918
+ this.#assertMinimumSwapOutput(
1919
+ requestedMinimumTokenOutAmount,
1920
+ initialPlan.minimumTokenOutAmount,
1921
+ initialPlan.tokenOutAmount
1922
+ );
1923
+
1924
+ const approvalExecution = await this.#executeSwapApprovalsIfNeeded({
1925
+ account,
1926
+ runtimeConfig,
1927
+ swapRequest,
1928
+ plan: initialPlan,
1929
+ });
1930
+
1931
+ let finalPlan = initialPlan;
1932
+ try {
1933
+ if (approvalExecution.performed) {
1934
+ finalPlan = await this.#buildVeloraSwapPlan({
1935
+ account,
1936
+ runtimeConfig,
1937
+ swapRequest,
1938
+ });
1939
+ this.#assertExpectedSwapFingerprint(
1940
+ normalizedExpectedQuoteFingerprint,
1941
+ finalPlan.quoteFingerprint
1942
+ );
1943
+ }
1944
+ this.#assertMinimumSwapOutput(
1945
+ requestedMinimumTokenOutAmount,
1946
+ finalPlan.minimumTokenOutAmount,
1947
+ finalPlan.tokenOutAmount
1948
+ );
1949
+
1950
+ const allowanceReadUncertain =
1951
+ approvalExecution.performed && finalPlan.allowanceReadError !== null;
1952
+
1953
+ if (finalPlan.approval.required && !allowanceReadUncertain) {
1954
+ throw createTaggedError(
1955
+ "Swap still requires token approval after the approval step completed.",
1956
+ "swap_approval_required",
1957
+ {
1958
+ spender: finalPlan.spender,
1959
+ requiredAllowance: finalPlan.tokenInAmount.toString(),
1960
+ currentAllowance: finalPlan.currentAllowance.toString(),
1961
+ }
1962
+ );
1963
+ }
1964
+
1965
+ const effectiveSimulation = allowanceReadUncertain
1966
+ ? await this.#simulatePreparedTransaction({
1967
+ runtimeConfig,
1968
+ from: address,
1969
+ tx: finalPlan.swapTx,
1970
+ })
1971
+ : finalPlan.simulation;
1972
+ this.#assertSimulationSucceeded(effectiveSimulation);
1973
+ const { hash } = await account.sendTransaction(finalPlan.swapTx);
1974
+ const totalFee = approvalExecution.totalFee + finalPlan.swapFee;
1975
+ const result = {
1976
+ hash,
1977
+ fee: totalFee.toString(),
1978
+ swapFee: finalPlan.swapFee.toString(),
1979
+ approvalFee: approvalExecution.totalFee.toString(),
1980
+ tokenInAmount: finalPlan.tokenInAmount.toString(),
1981
+ tokenOutAmount: finalPlan.tokenOutAmount.toString(),
1982
+ ...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
1983
+ ...(approvalExecution.resetAllowanceHash
1984
+ ? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
1985
+ : {}),
1986
+ };
1987
+ return {
1988
+ network: runtimeConfig.network,
1989
+ chainId: runtimeConfig.chainId,
1990
+ accountIndex,
1991
+ address,
1992
+ protocol: "velora",
1993
+ executionSupported: true,
1994
+ swapRequest,
1995
+ tokenInMetadata,
1996
+ tokenOutMetadata,
1997
+ inputAmountFormatted: formatUnits(swapRequest.tokenInAmount, tokenInMetadata.decimals),
1998
+ outputAmountFormatted: formatUnits(finalPlan.tokenOutAmount, tokenOutMetadata.decimals),
1999
+ quoteFingerprint: finalPlan.quoteFingerprint,
2000
+ estimatedFeeWei: totalFee.toString(),
2001
+ estimatedSwapFeeWei: finalPlan.swapFee.toString(),
2002
+ estimatedApprovalFeeWei: approvalExecution.totalFee.toString(),
2003
+ feeEstimateAvailable: true,
2004
+ feeEstimateError: null,
2005
+ slippageBps: finalPlan.slippageBps,
2006
+ minimumOutputAmountRaw: finalPlan.minimumTokenOutAmount.toString(),
2007
+ allowance: {
2008
+ spender: finalPlan.spender,
2009
+ currentAllowance: finalPlan.currentAllowance.toString(),
2010
+ requiredAllowance: finalPlan.tokenInAmount.toString(),
2011
+ approvalRequired: finalPlan.approval.required,
2012
+ approvalSequence: finalPlan.approval.steps,
2013
+ readError: finalPlan.allowanceReadError,
2014
+ },
2015
+ router: finalPlan.router,
2016
+ simulation: effectiveSimulation,
2017
+ swapTransaction: finalPlan.swapTransaction,
2018
+ result,
2019
+ source: "wdk-protocol-swap-velora-evm",
2020
+ };
2021
+ } catch (error) {
2022
+ const cleanup = await this.#restoreAllowanceAfterFailedSwap({
2023
+ account,
2024
+ runtimeConfig,
2025
+ tokenAddress: swapRequest.tokenIn,
2026
+ spender: initialPlan.spender,
2027
+ originalAllowance: initialPlan.currentAllowance,
2028
+ approvalExecution,
2029
+ });
2030
+ this.#throwSwapFailureWithCleanup(error, cleanup);
2031
+ }
2032
+ });
2033
+ }
2034
+
2035
+ async sendLifiSwap({
2036
+ seedPhrase,
2037
+ tokenIn,
2038
+ destinationChain,
2039
+ outputToken,
2040
+ destinationAddress,
2041
+ tokenInAmount,
2042
+ slippage = DEFAULT_LIFI_SLIPPAGE,
2043
+ allowBridges = null,
2044
+ denyBridges = null,
2045
+ preferBridges = null,
2046
+ accountIndex = 0,
2047
+ network,
2048
+ minimumTokenOutAmount = null,
2049
+ }) {
2050
+ return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
2051
+ assertLifiSupportedNetwork(runtimeConfig.network);
2052
+ const swapRequest = buildLifiEvmSwapRequest({
2053
+ tokenIn,
2054
+ destinationChain,
2055
+ outputToken,
2056
+ destinationAddress,
2057
+ tokenInAmount,
2058
+ slippage,
2059
+ allowBridges,
2060
+ denyBridges,
2061
+ preferBridges,
2062
+ });
2063
+ const requestedMinimumTokenOutAmount =
2064
+ minimumTokenOutAmount !== null && minimumTokenOutAmount !== undefined
2065
+ ? assertPositiveBigIntString(minimumTokenOutAmount, "minimumTokenOutAmount")
2066
+ : null;
2067
+ const sourceAddress = await account.getAddress();
2068
+ let initialPlan = await this.#buildLifiEvmSwapPlan({
2069
+ account,
2070
+ runtimeConfig,
2071
+ address: sourceAddress,
2072
+ swapRequest,
2073
+ });
2074
+ this.#assertMinimumSwapOutput(
2075
+ requestedMinimumTokenOutAmount,
2076
+ initialPlan.minimumTokenOutAmount,
2077
+ initialPlan.tokenOutAmount
2078
+ );
2079
+
2080
+ const approvalExecution = await this.#executeSwapApprovalsIfNeeded({
2081
+ account,
2082
+ runtimeConfig,
2083
+ swapRequest: {
2084
+ tokenIn: swapRequest.tokenIn,
2085
+ },
2086
+ plan: initialPlan,
2087
+ });
2088
+
2089
+ let finalPlan = initialPlan;
2090
+ try {
2091
+ if (approvalExecution.performed) {
2092
+ finalPlan = await this.#buildLifiEvmSwapPlan({
2093
+ account,
2094
+ runtimeConfig,
2095
+ address: sourceAddress,
2096
+ swapRequest,
2097
+ });
2098
+ }
2099
+ this.#assertMinimumSwapOutput(
2100
+ requestedMinimumTokenOutAmount,
2101
+ finalPlan.minimumTokenOutAmount,
2102
+ finalPlan.tokenOutAmount
2103
+ );
2104
+
2105
+ const allowanceReadUncertain =
2106
+ approvalExecution.performed && finalPlan.allowanceReadError !== null;
2107
+
2108
+ if (finalPlan.approval.required && !allowanceReadUncertain) {
2109
+ throw createTaggedError(
2110
+ "LI.FI cross-chain swap still requires token approval after the approval step completed.",
2111
+ "swap_approval_required",
2112
+ {
2113
+ spender: finalPlan.spender,
2114
+ requiredAllowance: finalPlan.tokenInAmount.toString(),
2115
+ currentAllowance: finalPlan.currentAllowance.toString(),
2116
+ }
2117
+ );
2118
+ }
2119
+
2120
+ const effectiveSimulation = allowanceReadUncertain
2121
+ ? await this.#simulatePreparedTransaction({
2122
+ runtimeConfig,
2123
+ from: sourceAddress,
2124
+ tx: finalPlan.swapTx,
2125
+ })
2126
+ : finalPlan.simulation;
2127
+ this.#assertSimulationSucceeded(effectiveSimulation);
2128
+
2129
+ const { hash } = await account.sendTransaction(finalPlan.swapTx);
2130
+ const totalFee = approvalExecution.totalFee + finalPlan.swapFee;
2131
+ const result = {
2132
+ hash,
2133
+ fee: totalFee.toString(),
2134
+ swapFee: finalPlan.swapFee.toString(),
2135
+ approvalFee: approvalExecution.totalFee.toString(),
2136
+ tokenInAmount: finalPlan.tokenInAmount.toString(),
2137
+ tokenOutAmount: finalPlan.tokenOutAmount.toString(),
2138
+ ...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
2139
+ ...(approvalExecution.resetAllowanceHash
2140
+ ? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
2141
+ : {}),
2142
+ };
2143
+ return {
2144
+ ...this.#formatLifiSwapResponse({
2145
+ runtimeConfig,
2146
+ accountIndex,
2147
+ address: sourceAddress,
2148
+ swapRequest,
2149
+ plan: {
2150
+ ...finalPlan,
2151
+ simulation: effectiveSimulation,
2152
+ swapFee: totalFee,
2153
+ totalEstimatedFee: totalFee,
2154
+ approval: {
2155
+ ...finalPlan.approval,
2156
+ estimatedFee: approvalExecution.totalFee,
2157
+ },
2158
+ },
2159
+ }),
2160
+ result,
2161
+ };
2162
+ } catch (error) {
2163
+ const cleanup = await this.#restoreAllowanceAfterFailedSwap({
2164
+ account,
2165
+ runtimeConfig,
2166
+ tokenAddress: swapRequest.tokenIn,
2167
+ spender: initialPlan.spender,
2168
+ originalAllowance: initialPlan.currentAllowance,
2169
+ approvalExecution,
2170
+ });
2171
+ this.#throwSwapFailureWithCleanup(error, cleanup);
2172
+ }
2173
+ });
2174
+ }
2175
+
2176
+ async quoteNativeTransfer({ seedPhrase, to, value, accountIndex = 0, network }) {
2177
+ return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
2178
+ const tx = {
2179
+ to: normalizeAddress(to, "to"),
2180
+ value: assertPositiveBigIntString(value, "value"),
2181
+ };
2182
+ const quote = await account.quoteSendTransaction(tx);
2183
+ return {
2184
+ network: runtimeConfig.network,
2185
+ chainId: runtimeConfig.chainId,
2186
+ accountIndex,
2187
+ transaction: tx,
2188
+ quote,
2189
+ source: "wdk-wallet-evm",
2190
+ };
2191
+ });
2192
+ }
2193
+
2194
+ async sendNativeTransfer({ seedPhrase, to, value, accountIndex = 0, network }) {
2195
+ return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
2196
+ const tx = {
2197
+ to: normalizeAddress(to, "to"),
2198
+ value: assertPositiveBigIntString(value, "value"),
2199
+ };
2200
+ const result = await account.sendTransaction(tx);
2201
+ return {
2202
+ network: runtimeConfig.network,
2203
+ chainId: runtimeConfig.chainId,
2204
+ accountIndex,
2205
+ transaction: tx,
2206
+ result,
2207
+ source: "wdk-wallet-evm",
2208
+ };
2209
+ });
2210
+ }
2211
+
2212
+ async quoteTokenTransfer({
2213
+ seedPhrase,
2214
+ tokenAddress,
2215
+ recipient,
2216
+ amount,
2217
+ accountIndex = 0,
2218
+ network,
2219
+ }) {
2220
+ return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
2221
+ const transfer = {
2222
+ token: normalizeAddress(tokenAddress, "tokenAddress"),
2223
+ recipient: normalizeAddress(recipient, "recipient"),
2224
+ amount: assertPositiveBigIntString(amount, "amount"),
2225
+ };
2226
+ const ownerAddress = await account.getAddress();
2227
+ const { tokenMetadata } = await this.#prepareTokenTransferContext({
2228
+ account,
2229
+ runtimeConfig,
2230
+ transfer,
2231
+ ownerAddress,
2232
+ });
2233
+ let quote;
2234
+ try {
2235
+ quote = await account.quoteTransfer(transfer);
2236
+ } catch (error) {
2237
+ if (isRecoverableTokenTransferSimulationFailure(error)) {
2238
+ throw createTaggedError(
2239
+ "Token transfer could not be simulated by the token contract.",
2240
+ "token_transfer_failed",
2241
+ {
2242
+ network: runtimeConfig.network,
2243
+ tokenAddress: transfer.token,
2244
+ ownerAddress,
2245
+ recipient: transfer.recipient,
2246
+ amount: transfer.amount.toString(),
2247
+ underlying:
2248
+ error instanceof Error
2249
+ ? {
2250
+ message: error.message,
2251
+ code: String(error.errorCode || error.code || "").trim() || null,
2252
+ }
2253
+ : {
2254
+ message: String(error),
2255
+ code: null,
2256
+ },
2257
+ }
2258
+ );
2259
+ }
2260
+ throw error;
2261
+ }
2262
+ return {
2263
+ network: runtimeConfig.network,
2264
+ chainId: runtimeConfig.chainId,
2265
+ accountIndex,
2266
+ transfer,
2267
+ tokenMetadata,
2268
+ amountFormatted: formatUnits(transfer.amount, tokenMetadata.decimals),
2269
+ quote,
2270
+ source: "wdk-wallet-evm",
2271
+ };
2272
+ });
2273
+ }
2274
+
2275
+ async sendTokenTransfer({
2276
+ seedPhrase,
2277
+ tokenAddress,
2278
+ recipient,
2279
+ amount,
2280
+ accountIndex = 0,
2281
+ network,
2282
+ }) {
2283
+ return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
2284
+ const transfer = {
2285
+ token: normalizeAddress(tokenAddress, "tokenAddress"),
2286
+ recipient: normalizeAddress(recipient, "recipient"),
2287
+ amount: assertPositiveBigIntString(amount, "amount"),
2288
+ };
2289
+ const ownerAddress = await account.getAddress();
2290
+ const { tokenMetadata } = await this.#prepareTokenTransferContext({
2291
+ account,
2292
+ runtimeConfig,
2293
+ transfer,
2294
+ ownerAddress,
2295
+ });
2296
+ let result;
2297
+ try {
2298
+ result = await account.transfer(transfer);
2299
+ } catch (error) {
2300
+ if (isRecoverableTokenTransferSimulationFailure(error)) {
2301
+ throw createTaggedError(
2302
+ "Token transfer could not be simulated by the token contract.",
2303
+ "token_transfer_failed",
2304
+ {
2305
+ network: runtimeConfig.network,
2306
+ tokenAddress: transfer.token,
2307
+ ownerAddress,
2308
+ recipient: transfer.recipient,
2309
+ amount: transfer.amount.toString(),
2310
+ underlying:
2311
+ error instanceof Error
2312
+ ? {
2313
+ message: error.message,
2314
+ code: String(error.errorCode || error.code || "").trim() || null,
2315
+ }
2316
+ : {
2317
+ message: String(error),
2318
+ code: null,
2319
+ },
2320
+ }
2321
+ );
2322
+ }
2323
+ throw error;
2324
+ }
2325
+ return {
2326
+ network: runtimeConfig.network,
2327
+ chainId: runtimeConfig.chainId,
2328
+ accountIndex,
2329
+ transfer,
2330
+ tokenMetadata,
2331
+ amountFormatted: formatUnits(transfer.amount, tokenMetadata.decimals),
2332
+ result,
2333
+ source: "wdk-wallet-evm",
2334
+ };
2335
+ });
2336
+ }
2337
+
2338
+ #resolveRuntimeConfig(networkOverride) {
2339
+ const network = assertValidNetwork(networkOverride) || this.config.network;
2340
+ const profile = this.config.networkProfiles?.[network];
2341
+ if (!profile) {
2342
+ throw new Error(`Missing RPC profile for network: ${network}`);
2343
+ }
2344
+ return {
2345
+ ...this.config,
2346
+ network,
2347
+ chainId: profile.chainId,
2348
+ providerUrl: profile.providerUrl,
2349
+ nativeSymbol: profile.nativeSymbol,
2350
+ };
2351
+ }
2352
+
2353
+ async #withWallet({ seedPhrase, network }, callback) {
2354
+ const mnemonic = assertValidSeedPhrase(seedPhrase);
2355
+ const runtimeConfig = this.#resolveRuntimeConfig(network);
2356
+ const options = {
2357
+ provider: runtimeConfig.providerUrl,
2358
+ chainId: runtimeConfig.chainId,
2359
+ };
2360
+ if (runtimeConfig.transferMaxFeeWei !== null) {
2361
+ options.transferMaxFee = runtimeConfig.transferMaxFeeWei;
2362
+ }
2363
+ const wallet = new WalletManagerEvm(mnemonic, options);
2364
+ try {
2365
+ return await callback(wallet, runtimeConfig);
2366
+ } finally {
2367
+ await maybeDispose(wallet);
2368
+ }
2369
+ }
2370
+
2371
+ async #withAccount({ seedPhrase, accountIndex, network }, callback) {
2372
+ return this.#withWallet({ seedPhrase, network }, async (wallet, runtimeConfig) => {
2373
+ const account = await wallet.getAccount(assertNonNegativeInteger(accountIndex, "accountIndex"));
2374
+ return await callback(account, runtimeConfig);
2375
+ });
2376
+ }
2377
+
2378
+ async #withReadableAccount({ seedPhrase, address, accountIndex, network }, callback) {
2379
+ const normalizedAddress = String(address || "").trim();
2380
+ if (normalizedAddress) {
2381
+ const runtimeConfig = this.#resolveRuntimeConfig(network);
2382
+ const account = new WalletAccountReadOnlyEvm(
2383
+ normalizeAddress(normalizedAddress, "address"),
2384
+ { provider: runtimeConfig.providerUrl }
2385
+ );
2386
+ try {
2387
+ return await callback(account, runtimeConfig);
2388
+ } finally {
2389
+ await maybeDispose(account);
2390
+ }
2391
+ }
2392
+ return this.#withAccount({ seedPhrase, accountIndex, network }, callback);
2393
+ }
2394
+
2395
+ async #getTokenMetadata(runtimeConfig, tokenAddress) {
2396
+ const cacheKey = `${runtimeConfig.network}:${tokenAddress.toLowerCase()}`;
2397
+ const cached = this._tokenMetadataCache.get(cacheKey);
2398
+ if (cached) {
2399
+ return { ...cached };
2400
+ }
2401
+ const [name, symbol, decimalsRaw] = await Promise.all([
2402
+ ethCall(runtimeConfig.providerUrl, tokenAddress, ERC20_NAME_SELECTOR),
2403
+ ethCall(runtimeConfig.providerUrl, tokenAddress, ERC20_SYMBOL_SELECTOR),
2404
+ ethCall(runtimeConfig.providerUrl, tokenAddress, ERC20_DECIMALS_SELECTOR),
2405
+ ]);
2406
+ const decimals = Number(decodeUint256Result(decimalsRaw, "decimals"));
2407
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255) {
2408
+ throw new Error("decimals must be an integer between 0 and 255.");
2409
+ }
2410
+ const metadata = {
2411
+ address: tokenAddress,
2412
+ name: decodeAbiStringResult(name, "name"),
2413
+ symbol: decodeAbiStringResult(symbol, "symbol"),
2414
+ decimals,
2415
+ verified: false,
2416
+ source: "erc20-rpc",
2417
+ };
2418
+ this._tokenMetadataCache.set(cacheKey, metadata);
2419
+ return { ...metadata };
2420
+ }
2421
+
2422
+ async #getBestEffortTokenMetadata(runtimeConfig, tokenAddress) {
2423
+ try {
2424
+ return await this.#getTokenMetadata(runtimeConfig, tokenAddress);
2425
+ } catch {
2426
+ return {
2427
+ address: tokenAddress,
2428
+ name: null,
2429
+ symbol: null,
2430
+ decimals: null,
2431
+ verified: false,
2432
+ source: "erc20-rpc-unavailable",
2433
+ };
2434
+ }
2435
+ }
2436
+
2437
+ async #prepareTokenTransferContext({ account, runtimeConfig, transfer, ownerAddress }) {
2438
+ const currentBalance = await this.#readTokenBalanceWithFallback({
2439
+ account,
2440
+ runtimeConfig,
2441
+ tokenAddress: transfer.token,
2442
+ ownerAddress,
2443
+ });
2444
+ if (currentBalance < transfer.amount) {
2445
+ throw createTaggedError("Insufficient token balance for transfer.", "insufficient_funds", {
2446
+ network: runtimeConfig.network,
2447
+ tokenAddress: transfer.token,
2448
+ ownerAddress,
2449
+ recipient: transfer.recipient,
2450
+ currentBalance: currentBalance.toString(),
2451
+ requiredAmount: transfer.amount.toString(),
2452
+ assetType: "erc20",
2453
+ });
2454
+ }
2455
+ const tokenMetadata = await this.#getBestEffortTokenMetadata(runtimeConfig, transfer.token);
2456
+ return {
2457
+ currentBalance,
2458
+ tokenMetadata,
2459
+ };
2460
+ }
2461
+
2462
+ async #readTokenBalanceWithFallback({ account, runtimeConfig, tokenAddress, ownerAddress }) {
2463
+ try {
2464
+ return await account.getTokenBalance(tokenAddress);
2465
+ } catch (error) {
2466
+ if (!isRecoverableTokenBalanceReadFailure(error)) {
2467
+ throw error;
2468
+ }
2469
+ const code = await rpcRequest(runtimeConfig.providerUrl, "eth_getCode", [
2470
+ normalizeAddress(tokenAddress, "tokenAddress"),
2471
+ "latest",
2472
+ ]);
2473
+ if (!code || String(code).toLowerCase() === "0x") {
2474
+ throw createTaggedError("Token contract could not be resolved on this network.", "token_not_found", {
2475
+ network: runtimeConfig.network,
2476
+ tokenAddress,
2477
+ });
2478
+ }
2479
+ return await this.#readTokenBalanceDirect(runtimeConfig, tokenAddress, ownerAddress);
2480
+ }
2481
+ }
2482
+
2483
+ async #readTokenBalanceDirect(runtimeConfig, tokenAddress, ownerAddress) {
2484
+ const data = buildBalanceOfCallData(ownerAddress);
2485
+ let lastError = null;
2486
+ for (let attempt = 0; attempt < 3; attempt += 1) {
2487
+ try {
2488
+ const raw = await ethCall(runtimeConfig.providerUrl, tokenAddress, data);
2489
+ return decodeUint256Result(raw, "balanceOf");
2490
+ } catch (error) {
2491
+ lastError = error;
2492
+ if (
2493
+ attempt >= 2 ||
2494
+ !isRecoverableTokenBalanceReadFailure(error) ||
2495
+ normalizeErrorCodeValue(error) === "network_unavailable"
2496
+ ) {
2497
+ break;
2498
+ }
2499
+ await new Promise((resolve) => setTimeout(resolve, 150 * (attempt + 1)));
2500
+ }
2501
+ }
2502
+ throw createTaggedError("Token balance could not be read from the token contract.", "token_read_failed", {
2503
+ network: runtimeConfig.network,
2504
+ tokenAddress,
2505
+ ownerAddress,
2506
+ underlying:
2507
+ lastError instanceof Error
2508
+ ? {
2509
+ message: lastError.message,
2510
+ code: String(lastError.errorCode || lastError.code || "").trim() || null,
2511
+ }
2512
+ : {
2513
+ message: String(lastError),
2514
+ code: null,
2515
+ },
2516
+ });
2517
+ }
2518
+
2519
+ async #getSwapTokenMetadata(runtimeConfig, tokenAddress, fallbackDecimals) {
2520
+ if (isVeloraNativeTokenAddress(tokenAddress)) {
2521
+ return {
2522
+ address: tokenAddress,
2523
+ name: runtimeConfig.nativeSymbol === "ETH" ? "Ether" : runtimeConfig.nativeSymbol,
2524
+ symbol: runtimeConfig.nativeSymbol,
2525
+ decimals: 18,
2526
+ verified: true,
2527
+ source: "native-asset",
2528
+ };
2529
+ }
2530
+ try {
2531
+ return await this.#getTokenMetadata(runtimeConfig, tokenAddress);
2532
+ } catch (error) {
2533
+ const decimals = Number(fallbackDecimals);
2534
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255) {
2535
+ throw error;
2536
+ }
2537
+ return {
2538
+ address: tokenAddress,
2539
+ name: null,
2540
+ symbol: null,
2541
+ decimals,
2542
+ verified: false,
2543
+ source: "swap-route-fallback",
2544
+ };
2545
+ }
2546
+ }
2547
+
2548
+ #assertMaxFee(runtimeConfig, fee, operation) {
2549
+ if (
2550
+ runtimeConfig.transferMaxFeeWei !== null &&
2551
+ BigInt(fee) >= BigInt(runtimeConfig.transferMaxFeeWei)
2552
+ ) {
2553
+ throw createTaggedError(`Exceeded maximum fee cost for ${operation}.`, "fee_limit_exceeded", {
2554
+ network: runtimeConfig.network,
2555
+ operation,
2556
+ fee: BigInt(fee).toString(),
2557
+ maxFee: BigInt(runtimeConfig.transferMaxFeeWei).toString(),
2558
+ });
2559
+ }
2560
+ }
2561
+
2562
+ async #buildLifiEvmSwapPlan({
2563
+ account,
2564
+ runtimeConfig,
2565
+ address,
2566
+ swapRequest,
2567
+ tolerateSwapFeeFailure = false,
2568
+ }) {
2569
+ const quote = await this.#fetchLifiQuote({
2570
+ runtimeConfig,
2571
+ address,
2572
+ swapRequest,
2573
+ });
2574
+ const transactionRequest = quote.transactionRequest || {};
2575
+ const spender = !isZeroAddress(swapRequest.tokenIn)
2576
+ ? normalizeAddress(String(quote.estimate?.approvalAddress || ""), "approvalAddress")
2577
+ : normalizeAddress(String(transactionRequest.to || ""), "transactionRequest.to");
2578
+ const swapTx = {
2579
+ to: normalizeAddress(String(transactionRequest.to || ""), "transactionRequest.to"),
2580
+ data: assertNonEmptyString(String(transactionRequest.data || ""), "transactionRequest.data"),
2581
+ value: parseHexOrDecimalBigInt(transactionRequest.value || "0", "transactionRequest.value"),
2582
+ };
2583
+ const isNativeTokenIn = isZeroAddress(swapRequest.tokenIn);
2584
+ const allowanceState = isNativeTokenIn
2585
+ ? {
2586
+ currentAllowance: swapRequest.tokenInAmount,
2587
+ error: null,
2588
+ }
2589
+ : await this.#getSwapAllowanceState({
2590
+ account,
2591
+ tokenAddress: swapRequest.tokenIn,
2592
+ spender,
2593
+ });
2594
+ const currentAllowance = allowanceState.currentAllowance;
2595
+ const approval = isNativeTokenIn
2596
+ ? {
2597
+ required: false,
2598
+ estimatedFee: 0n,
2599
+ steps: [],
2600
+ }
2601
+ : await this.#buildSwapApprovalPlan({
2602
+ account,
2603
+ runtimeConfig,
2604
+ tokenAddress: swapRequest.tokenIn,
2605
+ spender,
2606
+ requiredAmount: swapRequest.tokenInAmount,
2607
+ currentAllowance,
2608
+ });
2609
+
2610
+ const swapFeeQuote = await this.#quoteSwapTransaction({
2611
+ account,
2612
+ runtimeConfig,
2613
+ from: address,
2614
+ swapTx,
2615
+ fallbackGasLimit: parseOptionalHexOrDecimalBigInt(transactionRequest.gasLimit),
2616
+ tolerateFailure: tolerateSwapFeeFailure || approval.required,
2617
+ });
2618
+ if (swapFeeQuote.fee === null && !tolerateSwapFeeFailure && !approval.required) {
2619
+ throw createTaggedError(
2620
+ "LI.FI swap fee estimate was unavailable.",
2621
+ "network_unavailable",
2622
+ {
2623
+ provider: "lifi",
2624
+ feeEstimateError: swapFeeQuote.error,
2625
+ }
2626
+ );
2627
+ }
2628
+ const simulation = approval.required
2629
+ ? {
2630
+ ok: null,
2631
+ skipped: true,
2632
+ reason: "allowance_required",
2633
+ }
2634
+ : await this.#simulatePreparedTransaction({
2635
+ runtimeConfig,
2636
+ from: address,
2637
+ tx: swapTx,
2638
+ });
2639
+ const tokenOutAmount = BigInt(String(quote.estimate?.toAmount || "0"));
2640
+ const minimumTokenOutAmount = BigInt(String(quote.estimate?.toAmountMin || quote.estimate?.toAmount || "0"));
2641
+ const swapTransaction = {
2642
+ to: swapTx.to,
2643
+ value: swapTx.value.toString(),
2644
+ dataHash: sha256Hex(swapTx.data),
2645
+ };
2646
+ const quoteFingerprint = sha256Hex(
2647
+ JSON.stringify({
2648
+ chainId: runtimeConfig.chainId,
2649
+ network: runtimeConfig.network,
2650
+ from: address.toLowerCase(),
2651
+ sourceChainId: LIFI_CHAIN_IDS_BY_NETWORK[runtimeConfig.network],
2652
+ destinationChainId: swapRequest.destinationChainId,
2653
+ tokenIn: swapRequest.tokenIn.toLowerCase(),
2654
+ outputToken: swapRequest.outputToken,
2655
+ destinationAddress: swapRequest.destinationAddress,
2656
+ tokenInAmount: swapRequest.tokenInAmount.toString(),
2657
+ minimumTokenOutAmount: minimumTokenOutAmount.toString(),
2658
+ tool: quote.tool,
2659
+ swapTxTo: swapTransaction.to.toLowerCase(),
2660
+ swapTxValue: swapTransaction.value,
2661
+ })
2662
+ );
2663
+ return {
2664
+ quote,
2665
+ quoteFingerprint,
2666
+ quoteId: String(quote.id || "").trim() || null,
2667
+ quoteType: String(quote.type || "").trim() || null,
2668
+ tool: String(quote.tool || "").trim() || null,
2669
+ toolDetails: quote.toolDetails || null,
2670
+ slippage: Number(quote.action?.slippage ?? swapRequest.slippage),
2671
+ minimumTokenOutAmount,
2672
+ router: swapTx.to,
2673
+ spender,
2674
+ currentAllowance,
2675
+ allowanceReadError: allowanceState.error,
2676
+ tokenInAmount: swapRequest.tokenInAmount,
2677
+ tokenOutAmount,
2678
+ swapTx,
2679
+ swapFee: swapFeeQuote.fee,
2680
+ swapFeeError: swapFeeQuote.error,
2681
+ totalEstimatedFee: swapFeeQuote.fee !== null ? swapFeeQuote.fee + approval.estimatedFee : null,
2682
+ approval,
2683
+ simulation,
2684
+ swapTransaction,
2685
+ tokenInMetadata: this.#buildLifiTokenMetadata(
2686
+ quote.action?.fromToken,
2687
+ swapRequest.tokenIn,
2688
+ isNativeTokenIn ? "native-asset" : "lifi-source-token"
2689
+ ),
2690
+ outputTokenMetadata: this.#buildLifiTokenMetadata(
2691
+ quote.action?.toToken,
2692
+ swapRequest.outputToken,
2693
+ "lifi-destination-token"
2694
+ ),
2695
+ };
2696
+ }
2697
+
2698
+ async #fetchLifiQuote({ runtimeConfig, address, swapRequest }) {
2699
+ const params = new URLSearchParams({
2700
+ fromChain: LIFI_CHAIN_IDS_BY_NETWORK[runtimeConfig.network],
2701
+ toChain: swapRequest.destinationChainId,
2702
+ fromToken: swapRequest.tokenIn,
2703
+ toToken: swapRequest.outputToken,
2704
+ fromAmount: swapRequest.tokenInAmount.toString(),
2705
+ fromAddress: address,
2706
+ toAddress: swapRequest.destinationAddress,
2707
+ slippage: String(swapRequest.slippage),
2708
+ integrator: this.config.lifiIntegrator || "openclaw",
2709
+ });
2710
+ const denyBridges = mergeBridgeLists(
2711
+ this.config.lifiDefaultDenyBridges,
2712
+ swapRequest.denyBridges,
2713
+ ALWAYS_DENIED_LIFI_BRIDGES
2714
+ );
2715
+ if (swapRequest.allowBridges) {
2716
+ params.set("allowBridges", swapRequest.allowBridges);
2717
+ }
2718
+ if (denyBridges) {
2719
+ params.set("denyBridges", denyBridges);
2720
+ }
2721
+ if (swapRequest.preferBridges) {
2722
+ params.set("preferBridges", swapRequest.preferBridges);
2723
+ }
2724
+ const response = await fetch(`${String(this.config.lifiApiBaseUrl).replace(/\/+$/, "")}/quote?${params.toString()}`, {
2725
+ headers: {
2726
+ Accept: "application/json",
2727
+ ...(this.config.lifiApiKey ? { "x-lifi-api-key": this.config.lifiApiKey } : {}),
2728
+ },
2729
+ });
2730
+ let payload;
2731
+ try {
2732
+ payload = await response.json();
2733
+ } catch {
2734
+ payload = null;
2735
+ }
2736
+ if (!response.ok) {
2737
+ const message =
2738
+ payload?.message || payload?.error || payload?.detail || `LI.FI quote failed with HTTP ${response.status}.`;
2739
+ throw createTaggedError(String(message), "network_unavailable", {
2740
+ provider: "lifi",
2741
+ httpStatus: response.status,
2742
+ });
2743
+ }
2744
+ if (!payload || typeof payload !== "object" || !payload.transactionRequest) {
2745
+ throw createTaggedError("LI.FI quote returned no executable transactionRequest.", "network_unavailable", {
2746
+ provider: "lifi",
2747
+ });
2748
+ }
2749
+ return payload;
2750
+ }
2751
+
2752
+ #buildLifiTokenMetadata(token, fallbackAddress, fallbackSource = "lifi-quote") {
2753
+ const raw = token && typeof token === "object" ? token : {};
2754
+ const decimals = Number(raw.decimals);
2755
+ return {
2756
+ address: String(raw.address || fallbackAddress || "").trim(),
2757
+ name: raw.name !== undefined && raw.name !== null ? String(raw.name) : null,
2758
+ symbol: raw.symbol !== undefined && raw.symbol !== null ? String(raw.symbol) : null,
2759
+ decimals: Number.isInteger(decimals) ? decimals : null,
2760
+ verified: Array.isArray(raw.tags) && raw.tags.includes("stablecoin"),
2761
+ source: fallbackSource,
2762
+ };
2763
+ }
2764
+
2765
+ #formatLifiSwapResponse({ runtimeConfig, accountIndex, address, swapRequest, plan }) {
2766
+ return {
2767
+ network: runtimeConfig.network,
2768
+ chainId: runtimeConfig.chainId,
2769
+ accountIndex,
2770
+ address,
2771
+ protocol: "lifi",
2772
+ executionSupported: true,
2773
+ sourceChain: runtimeConfig.network,
2774
+ destinationChainId: swapRequest.destinationChainId,
2775
+ destinationChain: swapRequest.destinationChainId,
2776
+ swapRequest: {
2777
+ tokenIn: swapRequest.tokenIn,
2778
+ outputToken: swapRequest.outputToken,
2779
+ destinationAddress: swapRequest.destinationAddress,
2780
+ tokenInAmount: swapRequest.tokenInAmount.toString(),
2781
+ },
2782
+ tokenInMetadata: plan.tokenInMetadata,
2783
+ outputTokenMetadata: plan.outputTokenMetadata,
2784
+ inputAmountFormatted:
2785
+ plan.tokenInMetadata.decimals !== null
2786
+ ? formatUnits(swapRequest.tokenInAmount, plan.tokenInMetadata.decimals)
2787
+ : null,
2788
+ outputAmountFormatted:
2789
+ plan.outputTokenMetadata.decimals !== null
2790
+ ? formatUnits(plan.tokenOutAmount, plan.outputTokenMetadata.decimals)
2791
+ : null,
2792
+ quoteFingerprint: plan.quoteFingerprint,
2793
+ estimatedFeeWei: plan.totalEstimatedFee !== null ? plan.totalEstimatedFee.toString() : null,
2794
+ estimatedSwapFeeWei: plan.swapFee !== null ? plan.swapFee.toString() : null,
2795
+ estimatedApprovalFeeWei: plan.approval.estimatedFee.toString(),
2796
+ feeEstimateAvailable: plan.swapFee !== null,
2797
+ feeEstimateError: plan.swapFeeError,
2798
+ slippage: plan.slippage,
2799
+ minimumOutputAmountRaw: plan.minimumTokenOutAmount.toString(),
2800
+ allowance: {
2801
+ spender: plan.spender,
2802
+ currentAllowance: plan.currentAllowance.toString(),
2803
+ requiredAllowance: plan.tokenInAmount.toString(),
2804
+ approvalRequired: plan.approval.required,
2805
+ approvalSequence: plan.approval.steps,
2806
+ readError: plan.allowanceReadError,
2807
+ },
2808
+ router: plan.router,
2809
+ simulation: plan.simulation,
2810
+ swapTransaction: plan.swapTransaction,
2811
+ quoteType: plan.quoteType,
2812
+ quoteId: plan.quoteId,
2813
+ tool: plan.tool,
2814
+ toolDetails: plan.toolDetails,
2815
+ quote: plan.quote,
2816
+ source: "lifi",
2817
+ };
2818
+ }
2819
+
2820
+ #assertExpectedSwapFingerprint(expectedQuoteFingerprint, actualQuoteFingerprint) {
2821
+ if (!expectedQuoteFingerprint) {
2822
+ return;
2823
+ }
2824
+ if (expectedQuoteFingerprint !== actualQuoteFingerprint) {
2825
+ throw createTaggedError(
2826
+ "Swap quote changed since preview. Generate a new preview and approval before execute.",
2827
+ "swap_quote_changed",
2828
+ {
2829
+ expectedQuoteFingerprint,
2830
+ actualQuoteFingerprint,
2831
+ }
2832
+ );
2833
+ }
2834
+ }
2835
+
2836
+ #assertMinimumSwapOutput(expectedMinimumTokenOutAmount, actualMinimumTokenOutAmount, actualTokenOutAmount) {
2837
+ if (expectedMinimumTokenOutAmount === null || expectedMinimumTokenOutAmount === undefined) {
2838
+ return;
2839
+ }
2840
+ if (BigInt(actualTokenOutAmount) < BigInt(expectedMinimumTokenOutAmount)) {
2841
+ throw createTaggedError(
2842
+ "Swap quote changed beyond the allowed slippage window. Generate a new preview and approval before execute.",
2843
+ "swap_quote_changed",
2844
+ {
2845
+ expectedMinimumTokenOutAmount: BigInt(expectedMinimumTokenOutAmount).toString(),
2846
+ actualMinimumTokenOutAmount: BigInt(actualMinimumTokenOutAmount).toString(),
2847
+ actualTokenOutAmount: BigInt(actualTokenOutAmount).toString(),
2848
+ }
2849
+ );
2850
+ }
2851
+ }
2852
+
2853
+ #assertSimulationSucceeded(simulation) {
2854
+ if (simulation?.ok === false) {
2855
+ throw createTaggedError(
2856
+ simulation.message || "Swap simulation failed.",
2857
+ "swap_simulation_failed",
2858
+ {
2859
+ ...(simulation.details && typeof simulation.details === "object" ? simulation.details : {}),
2860
+ }
2861
+ );
2862
+ }
2863
+ }
2864
+
2865
+ #formatAaveAccountData(accountData) {
2866
+ return {
2867
+ totalCollateralBase: BigInt(accountData.totalCollateralBase || 0).toString(),
2868
+ totalDebtBase: BigInt(accountData.totalDebtBase || 0).toString(),
2869
+ availableBorrowsBase: BigInt(accountData.availableBorrowsBase || 0).toString(),
2870
+ currentLiquidationThreshold: BigInt(accountData.currentLiquidationThreshold || 0).toString(),
2871
+ ltv: BigInt(accountData.ltv || 0).toString(),
2872
+ healthFactor: BigInt(accountData.healthFactor || 0).toString(),
2873
+ };
2874
+ }
2875
+
2876
+ async #readAaveReserveCatalog(protocol) {
2877
+ const addressMap = await protocol._getAddressMap();
2878
+ const uiPoolDataProviderContract = await protocol._getUiPoolDataProviderContract();
2879
+ const [reservesRaw, baseCurrencyInfoRaw] = await uiPoolDataProviderContract.getReservesData(
2880
+ addressMap.poolAddressesProvider
2881
+ );
2882
+ const baseCurrencyInfo = this.#formatAaveBaseCurrencyInfo(baseCurrencyInfoRaw);
2883
+ const reserves = (Array.isArray(reservesRaw) ? reservesRaw : []).map((reserve) =>
2884
+ this.#formatAaveReserveEntry(reserve, baseCurrencyInfo)
2885
+ );
2886
+ return {
2887
+ addresses: {
2888
+ pool: addressMap.pool,
2889
+ poolAddressesProvider: addressMap.poolAddressesProvider,
2890
+ uiPoolDataProvider: addressMap.uiPoolDataProvider,
2891
+ priceOracle: addressMap.priceOracle,
2892
+ },
2893
+ baseCurrencyInfo,
2894
+ reserves,
2895
+ };
2896
+ }
2897
+
2898
+ #getAaveProtocolDataProviderContract(network, protocol) {
2899
+ const contractAddress = AAVE_PROTOCOL_DATA_PROVIDER_BY_NETWORK[network];
2900
+ if (!contractAddress) {
2901
+ throw new Error(`Aave protocol data provider is not configured for network '${network}'.`);
2902
+ }
2903
+ return new Contract(contractAddress, AAVE_PROTOCOL_DATA_PROVIDER_ABI, protocol._provider);
2904
+ }
2905
+
2906
+ #formatAaveBaseCurrencyInfo(baseCurrencyInfo) {
2907
+ const usdDecimals = Number(baseCurrencyInfo?.networkBaseTokenPriceDecimals || 8);
2908
+ const marketReferenceCurrencyPriceInUsd = BigInt(
2909
+ baseCurrencyInfo?.marketReferenceCurrencyPriceInUsd || 0
2910
+ );
2911
+ const networkBaseTokenPriceInUsd = BigInt(baseCurrencyInfo?.networkBaseTokenPriceInUsd || 0);
2912
+ return {
2913
+ marketReferenceCurrencyUnit: BigInt(baseCurrencyInfo?.marketReferenceCurrencyUnit || 0).toString(),
2914
+ marketReferenceCurrencyPriceInUsd: marketReferenceCurrencyPriceInUsd.toString(),
2915
+ marketReferenceCurrencyPriceInUsdFormatted:
2916
+ marketReferenceCurrencyPriceInUsd > 0n ? formatUnits(marketReferenceCurrencyPriceInUsd, usdDecimals) : null,
2917
+ networkBaseTokenPriceInUsd: networkBaseTokenPriceInUsd.toString(),
2918
+ networkBaseTokenPriceInUsdFormatted:
2919
+ networkBaseTokenPriceInUsd > 0n ? formatUnits(networkBaseTokenPriceInUsd, usdDecimals) : null,
2920
+ networkBaseTokenPriceDecimals: usdDecimals,
2921
+ usdDecimals,
2922
+ };
2923
+ }
2924
+
2925
+ #formatAaveReserveEntry(reserve, baseCurrencyInfo) {
2926
+ const decimals = Number(reserve?.decimals || 18);
2927
+ const liquidityIndexRaw = BigInt(reserve?.liquidityIndex || 0);
2928
+ const variableBorrowIndexRaw = BigInt(reserve?.variableBorrowIndex || 0);
2929
+ const totalScaledVariableDebtRaw = BigInt(reserve?.totalScaledVariableDebt || 0);
2930
+ const totalVariableDebtRaw = rayMul(totalScaledVariableDebtRaw, variableBorrowIndexRaw);
2931
+ const priceInUsdRaw = computeAaveUsdPriceRaw(
2932
+ BigInt(reserve?.priceInMarketReferenceCurrency || 0),
2933
+ baseCurrencyInfo
2934
+ );
2935
+ return {
2936
+ underlyingAsset: normalizeAddress(String(reserve?.underlyingAsset || ""), "underlyingAsset").toLowerCase(),
2937
+ name: String(reserve?.name || "").trim() || null,
2938
+ symbol: String(reserve?.symbol || "").trim() || null,
2939
+ decimals,
2940
+ baseLtvAsCollateral: BigInt(reserve?.baseLTVasCollateral || 0).toString(),
2941
+ baseLtvAsCollateralPercent: formatBasisPoints(BigInt(reserve?.baseLTVasCollateral || 0)),
2942
+ reserveLiquidationThreshold: BigInt(reserve?.reserveLiquidationThreshold || 0).toString(),
2943
+ reserveLiquidationThresholdPercent: formatBasisPoints(
2944
+ BigInt(reserve?.reserveLiquidationThreshold || 0)
2945
+ ),
2946
+ reserveLiquidationBonus: BigInt(reserve?.reserveLiquidationBonus || 0).toString(),
2947
+ reserveFactor: BigInt(reserve?.reserveFactor || 0).toString(),
2948
+ reserveFactorPercent: formatBasisPoints(BigInt(reserve?.reserveFactor || 0)),
2949
+ usageAsCollateralEnabled: Boolean(reserve?.usageAsCollateralEnabled),
2950
+ borrowingEnabled: Boolean(reserve?.borrowingEnabled),
2951
+ isActive: Boolean(reserve?.isActive),
2952
+ isFrozen: Boolean(reserve?.isFrozen),
2953
+ isPaused: Boolean(reserve?.isPaused),
2954
+ isSiloedBorrowing: Boolean(reserve?.isSiloedBorrowing),
2955
+ flashLoanEnabled: Boolean(reserve?.flashLoanEnabled),
2956
+ borrowableInIsolation: Boolean(reserve?.borrowableInIsolation),
2957
+ virtualAccActive: Boolean(reserve?.virtualAccActive),
2958
+ aTokenAddress: normalizeAddress(String(reserve?.aTokenAddress || ""), "aTokenAddress").toLowerCase(),
2959
+ variableDebtTokenAddress: normalizeAddress(
2960
+ String(reserve?.variableDebtTokenAddress || ""),
2961
+ "variableDebtTokenAddress"
2962
+ ).toLowerCase(),
2963
+ interestRateStrategyAddress: normalizeAddress(
2964
+ String(reserve?.interestRateStrategyAddress || ""),
2965
+ "interestRateStrategyAddress"
2966
+ ).toLowerCase(),
2967
+ availableLiquidityRaw: BigInt(reserve?.availableLiquidity || 0).toString(),
2968
+ availableLiquidityFormatted: formatUnits(BigInt(reserve?.availableLiquidity || 0), decimals),
2969
+ totalScaledVariableDebtRaw: totalScaledVariableDebtRaw.toString(),
2970
+ totalVariableDebtRaw: totalVariableDebtRaw.toString(),
2971
+ totalVariableDebtFormatted: formatUnits(totalVariableDebtRaw, decimals),
2972
+ liquidityIndexRaw: liquidityIndexRaw.toString(),
2973
+ variableBorrowIndexRaw: variableBorrowIndexRaw.toString(),
2974
+ liquidityRateRaw: BigInt(reserve?.liquidityRate || 0).toString(),
2975
+ liquidityAprPercent: formatRayAprPercent(BigInt(reserve?.liquidityRate || 0)),
2976
+ variableBorrowRateRaw: BigInt(reserve?.variableBorrowRate || 0).toString(),
2977
+ variableBorrowAprPercent: formatRayAprPercent(BigInt(reserve?.variableBorrowRate || 0)),
2978
+ lastUpdateTimestamp: BigInt(reserve?.lastUpdateTimestamp || 0).toString(),
2979
+ priceInMarketReferenceCurrency: BigInt(reserve?.priceInMarketReferenceCurrency || 0).toString(),
2980
+ priceInUsdRaw: priceInUsdRaw !== null ? priceInUsdRaw.toString() : null,
2981
+ priceInUsdFormatted:
2982
+ priceInUsdRaw !== null ? formatUnits(priceInUsdRaw, baseCurrencyInfo.usdDecimals) : null,
2983
+ priceOracle: normalizeAddress(String(reserve?.priceOracle || ""), "priceOracle").toLowerCase(),
2984
+ variableRateSlope1Raw: BigInt(reserve?.variableRateSlope1 || 0).toString(),
2985
+ variableRateSlope2Raw: BigInt(reserve?.variableRateSlope2 || 0).toString(),
2986
+ baseVariableBorrowRateRaw: BigInt(reserve?.baseVariableBorrowRate || 0).toString(),
2987
+ optimalUsageRatioRaw: BigInt(reserve?.optimalUsageRatio || 0).toString(),
2988
+ accruedToTreasuryRaw: BigInt(reserve?.accruedToTreasury || 0).toString(),
2989
+ unbackedRaw: BigInt(reserve?.unbacked || 0).toString(),
2990
+ isolationModeTotalDebtRaw: BigInt(reserve?.isolationModeTotalDebt || 0).toString(),
2991
+ debtCeilingRaw: BigInt(reserve?.debtCeiling || 0).toString(),
2992
+ debtCeilingDecimals: Number(reserve?.debtCeilingDecimals || 0),
2993
+ borrowCapRaw: BigInt(reserve?.borrowCap || 0).toString(),
2994
+ supplyCapRaw: BigInt(reserve?.supplyCap || 0).toString(),
2995
+ virtualUnderlyingBalanceRaw: BigInt(reserve?.virtualUnderlyingBalance || 0).toString(),
2996
+ };
2997
+ }
2998
+
2999
+ async #buildAaveOperationPlan({
3000
+ account,
3001
+ runtimeConfig,
3002
+ address,
3003
+ request,
3004
+ tolerateOperationFeeFailure = false,
3005
+ }) {
3006
+ const protocol = new AaveProtocolEvm(account);
3007
+ try {
3008
+ const poolContract = await protocol._getPoolContract();
3009
+ const spender = normalizeAddress(String(poolContract.target || ""), "aavePool");
3010
+ const needsAllowance = ["supply", "repay"].includes(request.operation);
3011
+ const allowanceState = needsAllowance
3012
+ ? await this.#getSwapAllowanceState({
3013
+ account,
3014
+ tokenAddress: request.token,
3015
+ spender,
3016
+ })
3017
+ : {
3018
+ currentAllowance: request.amount,
3019
+ error: null,
3020
+ };
3021
+ const currentAllowance = allowanceState.currentAllowance;
3022
+ const approval = needsAllowance
3023
+ ? await this.#buildAaveApprovalPlan({
3024
+ account,
3025
+ runtimeConfig,
3026
+ tokenAddress: request.token,
3027
+ spender,
3028
+ requiredAmount: request.amount,
3029
+ currentAllowance,
3030
+ })
3031
+ : {
3032
+ required: false,
3033
+ estimatedFee: 0n,
3034
+ steps: [],
3035
+ };
3036
+ const operationFeeQuote = await this.#quoteAaveProtocolOperation({
3037
+ protocol,
3038
+ request,
3039
+ skipWhenApprovalRequired: approval.required,
3040
+ tolerateFailure: tolerateOperationFeeFailure || approval.required,
3041
+ });
3042
+ const tokenMetadata = await this.#getBestEffortTokenMetadata(runtimeConfig, request.token);
3043
+ const quoteFingerprint = sha256Hex(
3044
+ JSON.stringify({
3045
+ chainId: runtimeConfig.chainId,
3046
+ network: runtimeConfig.network,
3047
+ from: address.toLowerCase(),
3048
+ protocol: "aave-v3",
3049
+ operation: request.operation,
3050
+ pool: spender.toLowerCase(),
3051
+ token: request.token.toLowerCase(),
3052
+ amount: request.amount.toString(),
3053
+ })
3054
+ );
3055
+ return {
3056
+ quoteFingerprint,
3057
+ spender,
3058
+ currentAllowance,
3059
+ allowanceReadError: allowanceState.error,
3060
+ amount: request.amount,
3061
+ operationFee: operationFeeQuote.fee,
3062
+ operationFeeError: operationFeeQuote.error,
3063
+ totalEstimatedFee:
3064
+ operationFeeQuote.fee !== null ? operationFeeQuote.fee + approval.estimatedFee : null,
3065
+ approval,
3066
+ tokenMetadata,
3067
+ };
3068
+ } finally {
3069
+ await maybeDispose(protocol);
3070
+ }
3071
+ }
3072
+
3073
+ async #quoteAaveProtocolOperation({
3074
+ protocol,
3075
+ request,
3076
+ skipWhenApprovalRequired,
3077
+ tolerateFailure,
3078
+ }) {
3079
+ if (skipWhenApprovalRequired) {
3080
+ return {
3081
+ fee: null,
3082
+ error: {
3083
+ code: "allowance_required",
3084
+ message: "Operation fee estimate is unavailable until the Aave pool allowance is approved.",
3085
+ },
3086
+ };
3087
+ }
3088
+ const quoteMethod = {
3089
+ supply: "quoteSupply",
3090
+ withdraw: "quoteWithdraw",
3091
+ borrow: "quoteBorrow",
3092
+ repay: "quoteRepay",
3093
+ }[request.operation];
3094
+ try {
3095
+ const quote = await protocol[quoteMethod]({
3096
+ token: request.token,
3097
+ amount: request.amount,
3098
+ });
3099
+ const fee = BigInt(quote?.fee || 0);
3100
+ return {
3101
+ fee,
3102
+ error: null,
3103
+ };
3104
+ } catch (error) {
3105
+ if (!tolerateFailure) {
3106
+ throw error;
3107
+ }
3108
+ return {
3109
+ fee: null,
3110
+ error: {
3111
+ code: normalizeErrorCodeValue(error) || null,
3112
+ message: error instanceof Error ? error.message : String(error),
3113
+ },
3114
+ };
3115
+ }
3116
+ }
3117
+
3118
+ async #buildAaveApprovalPlan({
3119
+ account,
3120
+ runtimeConfig,
3121
+ tokenAddress,
3122
+ spender,
3123
+ requiredAmount,
3124
+ currentAllowance,
3125
+ }) {
3126
+ const steps = [];
3127
+ if (currentAllowance < requiredAmount) {
3128
+ if (
3129
+ runtimeConfig.chainId === 1 &&
3130
+ tokenAddress.toLowerCase() === USDT_MAINNET_ADDRESS &&
3131
+ currentAllowance > 0n
3132
+ ) {
3133
+ steps.push({ type: "reset_allowance", amount: "0" });
3134
+ }
3135
+ steps.push({ type: "approve", amount: requiredAmount.toString() });
3136
+ }
3137
+ let estimatedFee = 0n;
3138
+ for (const step of steps) {
3139
+ const quote = await account.quoteSendTransaction(
3140
+ buildErc20ApproveTransaction(tokenAddress, spender, step.amount)
3141
+ );
3142
+ const fee = BigInt(quote.fee);
3143
+ this.#assertMaxFee(runtimeConfig, fee, `aave ${step.type}`);
3144
+ step.estimatedFeeWei = fee.toString();
3145
+ estimatedFee += fee;
3146
+ }
3147
+ return {
3148
+ required: steps.length > 0,
3149
+ estimatedFee,
3150
+ steps,
3151
+ };
3152
+ }
3153
+
3154
+ #formatAaveOperationResponse({ runtimeConfig, accountIndex, address, request, plan }) {
3155
+ return {
3156
+ network: runtimeConfig.network,
3157
+ chainId: runtimeConfig.chainId,
3158
+ accountIndex,
3159
+ address,
3160
+ protocol: "aave-v3",
3161
+ operation: request.operation,
3162
+ operationRequest: {
3163
+ token: request.token,
3164
+ amount: request.amount.toString(),
3165
+ },
3166
+ tokenMetadata: plan.tokenMetadata,
3167
+ amountFormatted:
3168
+ plan.tokenMetadata && Number.isInteger(plan.tokenMetadata.decimals)
3169
+ ? formatUnits(request.amount, plan.tokenMetadata.decimals)
3170
+ : null,
3171
+ quoteFingerprint: plan.quoteFingerprint,
3172
+ estimatedFeeWei: plan.totalEstimatedFee !== null ? plan.totalEstimatedFee.toString() : null,
3173
+ estimatedOperationFeeWei: plan.operationFee !== null ? plan.operationFee.toString() : null,
3174
+ estimatedApprovalFeeWei: plan.approval.estimatedFee.toString(),
3175
+ feeEstimateAvailable: plan.operationFee !== null,
3176
+ feeEstimateError: plan.operationFeeError,
3177
+ allowance: {
3178
+ spender: plan.spender,
3179
+ currentAllowance: plan.currentAllowance.toString(),
3180
+ requiredAllowance: plan.amount.toString(),
3181
+ approvalRequired: plan.approval.required,
3182
+ approvalSequence: plan.approval.steps,
3183
+ readError: plan.allowanceReadError,
3184
+ },
3185
+ source: "wdk-protocol-lending-aave-evm",
3186
+ };
3187
+ }
3188
+
3189
+ #assertExpectedAaveFingerprint(expectedQuoteFingerprint, actualQuoteFingerprint) {
3190
+ if (!expectedQuoteFingerprint) {
3191
+ return;
3192
+ }
3193
+ if (expectedQuoteFingerprint !== actualQuoteFingerprint) {
3194
+ throw createTaggedError(
3195
+ "Aave quote changed since preview. Generate a new preview and approval before execute.",
3196
+ "aave_quote_changed",
3197
+ {
3198
+ expectedQuoteFingerprint,
3199
+ actualQuoteFingerprint,
3200
+ }
3201
+ );
3202
+ }
3203
+ }
3204
+
3205
+ async #executeAaveApprovalsIfNeeded({ account, runtimeConfig, request, plan }) {
3206
+ if (!plan.approval.required) {
3207
+ return {
3208
+ performed: false,
3209
+ totalFee: 0n,
3210
+ approveHash: null,
3211
+ resetAllowanceHash: null,
3212
+ };
3213
+ }
3214
+ let totalFee = 0n;
3215
+ let approveHash = null;
3216
+ let resetAllowanceHash = null;
3217
+ for (const step of plan.approval.steps) {
3218
+ const result = await account.approve({
3219
+ token: request.token,
3220
+ spender: plan.spender,
3221
+ amount: step.amount,
3222
+ });
3223
+ totalFee += BigInt(result.fee || 0);
3224
+ if (step.type === "reset_allowance") {
3225
+ resetAllowanceHash = result.hash;
3226
+ } else if (step.type === "approve") {
3227
+ approveHash = result.hash;
3228
+ }
3229
+ await this.#waitForTransactionReceipt(runtimeConfig, result.hash);
3230
+ }
3231
+ return {
3232
+ performed: true,
3233
+ totalFee,
3234
+ approveHash,
3235
+ resetAllowanceHash,
3236
+ };
3237
+ }
3238
+
3239
+ async #restoreAllowanceAfterFailedAaveOperation({
3240
+ account,
3241
+ runtimeConfig,
3242
+ tokenAddress,
3243
+ spender,
3244
+ originalAllowance,
3245
+ approvalExecution,
3246
+ }) {
3247
+ if (!approvalExecution?.performed) {
3248
+ return {
3249
+ attempted: false,
3250
+ restored: false,
3251
+ originalAllowance: BigInt(originalAllowance || 0n).toString(),
3252
+ };
3253
+ }
3254
+ const cleanup = {
3255
+ attempted: true,
3256
+ restored: false,
3257
+ originalAllowance: BigInt(originalAllowance || 0n).toString(),
3258
+ restoreHashes: [],
3259
+ restoreSteps: [],
3260
+ error: null,
3261
+ };
3262
+ try {
3263
+ const restorePlan = await this.#buildAllowanceRestorePlan({
3264
+ account,
3265
+ runtimeConfig,
3266
+ tokenAddress,
3267
+ spender,
3268
+ targetAllowance: BigInt(originalAllowance || 0n),
3269
+ });
3270
+ cleanup.restoreSteps = restorePlan.steps.map((step) => ({ ...step }));
3271
+ if (!restorePlan.required) {
3272
+ cleanup.restored = true;
3273
+ return cleanup;
3274
+ }
3275
+ for (const step of restorePlan.steps) {
3276
+ const result = await account.approve({
3277
+ token: tokenAddress,
3278
+ spender,
3279
+ amount: step.amount,
3280
+ });
3281
+ cleanup.restoreHashes.push({
3282
+ type: step.type,
3283
+ hash: result.hash,
3284
+ fee: BigInt(result.fee || 0).toString(),
3285
+ });
3286
+ await this.#waitForTransactionReceipt(runtimeConfig, result.hash);
3287
+ }
3288
+ const finalAllowance = await account.getAllowance(tokenAddress, spender);
3289
+ cleanup.finalAllowance = finalAllowance.toString();
3290
+ cleanup.restored = finalAllowance === BigInt(originalAllowance || 0n);
3291
+ return cleanup;
3292
+ } catch (cleanupError) {
3293
+ cleanup.error = {
3294
+ message: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
3295
+ code:
3296
+ cleanupError && typeof cleanupError === "object"
3297
+ ? String(cleanupError.errorCode || cleanupError.code || "").trim() || null
3298
+ : null,
3299
+ };
3300
+ return cleanup;
3301
+ }
3302
+ }
3303
+
3304
+ #throwAaveFailureWithCleanup(error, cleanup) {
3305
+ if (cleanup?.attempted && cleanup.restored !== true) {
3306
+ throw createTaggedError(
3307
+ "Aave operation failed after approval and automatic allowance restore did not complete.",
3308
+ "aave_cleanup_failed",
3309
+ {
3310
+ originalError:
3311
+ error instanceof Error
3312
+ ? {
3313
+ message: error.message,
3314
+ code: String(error.errorCode || error.code || "").trim() || null,
3315
+ }
3316
+ : { message: String(error), code: null },
3317
+ cleanup,
3318
+ }
3319
+ );
3320
+ }
3321
+ throw error;
3322
+ }
3323
+
3324
+ #getLidoContracts(network) {
3325
+ const contracts = LIDO_CONTRACTS_BY_NETWORK[network];
3326
+ if (!contracts) {
3327
+ throw new Error(`Lido contracts are not configured for network '${network}'.`);
3328
+ }
3329
+ return contracts;
3330
+ }
3331
+
3332
+ #getLidoReferralAddress() {
3333
+ const configured = String(this.config.lidoReferralAddress || "").trim();
3334
+ if (!configured) {
3335
+ return ZERO_ADDRESS;
3336
+ }
3337
+ return normalizeAddress(configured, "lidoReferralAddress").toLowerCase();
3338
+ }
3339
+
3340
+ async #getLidoTokenMetadata(runtimeConfig, tokenDefinition) {
3341
+ const metadata = await this.#getBestEffortTokenMetadata(runtimeConfig, tokenDefinition.address);
3342
+ return withLidoMetadataDefaults(metadata, tokenDefinition);
3343
+ }
3344
+
3345
+ async #readLidoSampleRates(runtimeConfig) {
3346
+ const sampleAmount = 10n ** 18n;
3347
+ const [wstEthPerStEthRaw, stEthPerWstEthRaw] = await Promise.all([
3348
+ this.#quoteLidoOutputRaw({
3349
+ runtimeConfig,
3350
+ operation: "wrap_steth",
3351
+ amount: sampleAmount,
3352
+ }),
3353
+ this.#quoteLidoOutputRaw({
3354
+ runtimeConfig,
3355
+ operation: "unwrap_wsteth",
3356
+ amount: sampleAmount,
3357
+ }),
3358
+ ]);
3359
+ return {
3360
+ sampleBaseUnits: sampleAmount.toString(),
3361
+ wstEthPerStEthRaw: wstEthPerStEthRaw.toString(),
3362
+ wstEthPerStEthFormatted: formatUnits(wstEthPerStEthRaw, 18),
3363
+ stEthPerWstEthRaw: stEthPerWstEthRaw.toString(),
3364
+ stEthPerWstEthFormatted: formatUnits(stEthPerWstEthRaw, 18),
3365
+ };
3366
+ }
3367
+
3368
+ async #readLidoStakingApr(runtimeConfig) {
3369
+ if (runtimeConfig.network !== "ethereum") {
3370
+ return {
3371
+ data: null,
3372
+ error: null,
3373
+ };
3374
+ }
3375
+ const baseUrl = String(this.config.lidoApiBaseUrl || "https://eth-api.lido.fi/v1").replace(
3376
+ /\/+$/,
3377
+ ""
3378
+ );
3379
+ if (!baseUrl) {
3380
+ return {
3381
+ data: null,
3382
+ error: {
3383
+ code: "lido_apr_unavailable",
3384
+ message: "Lido APR API base URL is not configured.",
3385
+ },
3386
+ };
3387
+ }
3388
+ try {
3389
+ const [lastPayload, smaPayload] = await Promise.all([
3390
+ fetchJson(`${baseUrl}/protocol/steth/apr/last`),
3391
+ fetchJson(`${baseUrl}/protocol/steth/apr/sma`),
3392
+ ]);
3393
+ return {
3394
+ data: this.#normalizeLidoStakingApr({
3395
+ lastPayload,
3396
+ smaPayload,
3397
+ }),
3398
+ error: null,
3399
+ };
3400
+ } catch (error) {
3401
+ return {
3402
+ data: null,
3403
+ error: {
3404
+ code:
3405
+ error && typeof error === "object"
3406
+ ? String(error.errorCode || error.code || "").trim() || "lido_apr_unavailable"
3407
+ : "lido_apr_unavailable",
3408
+ message: error instanceof Error ? error.message : String(error),
3409
+ },
3410
+ };
3411
+ }
3412
+ }
3413
+
3414
+ #normalizeLidoStakingApr({ lastPayload, smaPayload }) {
3415
+ const lastData =
3416
+ lastPayload && typeof lastPayload === "object" && lastPayload.data && typeof lastPayload.data === "object"
3417
+ ? lastPayload.data
3418
+ : {};
3419
+ const smaData =
3420
+ smaPayload && typeof smaPayload === "object" && smaPayload.data && typeof smaPayload.data === "object"
3421
+ ? smaPayload.data
3422
+ : {};
3423
+ const meta =
3424
+ lastPayload && typeof lastPayload === "object" && lastPayload.meta && typeof lastPayload.meta === "object"
3425
+ ? lastPayload.meta
3426
+ : smaPayload && typeof smaPayload === "object" && smaPayload.meta && typeof smaPayload.meta === "object"
3427
+ ? smaPayload.meta
3428
+ : {};
3429
+ const aprSeries = Array.isArray(smaData.aprs)
3430
+ ? smaData.aprs
3431
+ .map((entry) => ({
3432
+ timeUnix: Number(entry?.timeUnix),
3433
+ apr: Number(entry?.apr),
3434
+ }))
3435
+ .filter((entry) => Number.isFinite(entry.timeUnix) && Number.isFinite(entry.apr))
3436
+ : [];
3437
+ const lastApr = Number(lastData.apr);
3438
+ const lastTimeUnix = Number(lastData.timeUnix);
3439
+ const smaApr = Number(smaData.smaApr);
3440
+ const chainId = Number(meta.chainId);
3441
+ return {
3442
+ source: "lido-public-api",
3443
+ symbol: typeof meta.symbol === "string" && meta.symbol.trim() ? meta.symbol.trim() : "stETH",
3444
+ address:
3445
+ typeof meta.address === "string" && meta.address.trim() ? meta.address.trim().toLowerCase() : null,
3446
+ chainId: Number.isFinite(chainId) ? chainId : 1,
3447
+ lastApr: Number.isFinite(lastApr) ? lastApr : null,
3448
+ lastAprTimeUnix: Number.isFinite(lastTimeUnix) ? lastTimeUnix : null,
3449
+ smaApr: Number.isFinite(smaApr) ? smaApr : null,
3450
+ smaWindowDays: 7,
3451
+ aprSeries,
3452
+ };
3453
+ }
3454
+
3455
+ async #getLidoWithdrawalRequestIds(runtimeConfig, ownerAddress) {
3456
+ const contracts = this.#getLidoContracts(runtimeConfig.network);
3457
+ const [requestIdsRaw] = await callContract(
3458
+ runtimeConfig.providerUrl,
3459
+ contracts.withdrawalQueue,
3460
+ LIDO_WITHDRAWAL_QUEUE_INTERFACE,
3461
+ "getWithdrawalRequests",
3462
+ [normalizeAddress(ownerAddress, "ownerAddress")]
3463
+ );
3464
+ return Array.isArray(requestIdsRaw) ? requestIdsRaw.map((value) => BigInt(value)) : [];
3465
+ }
3466
+
3467
+ async #getLidoWithdrawalStatuses(runtimeConfig, requestIds) {
3468
+ if (!Array.isArray(requestIds) || requestIds.length === 0) {
3469
+ return [];
3470
+ }
3471
+ const contracts = this.#getLidoContracts(runtimeConfig.network);
3472
+ const normalizedIds = requestIds.map((value) => BigInt(value));
3473
+ const [statusesRaw] = await callContract(
3474
+ runtimeConfig.providerUrl,
3475
+ contracts.withdrawalQueue,
3476
+ LIDO_WITHDRAWAL_QUEUE_INTERFACE,
3477
+ "getWithdrawalStatus",
3478
+ [normalizedIds]
3479
+ );
3480
+ const entries = Array.isArray(statusesRaw) ? statusesRaw : [];
3481
+ return entries.map((entry, index) => ({
3482
+ owner:
3483
+ /^0x[a-fA-F0-9]{40}$/.test(String(entry.owner ?? entry[2] ?? ZERO_ADDRESS).trim())
3484
+ ? String(entry.owner ?? entry[2] ?? ZERO_ADDRESS).trim().toLowerCase()
3485
+ : ZERO_ADDRESS,
3486
+ requestId: normalizedIds[index],
3487
+ amountOfStETH: BigInt(entry.amountOfStETH ?? entry[0] ?? 0),
3488
+ amountOfShares: BigInt(entry.amountOfShares ?? entry[1] ?? 0),
3489
+ timestamp: BigInt(entry.timestamp ?? entry[3] ?? 0),
3490
+ isFinalized: Boolean(entry.isFinalized ?? entry[4]),
3491
+ isClaimed: Boolean(entry.isClaimed ?? entry[5]),
3492
+ }));
3493
+ }
3494
+
3495
+ #formatLidoWithdrawalStatus(status, stEthMetadata, wstEthMetadata) {
3496
+ const claimable = Boolean(status.isFinalized) && !Boolean(status.isClaimed);
3497
+ const amountOfWstEthRaw =
3498
+ status.amountOfShares > 0n && status.amountOfStETH > 0n
3499
+ ? status.amountOfShares
3500
+ : null;
3501
+ return {
3502
+ requestId: status.requestId.toString(),
3503
+ owner: status.owner,
3504
+ timestamp: status.timestamp.toString(),
3505
+ amountOfStETHRaw: status.amountOfStETH.toString(),
3506
+ amountOfStETHFormatted: formatUnits(status.amountOfStETH, stEthMetadata.decimals),
3507
+ amountOfSharesRaw: status.amountOfShares.toString(),
3508
+ amountOfSharesFormatted: formatUnits(status.amountOfShares, LIDO_STETH_DECIMALS),
3509
+ amountOfWstETHRaw: amountOfWstEthRaw !== null ? amountOfWstEthRaw.toString() : null,
3510
+ amountOfWstETHFormatted:
3511
+ amountOfWstEthRaw !== null ? formatUnits(amountOfWstEthRaw, wstEthMetadata.decimals) : null,
3512
+ isFinalized: Boolean(status.isFinalized),
3513
+ isClaimed: Boolean(status.isClaimed),
3514
+ claimable,
3515
+ };
3516
+ }
3517
+
3518
+ async #quoteLidoOutputRaw({ runtimeConfig, operation, amount, fromAddress = ZERO_ADDRESS }) {
3519
+ const contracts = this.#getLidoContracts(runtimeConfig.network);
3520
+ const normalizedOperation = normalizeLidoOperation(operation);
3521
+ if (normalizedOperation === "stake_eth_for_wsteth") {
3522
+ const data = LIDO_REFERRAL_STAKER_INTERFACE.encodeFunctionData("stakeETH", [
3523
+ this.#getLidoReferralAddress(),
3524
+ ]);
3525
+ const raw = await ethCallTransaction(runtimeConfig.providerUrl, {
3526
+ from: normalizeAddress(fromAddress, "fromAddress"),
3527
+ to: contracts.referralStaker,
3528
+ data,
3529
+ value: toRpcHex(amount),
3530
+ });
3531
+ return decodeUint256Result(raw, "stakeETH");
3532
+ }
3533
+ const callData =
3534
+ normalizedOperation === "wrap_steth"
3535
+ ? LIDO_WSTETH_INTERFACE.encodeFunctionData("getWstETHByStETH", [amount])
3536
+ : LIDO_WSTETH_INTERFACE.encodeFunctionData("getStETHByWstETH", [amount]);
3537
+ const raw = await ethCall(runtimeConfig.providerUrl, contracts.wsteth.address, callData);
3538
+ return decodeUint256Result(
3539
+ raw,
3540
+ normalizedOperation === "wrap_steth" ? "getWstETHByStETH" : "getStETHByWstETH"
3541
+ );
3542
+ }
3543
+
3544
+ async #buildLidoOperationPlan({
3545
+ account,
3546
+ runtimeConfig,
3547
+ address,
3548
+ request,
3549
+ tolerateOperationFeeFailure = false,
3550
+ }) {
3551
+ const contracts = this.#getLidoContracts(runtimeConfig.network);
3552
+ const nativeMetadata = {
3553
+ address: ZERO_ADDRESS,
3554
+ name: runtimeConfig.nativeSymbol === "ETH" ? "Ether" : runtimeConfig.nativeSymbol,
3555
+ symbol: runtimeConfig.nativeSymbol,
3556
+ decimals: 18,
3557
+ verified: true,
3558
+ source: "native-asset",
3559
+ };
3560
+ const [stEthMetadata, wstEthMetadata] = await Promise.all([
3561
+ this.#getLidoTokenMetadata(runtimeConfig, contracts.steth),
3562
+ this.#getLidoTokenMetadata(runtimeConfig, contracts.wsteth),
3563
+ ]);
3564
+ const inputTokenAddress =
3565
+ request.operation === "wrap_steth"
3566
+ ? contracts.steth.address
3567
+ : request.operation === "unwrap_wsteth"
3568
+ ? contracts.wsteth.address
3569
+ : ZERO_ADDRESS;
3570
+ const inputMetadata =
3571
+ request.operation === "wrap_steth"
3572
+ ? stEthMetadata
3573
+ : request.operation === "unwrap_wsteth"
3574
+ ? wstEthMetadata
3575
+ : nativeMetadata;
3576
+ const outputMetadata = request.operation === "unwrap_wsteth" ? stEthMetadata : wstEthMetadata;
3577
+ const spender = request.operation === "wrap_steth" ? contracts.wsteth.address : null;
3578
+ const currentAllowanceState =
3579
+ request.operation === "wrap_steth"
3580
+ ? await this.#getSwapAllowanceState({
3581
+ account,
3582
+ tokenAddress: contracts.steth.address,
3583
+ spender: contracts.wsteth.address,
3584
+ })
3585
+ : {
3586
+ currentAllowance: request.amount,
3587
+ error: null,
3588
+ };
3589
+ const approval =
3590
+ request.operation === "wrap_steth"
3591
+ ? await this.#buildLidoApprovalPlan({
3592
+ account,
3593
+ runtimeConfig,
3594
+ tokenAddress: contracts.steth.address,
3595
+ spender: contracts.wsteth.address,
3596
+ requiredAmount: request.amount,
3597
+ currentAllowance: currentAllowanceState.currentAllowance,
3598
+ })
3599
+ : {
3600
+ required: false,
3601
+ estimatedFee: 0n,
3602
+ steps: [],
3603
+ };
3604
+ if (request.operation === "wrap_steth" || request.operation === "unwrap_wsteth") {
3605
+ const balance = await this.#readTokenBalanceWithFallback({
3606
+ account,
3607
+ runtimeConfig,
3608
+ tokenAddress: inputTokenAddress,
3609
+ ownerAddress: address,
3610
+ });
3611
+ if (balance < request.amount) {
3612
+ throw createTaggedError(
3613
+ "Insufficient token balance for Lido operation.",
3614
+ "insufficient_funds",
3615
+ {
3616
+ network: runtimeConfig.network,
3617
+ tokenAddress: inputTokenAddress,
3618
+ ownerAddress: address,
3619
+ currentBalance: balance.toString(),
3620
+ requiredAmount: request.amount.toString(),
3621
+ protocol: "lido",
3622
+ }
3623
+ );
3624
+ }
3625
+ }
3626
+
3627
+ const operationTx = this.#buildLidoOperationTransaction(runtimeConfig, request);
3628
+ const expectedOutputAmount = await this.#quoteLidoOutputRaw({
3629
+ runtimeConfig,
3630
+ operation: request.operation,
3631
+ amount: request.amount,
3632
+ fromAddress: address,
3633
+ });
3634
+ const operationFeeQuote = await this.#quoteSwapTransaction({
3635
+ account,
3636
+ runtimeConfig,
3637
+ from: address,
3638
+ swapTx: operationTx,
3639
+ tolerateFailure: tolerateOperationFeeFailure || approval.required,
3640
+ operationLabel: `lido ${request.operation}`,
3641
+ });
3642
+ const simulation = approval.required
3643
+ ? {
3644
+ ok: null,
3645
+ skipped: true,
3646
+ reason: "allowance_required",
3647
+ }
3648
+ : await this.#simulatePreparedTransaction({
3649
+ runtimeConfig,
3650
+ from: address,
3651
+ tx: operationTx,
3652
+ operationLabel: "Lido operation",
3653
+ });
3654
+ const operationTransaction = {
3655
+ to: operationTx.to,
3656
+ value: operationTx.value.toString(),
3657
+ dataHash: sha256Hex(String(operationTx.data || "")),
3658
+ };
3659
+ const quoteFingerprint = sha256Hex(
3660
+ JSON.stringify({
3661
+ chainId: runtimeConfig.chainId,
3662
+ network: runtimeConfig.network,
3663
+ from: address.toLowerCase(),
3664
+ protocol: "lido",
3665
+ operation: request.operation,
3666
+ inputToken: inputTokenAddress.toLowerCase(),
3667
+ outputToken: outputMetadata.address.toLowerCase(),
3668
+ amount: request.amount.toString(),
3669
+ outputAmount: expectedOutputAmount.toString(),
3670
+ operationTxTo: operationTransaction.to.toLowerCase(),
3671
+ operationTxValue: operationTransaction.value,
3672
+ })
3673
+ );
3674
+ return {
3675
+ quoteFingerprint,
3676
+ contracts,
3677
+ spender,
3678
+ inputTokenAddress,
3679
+ currentAllowance: currentAllowanceState.currentAllowance,
3680
+ allowanceReadError: currentAllowanceState.error,
3681
+ amount: request.amount,
3682
+ expectedOutputAmount,
3683
+ operationTx,
3684
+ operationFee: operationFeeQuote.fee,
3685
+ operationFeeError: operationFeeQuote.error,
3686
+ totalEstimatedFee:
3687
+ operationFeeQuote.fee !== null ? operationFeeQuote.fee + approval.estimatedFee : null,
3688
+ approval,
3689
+ inputMetadata,
3690
+ outputMetadata,
3691
+ simulation,
3692
+ operationTransaction,
3693
+ };
3694
+ }
3695
+
3696
+ async #buildLidoWithdrawalPlan({
3697
+ account,
3698
+ runtimeConfig,
3699
+ address,
3700
+ request,
3701
+ tolerateOperationFeeFailure = false,
3702
+ }) {
3703
+ const contracts = this.#getLidoContracts(runtimeConfig.network);
3704
+ const [stEthMetadata, wstEthMetadata] = await Promise.all([
3705
+ this.#getLidoTokenMetadata(runtimeConfig, contracts.steth),
3706
+ this.#getLidoTokenMetadata(runtimeConfig, contracts.wsteth),
3707
+ ]);
3708
+
3709
+ if (request.operation === "claim_withdrawal") {
3710
+ const status = await this.#getSingleLidoWithdrawalStatus(runtimeConfig, request.requestId);
3711
+ if (status.owner.toLowerCase() !== address.toLowerCase()) {
3712
+ throw createTaggedError(
3713
+ "Withdrawal request does not belong to the active wallet.",
3714
+ "lido_withdrawal_owner_mismatch",
3715
+ {
3716
+ requestId: request.requestId.toString(),
3717
+ owner: status.owner,
3718
+ activeAddress: address,
3719
+ }
3720
+ );
3721
+ }
3722
+ if (status.isClaimed) {
3723
+ throw createTaggedError(
3724
+ "Withdrawal request has already been claimed.",
3725
+ "lido_withdrawal_already_claimed",
3726
+ {
3727
+ requestId: request.requestId.toString(),
3728
+ }
3729
+ );
3730
+ }
3731
+ if (!status.isFinalized) {
3732
+ throw createTaggedError(
3733
+ "Withdrawal request is not finalized yet and cannot be claimed.",
3734
+ "lido_withdrawal_not_finalized",
3735
+ {
3736
+ requestId: request.requestId.toString(),
3737
+ }
3738
+ );
3739
+ }
3740
+ const operationTx = {
3741
+ to: contracts.withdrawalQueue,
3742
+ value: 0n,
3743
+ data: LIDO_WITHDRAWAL_QUEUE_INTERFACE.encodeFunctionData("claimWithdrawal", [
3744
+ request.requestId,
3745
+ ]),
3746
+ };
3747
+ const operationFeeQuote = await this.#quoteSwapTransaction({
3748
+ account,
3749
+ runtimeConfig,
3750
+ from: address,
3751
+ swapTx: operationTx,
3752
+ tolerateFailure: tolerateOperationFeeFailure,
3753
+ operationLabel: "lido claim_withdrawal",
3754
+ });
3755
+ const simulation = await this.#simulatePreparedTransaction({
3756
+ runtimeConfig,
3757
+ from: address,
3758
+ tx: operationTx,
3759
+ operationLabel: "Lido withdrawal claim",
3760
+ });
3761
+ const operationTransaction = {
3762
+ to: operationTx.to,
3763
+ value: "0",
3764
+ dataHash: sha256Hex(String(operationTx.data || "")),
3765
+ };
3766
+ const quoteFingerprint = sha256Hex(
3767
+ JSON.stringify({
3768
+ chainId: runtimeConfig.chainId,
3769
+ network: runtimeConfig.network,
3770
+ from: address.toLowerCase(),
3771
+ protocol: "lido",
3772
+ operation: request.operation,
3773
+ requestId: request.requestId.toString(),
3774
+ withdrawalQueue: contracts.withdrawalQueue.toLowerCase(),
3775
+ })
3776
+ );
3777
+ return {
3778
+ quoteFingerprint,
3779
+ contracts,
3780
+ spender: null,
3781
+ inputTokenAddress: ZERO_ADDRESS,
3782
+ currentAllowance: 0n,
3783
+ requiredAllowance: 0n,
3784
+ allowanceReadError: null,
3785
+ operationFee: operationFeeQuote.fee,
3786
+ operationFeeError: operationFeeQuote.error,
3787
+ totalEstimatedFee: operationFeeQuote.fee,
3788
+ approval: { required: false, estimatedFee: 0n, steps: [] },
3789
+ inputMetadata: null,
3790
+ queueAssetMetadata: stEthMetadata,
3791
+ withdrawalRequest: this.#formatLidoWithdrawalStatus(status, stEthMetadata, wstEthMetadata),
3792
+ simulation,
3793
+ operationTx,
3794
+ operationTransaction,
3795
+ };
3796
+ }
3797
+
3798
+ const inputTokenAddress =
3799
+ request.operation === "request_withdrawal_steth" ? contracts.steth.address : contracts.wsteth.address;
3800
+ const inputMetadata =
3801
+ request.operation === "request_withdrawal_steth" ? stEthMetadata : wstEthMetadata;
3802
+ const spender = contracts.withdrawalQueue;
3803
+ const queuedStEthAmount =
3804
+ request.operation === "request_withdrawal_steth"
3805
+ ? request.amount
3806
+ : await this.#quoteLidoOutputRaw({
3807
+ runtimeConfig,
3808
+ operation: "unwrap_wsteth",
3809
+ amount: request.amount,
3810
+ fromAddress: address,
3811
+ });
3812
+ this.#assertLidoWithdrawalAmountWithinLimits(queuedStEthAmount);
3813
+ const balance = await this.#readTokenBalanceWithFallback({
3814
+ account,
3815
+ runtimeConfig,
3816
+ tokenAddress: inputTokenAddress,
3817
+ ownerAddress: address,
3818
+ });
3819
+ if (balance < request.amount) {
3820
+ throw createTaggedError(
3821
+ "Insufficient token balance for Lido withdrawal request.",
3822
+ "insufficient_funds",
3823
+ {
3824
+ network: runtimeConfig.network,
3825
+ tokenAddress: inputTokenAddress,
3826
+ ownerAddress: address,
3827
+ currentBalance: balance.toString(),
3828
+ requiredAmount: request.amount.toString(),
3829
+ protocol: "lido",
3830
+ }
3831
+ );
3832
+ }
3833
+ const allowanceState = await this.#getSwapAllowanceState({
3834
+ account,
3835
+ tokenAddress: inputTokenAddress,
3836
+ spender,
3837
+ });
3838
+ const approval = await this.#buildLidoApprovalPlan({
3839
+ account,
3840
+ runtimeConfig,
3841
+ tokenAddress: inputTokenAddress,
3842
+ spender,
3843
+ requiredAmount: request.amount,
3844
+ currentAllowance: allowanceState.currentAllowance,
3845
+ });
3846
+ const operationTx = {
3847
+ to: contracts.withdrawalQueue,
3848
+ value: 0n,
3849
+ data:
3850
+ request.operation === "request_withdrawal_steth"
3851
+ ? LIDO_WITHDRAWAL_QUEUE_INTERFACE.encodeFunctionData("requestWithdrawals", [
3852
+ [request.amount],
3853
+ address,
3854
+ ])
3855
+ : LIDO_WITHDRAWAL_QUEUE_INTERFACE.encodeFunctionData("requestWithdrawalsWstETH", [
3856
+ [request.amount],
3857
+ address,
3858
+ ]),
3859
+ };
3860
+ const operationFeeQuote = await this.#quoteSwapTransaction({
3861
+ account,
3862
+ runtimeConfig,
3863
+ from: address,
3864
+ swapTx: operationTx,
3865
+ tolerateFailure: tolerateOperationFeeFailure || approval.required,
3866
+ operationLabel: `lido ${request.operation}`,
3867
+ });
3868
+ const simulation = approval.required
3869
+ ? {
3870
+ ok: null,
3871
+ skipped: true,
3872
+ reason: "allowance_required",
3873
+ }
3874
+ : await this.#simulatePreparedTransaction({
3875
+ runtimeConfig,
3876
+ from: address,
3877
+ tx: operationTx,
3878
+ operationLabel: "Lido withdrawal request",
3879
+ });
3880
+ const operationTransaction = {
3881
+ to: operationTx.to,
3882
+ value: "0",
3883
+ dataHash: sha256Hex(String(operationTx.data || "")),
3884
+ };
3885
+ const quoteFingerprint = sha256Hex(
3886
+ JSON.stringify({
3887
+ chainId: runtimeConfig.chainId,
3888
+ network: runtimeConfig.network,
3889
+ from: address.toLowerCase(),
3890
+ protocol: "lido",
3891
+ operation: request.operation,
3892
+ inputToken: inputTokenAddress.toLowerCase(),
3893
+ inputAmount: request.amount.toString(),
3894
+ queuedStEthAmount: queuedStEthAmount.toString(),
3895
+ withdrawalQueue: contracts.withdrawalQueue.toLowerCase(),
3896
+ })
3897
+ );
3898
+ return {
3899
+ quoteFingerprint,
3900
+ contracts,
3901
+ spender,
3902
+ inputTokenAddress,
3903
+ currentAllowance: allowanceState.currentAllowance,
3904
+ requiredAllowance: request.amount,
3905
+ allowanceReadError: allowanceState.error,
3906
+ operationFee: operationFeeQuote.fee,
3907
+ operationFeeError: operationFeeQuote.error,
3908
+ totalEstimatedFee:
3909
+ operationFeeQuote.fee !== null ? operationFeeQuote.fee + approval.estimatedFee : null,
3910
+ approval,
3911
+ inputMetadata,
3912
+ queueAssetMetadata: stEthMetadata,
3913
+ queuedStEthAmount,
3914
+ simulation,
3915
+ operationTx,
3916
+ operationTransaction,
3917
+ };
3918
+ }
3919
+
3920
+ #assertLidoWithdrawalAmountWithinLimits(amountOfStETH) {
3921
+ const normalizedAmount = BigInt(amountOfStETH || 0);
3922
+ if (normalizedAmount < LIDO_MIN_STETH_WITHDRAWAL_AMOUNT) {
3923
+ throw createTaggedError(
3924
+ "Lido withdrawal amount is below the minimum queue size.",
3925
+ "lido_withdrawal_amount_too_small",
3926
+ {
3927
+ minStEthAmountRaw: LIDO_MIN_STETH_WITHDRAWAL_AMOUNT.toString(),
3928
+ providedStEthAmountRaw: normalizedAmount.toString(),
3929
+ }
3930
+ );
3931
+ }
3932
+ if (normalizedAmount > LIDO_MAX_STETH_WITHDRAWAL_AMOUNT) {
3933
+ throw createTaggedError(
3934
+ "Lido withdrawal amount exceeds the maximum queue size.",
3935
+ "lido_withdrawal_amount_too_large",
3936
+ {
3937
+ maxStEthAmountRaw: LIDO_MAX_STETH_WITHDRAWAL_AMOUNT.toString(),
3938
+ providedStEthAmountRaw: normalizedAmount.toString(),
3939
+ }
3940
+ );
3941
+ }
3942
+ }
3943
+
3944
+ async #getSingleLidoWithdrawalStatus(runtimeConfig, requestId) {
3945
+ const statuses = await this.#getLidoWithdrawalStatuses(runtimeConfig, [requestId]);
3946
+ if (!statuses.length || statuses[0].owner === ZERO_ADDRESS) {
3947
+ throw createTaggedError(
3948
+ "Lido withdrawal request was not found.",
3949
+ "lido_withdrawal_not_found",
3950
+ {
3951
+ requestId: BigInt(requestId).toString(),
3952
+ }
3953
+ );
3954
+ }
3955
+ return statuses[0];
3956
+ }
3957
+
3958
+ #formatLidoWithdrawalResponse({ runtimeConfig, accountIndex, address, request, plan }) {
3959
+ return {
3960
+ network: runtimeConfig.network,
3961
+ chainId: runtimeConfig.chainId,
3962
+ accountIndex,
3963
+ address,
3964
+ protocol: "lido",
3965
+ operation: request.operation,
3966
+ withdrawalQueue: plan.contracts.withdrawalQueue,
3967
+ operationRequest:
3968
+ request.operation === "claim_withdrawal"
3969
+ ? {
3970
+ requestId: request.requestId.toString(),
3971
+ }
3972
+ : {
3973
+ amount: request.amount.toString(),
3974
+ },
3975
+ inputAsset: plan.inputMetadata,
3976
+ queueAsset: plan.queueAssetMetadata,
3977
+ amountFormatted:
3978
+ request.operation !== "claim_withdrawal" &&
3979
+ plan.inputMetadata &&
3980
+ Number.isInteger(plan.inputMetadata.decimals)
3981
+ ? formatUnits(request.amount, plan.inputMetadata.decimals)
3982
+ : null,
3983
+ queuedStEthAmountRaw:
3984
+ plan.queuedStEthAmount !== undefined && plan.queuedStEthAmount !== null
3985
+ ? plan.queuedStEthAmount.toString()
3986
+ : null,
3987
+ queuedStEthAmountFormatted:
3988
+ plan.queuedStEthAmount !== undefined && plan.queuedStEthAmount !== null
3989
+ ? formatUnits(plan.queuedStEthAmount, LIDO_STETH_DECIMALS)
3990
+ : null,
3991
+ requestId: request.requestId ? request.requestId.toString() : null,
3992
+ withdrawalRequest: plan.withdrawalRequest || null,
3993
+ quoteFingerprint: plan.quoteFingerprint,
3994
+ estimatedFeeWei: plan.totalEstimatedFee !== null ? plan.totalEstimatedFee.toString() : null,
3995
+ estimatedOperationFeeWei: plan.operationFee !== null ? plan.operationFee.toString() : null,
3996
+ estimatedApprovalFeeWei: plan.approval.estimatedFee.toString(),
3997
+ feeEstimateAvailable: plan.operationFee !== null,
3998
+ feeEstimateError: plan.operationFeeError,
3999
+ allowance: {
4000
+ spender: plan.spender,
4001
+ currentAllowance: plan.currentAllowance.toString(),
4002
+ requiredAllowance: plan.requiredAllowance.toString(),
4003
+ approvalRequired: plan.approval.required,
4004
+ approvalSequence: plan.approval.steps,
4005
+ readError: plan.allowanceReadError,
4006
+ },
4007
+ simulation: plan.simulation,
4008
+ operationTransaction: plan.operationTransaction,
4009
+ source: "lido-contracts",
4010
+ };
4011
+ }
4012
+
4013
+ #assertExpectedLidoWithdrawalFingerprint(expectedQuoteFingerprint, actualQuoteFingerprint) {
4014
+ if (!expectedQuoteFingerprint) {
4015
+ return;
4016
+ }
4017
+ if (expectedQuoteFingerprint !== actualQuoteFingerprint) {
4018
+ throw createTaggedError(
4019
+ "Lido withdrawal quote changed since preview. Generate a new preview and approval before execute.",
4020
+ "lido_withdrawal_quote_changed",
4021
+ {
4022
+ expectedQuoteFingerprint,
4023
+ actualQuoteFingerprint,
4024
+ }
4025
+ );
4026
+ }
4027
+ }
4028
+
4029
+ async #executeLidoWithdrawalApprovalsIfNeeded({ account, runtimeConfig, plan }) {
4030
+ if (!plan.approval.required || !plan.spender || isZeroAddress(plan.inputTokenAddress)) {
4031
+ return {
4032
+ performed: false,
4033
+ totalFee: 0n,
4034
+ approveHash: null,
4035
+ resetAllowanceHash: null,
4036
+ };
4037
+ }
4038
+ let totalFee = 0n;
4039
+ let approveHash = null;
4040
+ let resetAllowanceHash = null;
4041
+ for (const step of plan.approval.steps) {
4042
+ const result = await account.approve({
4043
+ token: plan.inputTokenAddress,
4044
+ spender: plan.spender,
4045
+ amount: step.amount,
4046
+ });
4047
+ totalFee += BigInt(result.fee || 0);
4048
+ if (step.type === "reset_allowance") {
4049
+ resetAllowanceHash = result.hash;
4050
+ } else if (step.type === "approve") {
4051
+ approveHash = result.hash;
4052
+ }
4053
+ await this.#waitForTransactionReceipt(runtimeConfig, result.hash);
4054
+ }
4055
+ return {
4056
+ performed: true,
4057
+ totalFee,
4058
+ approveHash,
4059
+ resetAllowanceHash,
4060
+ };
4061
+ }
4062
+
4063
+ async #restoreAllowanceAfterFailedLidoWithdrawal({
4064
+ account,
4065
+ runtimeConfig,
4066
+ tokenAddress,
4067
+ spender,
4068
+ originalAllowance,
4069
+ approvalExecution,
4070
+ }) {
4071
+ if (!approvalExecution?.performed || !tokenAddress || isZeroAddress(tokenAddress) || !spender) {
4072
+ return {
4073
+ attempted: false,
4074
+ restored: false,
4075
+ originalAllowance: BigInt(originalAllowance || 0n).toString(),
4076
+ };
4077
+ }
4078
+ const cleanup = {
4079
+ attempted: true,
4080
+ restored: false,
4081
+ originalAllowance: BigInt(originalAllowance || 0n).toString(),
4082
+ restoreHashes: [],
4083
+ restoreSteps: [],
4084
+ error: null,
4085
+ };
4086
+ try {
4087
+ const restorePlan = await this.#buildAllowanceRestorePlan({
4088
+ account,
4089
+ runtimeConfig,
4090
+ tokenAddress,
4091
+ spender,
4092
+ targetAllowance: BigInt(originalAllowance || 0n),
4093
+ operationLabel: "lido withdrawal",
4094
+ });
4095
+ cleanup.restoreSteps = restorePlan.steps.map((step) => ({ ...step }));
4096
+ if (!restorePlan.required) {
4097
+ cleanup.restored = true;
4098
+ return cleanup;
4099
+ }
4100
+ for (const step of restorePlan.steps) {
4101
+ const result = await account.approve({
4102
+ token: tokenAddress,
4103
+ spender,
4104
+ amount: step.amount,
4105
+ });
4106
+ cleanup.restoreHashes.push({
4107
+ type: step.type,
4108
+ hash: result.hash,
4109
+ fee: BigInt(result.fee || 0).toString(),
4110
+ });
4111
+ await this.#waitForTransactionReceipt(runtimeConfig, result.hash);
4112
+ }
4113
+ const finalAllowance = await account.getAllowance(tokenAddress, spender);
4114
+ cleanup.finalAllowance = finalAllowance.toString();
4115
+ cleanup.restored = finalAllowance === BigInt(originalAllowance || 0n);
4116
+ return cleanup;
4117
+ } catch (cleanupError) {
4118
+ cleanup.error = {
4119
+ message: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
4120
+ code:
4121
+ cleanupError && typeof cleanupError === "object"
4122
+ ? String(cleanupError.errorCode || cleanupError.code || "").trim() || null
4123
+ : null,
4124
+ };
4125
+ return cleanup;
4126
+ }
4127
+ }
4128
+
4129
+ #throwLidoWithdrawalFailureWithCleanup(error, cleanup) {
4130
+ if (cleanup?.attempted && cleanup.restored !== true) {
4131
+ throw createTaggedError(
4132
+ "Lido withdrawal failed after approval and automatic allowance restore did not complete.",
4133
+ "lido_withdrawal_cleanup_failed",
4134
+ {
4135
+ originalError:
4136
+ error instanceof Error
4137
+ ? {
4138
+ message: error.message,
4139
+ code: String(error.errorCode || error.code || "").trim() || null,
4140
+ }
4141
+ : { message: String(error), code: null },
4142
+ cleanup,
4143
+ }
4144
+ );
4145
+ }
4146
+ throw error;
4147
+ }
4148
+
4149
+ #buildLidoOperationTransaction(runtimeConfig, request) {
4150
+ const contracts = this.#getLidoContracts(runtimeConfig.network);
4151
+ if (request.operation === "stake_eth_for_wsteth") {
4152
+ return {
4153
+ to: contracts.referralStaker,
4154
+ value: request.amount,
4155
+ data: LIDO_REFERRAL_STAKER_INTERFACE.encodeFunctionData("stakeETH", [
4156
+ this.#getLidoReferralAddress(),
4157
+ ]),
4158
+ };
4159
+ }
4160
+ return {
4161
+ to: contracts.wsteth.address,
4162
+ value: 0n,
4163
+ data:
4164
+ request.operation === "wrap_steth"
4165
+ ? LIDO_WSTETH_INTERFACE.encodeFunctionData("wrap", [request.amount])
4166
+ : LIDO_WSTETH_INTERFACE.encodeFunctionData("unwrap", [request.amount]),
4167
+ };
4168
+ }
4169
+
4170
+ async #buildLidoApprovalPlan({
4171
+ account,
4172
+ runtimeConfig,
4173
+ tokenAddress,
4174
+ spender,
4175
+ requiredAmount,
4176
+ currentAllowance,
4177
+ }) {
4178
+ const steps = [];
4179
+ if (currentAllowance < requiredAmount) {
4180
+ steps.push({ type: "approve", amount: requiredAmount.toString() });
4181
+ }
4182
+ let estimatedFee = 0n;
4183
+ for (const step of steps) {
4184
+ const quote = await account.quoteSendTransaction(
4185
+ buildErc20ApproveTransaction(tokenAddress, spender, step.amount)
4186
+ );
4187
+ const fee = BigInt(quote.fee);
4188
+ this.#assertMaxFee(runtimeConfig, fee, `lido ${step.type}`);
4189
+ step.estimatedFeeWei = fee.toString();
4190
+ estimatedFee += fee;
4191
+ }
4192
+ return {
4193
+ required: steps.length > 0,
4194
+ estimatedFee,
4195
+ steps,
4196
+ };
4197
+ }
4198
+
4199
+ #formatLidoOperationResponse({ runtimeConfig, accountIndex, address, request, plan }) {
4200
+ return {
4201
+ network: runtimeConfig.network,
4202
+ chainId: runtimeConfig.chainId,
4203
+ accountIndex,
4204
+ address,
4205
+ protocol: "lido",
4206
+ operation: request.operation,
4207
+ preferredPositionToken: "wstETH",
4208
+ operationRequest: {
4209
+ amount: request.amount.toString(),
4210
+ },
4211
+ inputAsset: plan.inputMetadata,
4212
+ outputAsset: plan.outputMetadata,
4213
+ amountFormatted:
4214
+ plan.inputMetadata && Number.isInteger(plan.inputMetadata.decimals)
4215
+ ? formatUnits(request.amount, plan.inputMetadata.decimals)
4216
+ : null,
4217
+ expectedOutputAmountRaw: plan.expectedOutputAmount.toString(),
4218
+ expectedOutputAmountFormatted:
4219
+ plan.outputMetadata && Number.isInteger(plan.outputMetadata.decimals)
4220
+ ? formatUnits(plan.expectedOutputAmount, plan.outputMetadata.decimals)
4221
+ : null,
4222
+ quoteFingerprint: plan.quoteFingerprint,
4223
+ estimatedFeeWei: plan.totalEstimatedFee !== null ? plan.totalEstimatedFee.toString() : null,
4224
+ estimatedOperationFeeWei: plan.operationFee !== null ? plan.operationFee.toString() : null,
4225
+ estimatedApprovalFeeWei: plan.approval.estimatedFee.toString(),
4226
+ feeEstimateAvailable: plan.operationFee !== null,
4227
+ feeEstimateError: plan.operationFeeError,
4228
+ allowance: {
4229
+ spender: plan.spender,
4230
+ currentAllowance: plan.currentAllowance.toString(),
4231
+ requiredAllowance: plan.amount.toString(),
4232
+ approvalRequired: plan.approval.required,
4233
+ approvalSequence: plan.approval.steps,
4234
+ readError: plan.allowanceReadError,
4235
+ },
4236
+ contracts: {
4237
+ stETH: plan.contracts.steth.address,
4238
+ wstETH: plan.contracts.wsteth.address,
4239
+ referralStaker: plan.contracts.referralStaker,
4240
+ withdrawalQueue: plan.contracts.withdrawalQueue,
4241
+ },
4242
+ referralAddress: this.#getLidoReferralAddress(),
4243
+ simulation: plan.simulation,
4244
+ operationTransaction: plan.operationTransaction,
4245
+ source: "lido-contracts",
4246
+ };
4247
+ }
4248
+
4249
+ #assertExpectedLidoFingerprint(expectedQuoteFingerprint, actualQuoteFingerprint) {
4250
+ if (!expectedQuoteFingerprint) {
4251
+ return;
4252
+ }
4253
+ if (expectedQuoteFingerprint !== actualQuoteFingerprint) {
4254
+ throw createTaggedError(
4255
+ "Lido quote changed since preview. Generate a new preview and approval before execute.",
4256
+ "lido_quote_changed",
4257
+ {
4258
+ expectedQuoteFingerprint,
4259
+ actualQuoteFingerprint,
4260
+ }
4261
+ );
4262
+ }
4263
+ }
4264
+
4265
+ async #executeLidoApprovalsIfNeeded({ account, runtimeConfig, request, plan }) {
4266
+ if (!plan.approval.required || !plan.spender || isZeroAddress(plan.inputTokenAddress)) {
4267
+ return {
4268
+ performed: false,
4269
+ totalFee: 0n,
4270
+ approveHash: null,
4271
+ resetAllowanceHash: null,
4272
+ };
4273
+ }
4274
+ let totalFee = 0n;
4275
+ let approveHash = null;
4276
+ let resetAllowanceHash = null;
4277
+ for (const step of plan.approval.steps) {
4278
+ const result = await account.approve({
4279
+ token: plan.inputTokenAddress,
4280
+ spender: plan.spender,
4281
+ amount: step.amount,
4282
+ });
4283
+ totalFee += BigInt(result.fee || 0);
4284
+ if (step.type === "reset_allowance") {
4285
+ resetAllowanceHash = result.hash;
4286
+ } else if (step.type === "approve") {
4287
+ approveHash = result.hash;
4288
+ }
4289
+ await this.#waitForTransactionReceipt(runtimeConfig, result.hash);
4290
+ }
4291
+ return {
4292
+ performed: true,
4293
+ totalFee,
4294
+ approveHash,
4295
+ resetAllowanceHash,
4296
+ };
4297
+ }
4298
+
4299
+ async #restoreAllowanceAfterFailedLidoOperation({
4300
+ account,
4301
+ runtimeConfig,
4302
+ tokenAddress,
4303
+ spender,
4304
+ originalAllowance,
4305
+ approvalExecution,
4306
+ }) {
4307
+ if (!approvalExecution?.performed || !tokenAddress || isZeroAddress(tokenAddress) || !spender) {
4308
+ return {
4309
+ attempted: false,
4310
+ restored: false,
4311
+ originalAllowance: BigInt(originalAllowance || 0n).toString(),
4312
+ };
4313
+ }
4314
+ const cleanup = {
4315
+ attempted: true,
4316
+ restored: false,
4317
+ originalAllowance: BigInt(originalAllowance || 0n).toString(),
4318
+ restoreHashes: [],
4319
+ restoreSteps: [],
4320
+ error: null,
4321
+ };
4322
+ try {
4323
+ const restorePlan = await this.#buildAllowanceRestorePlan({
4324
+ account,
4325
+ runtimeConfig,
4326
+ tokenAddress,
4327
+ spender,
4328
+ targetAllowance: BigInt(originalAllowance || 0n),
4329
+ operationLabel: "lido",
4330
+ });
4331
+ cleanup.restoreSteps = restorePlan.steps.map((step) => ({ ...step }));
4332
+ if (!restorePlan.required) {
4333
+ cleanup.restored = true;
4334
+ return cleanup;
4335
+ }
4336
+ for (const step of restorePlan.steps) {
4337
+ const result = await account.approve({
4338
+ token: tokenAddress,
4339
+ spender,
4340
+ amount: step.amount,
4341
+ });
4342
+ cleanup.restoreHashes.push({
4343
+ type: step.type,
4344
+ hash: result.hash,
4345
+ fee: BigInt(result.fee || 0).toString(),
4346
+ });
4347
+ await this.#waitForTransactionReceipt(runtimeConfig, result.hash);
4348
+ }
4349
+ const finalAllowance = await account.getAllowance(tokenAddress, spender);
4350
+ cleanup.finalAllowance = finalAllowance.toString();
4351
+ cleanup.restored = finalAllowance === BigInt(originalAllowance || 0n);
4352
+ return cleanup;
4353
+ } catch (cleanupError) {
4354
+ cleanup.error = {
4355
+ message: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
4356
+ code:
4357
+ cleanupError && typeof cleanupError === "object"
4358
+ ? String(cleanupError.errorCode || cleanupError.code || "").trim() || null
4359
+ : null,
4360
+ };
4361
+ return cleanup;
4362
+ }
4363
+ }
4364
+
4365
+ #throwLidoFailureWithCleanup(error, cleanup) {
4366
+ if (cleanup?.attempted && cleanup.restored !== true) {
4367
+ throw createTaggedError(
4368
+ "Lido operation failed after approval and automatic allowance restore did not complete.",
4369
+ "lido_cleanup_failed",
4370
+ {
4371
+ originalError:
4372
+ error instanceof Error
4373
+ ? {
4374
+ message: error.message,
4375
+ code: String(error.errorCode || error.code || "").trim() || null,
4376
+ }
4377
+ : { message: String(error), code: null },
4378
+ cleanup,
4379
+ }
4380
+ );
4381
+ }
4382
+ throw error;
4383
+ }
4384
+
4385
+ async #getSwapAllowanceState({ account, tokenAddress, spender }) {
4386
+ try {
4387
+ return {
4388
+ currentAllowance: await account.getAllowance(tokenAddress, spender),
4389
+ error: null,
4390
+ };
4391
+ } catch (error) {
4392
+ if (!isRecoverableAllowanceReadFailure(error)) {
4393
+ throw error;
4394
+ }
4395
+ return {
4396
+ currentAllowance: 0n,
4397
+ error: {
4398
+ code: normalizeErrorCodeValue(error) || null,
4399
+ message: error instanceof Error ? error.message : String(error),
4400
+ },
4401
+ };
4402
+ }
4403
+ }
4404
+
4405
+ async #buildVeloraSwapPlan({
4406
+ account,
4407
+ runtimeConfig,
4408
+ swapRequest,
4409
+ tolerateSwapFeeFailure = false,
4410
+ }) {
4411
+ const protocol = new VeloraProtocolEvm(account);
4412
+ try {
4413
+ const veloraSdk = await protocol._getVeloraSdk();
4414
+ const address = await account.getAddress();
4415
+ const normalizedTokenIn = swapRequest.tokenIn.toLowerCase();
4416
+ const normalizedTokenOut = swapRequest.tokenOut.toLowerCase();
4417
+ const slippageBps = DEFAULT_SWAP_SLIPPAGE_BPS;
4418
+ const priceRoute = await veloraSdk.swap.getRate({
4419
+ srcToken: normalizedTokenIn,
4420
+ destToken: normalizedTokenOut,
4421
+ amount: swapRequest.tokenInAmount.toString(),
4422
+ side: "SELL",
4423
+ });
4424
+ const swapTx = await veloraSdk.swap.buildTx(
4425
+ {
4426
+ partner: "wdk",
4427
+ srcToken: priceRoute.srcToken,
4428
+ destToken: priceRoute.destToken,
4429
+ srcAmount: priceRoute.srcAmount,
4430
+ slippage: slippageBps,
4431
+ userAddress: address,
4432
+ priceRoute,
4433
+ },
4434
+ {
4435
+ ignoreChecks: true,
4436
+ }
4437
+ );
4438
+ const [spender, contracts] = await Promise.all([
4439
+ veloraSdk.swap.getSpender(),
4440
+ typeof veloraSdk.swap.getContracts === "function"
4441
+ ? veloraSdk.swap.getContracts()
4442
+ : Promise.resolve(null),
4443
+ ]);
4444
+ const router = normalizeAddress(
4445
+ String(
4446
+ contracts?.AugustusSwapper ||
4447
+ swapTx.to ||
4448
+ ""
4449
+ ),
4450
+ "router"
4451
+ );
4452
+ const normalizedSpender = normalizeAddress(spender, "spender");
4453
+ const isNativeTokenIn = isVeloraNativeTokenAddress(swapRequest.tokenIn);
4454
+ const allowanceState = isNativeTokenIn
4455
+ ? {
4456
+ currentAllowance: swapRequest.tokenInAmount,
4457
+ error: null,
4458
+ }
4459
+ : await this.#getSwapAllowanceState({
4460
+ account,
4461
+ tokenAddress: swapRequest.tokenIn,
4462
+ spender: normalizedSpender,
4463
+ });
4464
+ const currentAllowance = allowanceState.currentAllowance;
4465
+ const approval = isNativeTokenIn
4466
+ ? {
4467
+ required: false,
4468
+ estimatedFee: 0n,
4469
+ steps: [],
4470
+ }
4471
+ : await this.#buildSwapApprovalPlan({
4472
+ account,
4473
+ runtimeConfig,
4474
+ tokenAddress: swapRequest.tokenIn,
4475
+ spender: normalizedSpender,
4476
+ requiredAmount: swapRequest.tokenInAmount,
4477
+ currentAllowance,
4478
+ });
4479
+ const swapFeeQuote = await this.#quoteSwapTransaction({
4480
+ account,
4481
+ runtimeConfig,
4482
+ from: address,
4483
+ swapTx,
4484
+ fallbackGasLimit: parseOptionalDecimalBigInt(priceRoute?.gasCost),
4485
+ tolerateFailure: tolerateSwapFeeFailure || approval.required,
4486
+ });
4487
+ const swapFee = swapFeeQuote.fee;
4488
+ const simulation = approval.required
4489
+ ? {
4490
+ ok: null,
4491
+ skipped: true,
4492
+ reason: "allowance_required",
4493
+ }
4494
+ : await this.#simulatePreparedTransaction({
4495
+ runtimeConfig,
4496
+ from: address,
4497
+ tx: swapTx,
4498
+ });
4499
+ const swapTransaction = {
4500
+ to: normalizeAddress(String(swapTx.to || ""), "swapTx.to"),
4501
+ value: BigInt(swapTx.value || 0).toString(),
4502
+ dataHash: sha256Hex(String(swapTx.data || "")),
4503
+ };
4504
+ const minimumTokenOutAmount = computeMinimumOutputAmount(priceRoute.destAmount, slippageBps);
4505
+ const quoteFingerprint = sha256Hex(
4506
+ JSON.stringify({
4507
+ chainId: runtimeConfig.chainId,
4508
+ network: runtimeConfig.network,
4509
+ from: address.toLowerCase(),
4510
+ router: router.toLowerCase(),
4511
+ spender: normalizedSpender.toLowerCase(),
4512
+ tokenIn: swapRequest.tokenIn.toLowerCase(),
4513
+ tokenOut: swapRequest.tokenOut.toLowerCase(),
4514
+ tokenInAmount: swapRequest.tokenInAmount.toString(),
4515
+ slippageBps,
4516
+ swapTxTo: swapTransaction.to.toLowerCase(),
4517
+ swapTxValue: swapTransaction.value,
4518
+ })
4519
+ );
4520
+ return {
4521
+ priceRoute,
4522
+ quoteFingerprint,
4523
+ slippageBps,
4524
+ minimumTokenOutAmount,
4525
+ router,
4526
+ spender: normalizedSpender,
4527
+ currentAllowance,
4528
+ allowanceReadError: allowanceState.error,
4529
+ tokenInAmount: BigInt(priceRoute.srcAmount),
4530
+ tokenOutAmount: BigInt(priceRoute.destAmount),
4531
+ swapTx,
4532
+ swapFee,
4533
+ swapFeeError: swapFeeQuote.error,
4534
+ totalEstimatedFee: swapFee !== null ? swapFee + approval.estimatedFee : null,
4535
+ approval,
4536
+ simulation,
4537
+ swapTransaction,
4538
+ };
4539
+ } finally {
4540
+ await maybeDispose(protocol);
4541
+ }
4542
+ }
4543
+
4544
+ async #buildSwapApprovalPlan({
4545
+ account,
4546
+ runtimeConfig,
4547
+ tokenAddress,
4548
+ spender,
4549
+ requiredAmount,
4550
+ currentAllowance,
4551
+ }) {
4552
+ const steps = [];
4553
+ if (currentAllowance < requiredAmount) {
4554
+ if (
4555
+ runtimeConfig.chainId === 1 &&
4556
+ tokenAddress.toLowerCase() === USDT_MAINNET_ADDRESS &&
4557
+ currentAllowance > 0n
4558
+ ) {
4559
+ steps.push({ type: "reset_allowance", amount: "0" });
4560
+ }
4561
+ steps.push({ type: "approve", amount: requiredAmount.toString() });
4562
+ }
4563
+ let estimatedFee = 0n;
4564
+ for (const step of steps) {
4565
+ const quote = await account.quoteSendTransaction(
4566
+ buildErc20ApproveTransaction(tokenAddress, spender, step.amount)
4567
+ );
4568
+ const fee = BigInt(quote.fee);
4569
+ this.#assertMaxFee(runtimeConfig, fee, `swap ${step.type}`);
4570
+ step.estimatedFeeWei = fee.toString();
4571
+ estimatedFee += fee;
4572
+ }
4573
+ return {
4574
+ required: steps.length > 0,
4575
+ estimatedFee,
4576
+ steps,
4577
+ };
4578
+ }
4579
+
4580
+ async #buildAllowanceRestorePlan({
4581
+ account,
4582
+ runtimeConfig,
4583
+ tokenAddress,
4584
+ spender,
4585
+ targetAllowance,
4586
+ operationLabel = "swap",
4587
+ }) {
4588
+ const currentAllowance = await account.getAllowance(tokenAddress, spender);
4589
+ const desiredAllowance = BigInt(targetAllowance);
4590
+ if (currentAllowance === desiredAllowance) {
4591
+ return {
4592
+ currentAllowance,
4593
+ targetAllowance: desiredAllowance,
4594
+ required: false,
4595
+ estimatedFee: 0n,
4596
+ steps: [],
4597
+ };
4598
+ }
4599
+ const steps = [];
4600
+ if (
4601
+ runtimeConfig.chainId === 1 &&
4602
+ tokenAddress.toLowerCase() === USDT_MAINNET_ADDRESS &&
4603
+ currentAllowance > 0n
4604
+ ) {
4605
+ steps.push({ type: "reset_allowance", amount: "0" });
4606
+ if (desiredAllowance > 0n) {
4607
+ steps.push({ type: "restore_allowance", amount: desiredAllowance.toString() });
4608
+ }
4609
+ } else {
4610
+ steps.push({
4611
+ type: desiredAllowance === 0n ? "reset_allowance" : "restore_allowance",
4612
+ amount: desiredAllowance.toString(),
4613
+ });
4614
+ }
4615
+ let estimatedFee = 0n;
4616
+ for (const step of steps) {
4617
+ const quote = await account.quoteSendTransaction(
4618
+ buildErc20ApproveTransaction(tokenAddress, spender, step.amount)
4619
+ );
4620
+ const fee = BigInt(quote.fee);
4621
+ this.#assertMaxFee(runtimeConfig, fee, `${operationLabel} ${step.type}`);
4622
+ step.estimatedFeeWei = fee.toString();
4623
+ estimatedFee += fee;
4624
+ }
4625
+ return {
4626
+ currentAllowance,
4627
+ targetAllowance: desiredAllowance,
4628
+ required: steps.length > 0,
4629
+ estimatedFee,
4630
+ steps,
4631
+ };
4632
+ }
4633
+
4634
+ async #executeSwapApprovalsIfNeeded({ account, runtimeConfig, swapRequest, plan }) {
4635
+ if (!plan.approval.required) {
4636
+ return {
4637
+ performed: false,
4638
+ totalFee: 0n,
4639
+ approveHash: null,
4640
+ resetAllowanceHash: null,
4641
+ };
4642
+ }
4643
+ let totalFee = 0n;
4644
+ let approveHash = null;
4645
+ let resetAllowanceHash = null;
4646
+ for (const step of plan.approval.steps) {
4647
+ const result = await account.approve({
4648
+ token: swapRequest.tokenIn,
4649
+ spender: plan.spender,
4650
+ amount: step.amount,
4651
+ });
4652
+ totalFee += BigInt(result.fee || 0);
4653
+ if (step.type === "reset_allowance") {
4654
+ resetAllowanceHash = result.hash;
4655
+ } else if (step.type === "approve") {
4656
+ approveHash = result.hash;
4657
+ }
4658
+ await this.#waitForTransactionReceipt(runtimeConfig, result.hash);
4659
+ }
4660
+ return {
4661
+ performed: true,
4662
+ totalFee,
4663
+ approveHash,
4664
+ resetAllowanceHash,
4665
+ };
4666
+ }
4667
+
4668
+ async #quoteSwapTransaction({
4669
+ account,
4670
+ runtimeConfig,
4671
+ from,
4672
+ swapTx,
4673
+ fallbackGasLimit = null,
4674
+ tolerateFailure,
4675
+ operationLabel = "swap",
4676
+ }) {
4677
+ try {
4678
+ const quote = await account.quoteSendTransaction(swapTx);
4679
+ const fee = BigInt(quote.fee);
4680
+ this.#assertMaxFee(runtimeConfig, fee, operationLabel);
4681
+ return {
4682
+ fee,
4683
+ error: null,
4684
+ };
4685
+ } catch (error) {
4686
+ const insufficientFundsHint = parseInsufficientFundsHint(error);
4687
+ if (
4688
+ normalizeErrorCodeValue(error) === "insufficient_funds" ||
4689
+ insufficientFundsHint !== null
4690
+ ) {
4691
+ try {
4692
+ const rpcQuote = await this.#quotePreparedTransactionFromRpc({
4693
+ runtimeConfig,
4694
+ from,
4695
+ tx: swapTx,
4696
+ operationLabel,
4697
+ });
4698
+ return {
4699
+ fee: rpcQuote.fee,
4700
+ error: null,
4701
+ };
4702
+ } catch (rpcEstimateError) {
4703
+ if (fallbackGasLimit !== null) {
4704
+ try {
4705
+ const routeQuote = await this.#quotePreparedTransactionFromGasLimit({
4706
+ runtimeConfig,
4707
+ gasLimit: fallbackGasLimit,
4708
+ operationLabel,
4709
+ });
4710
+ return {
4711
+ fee: routeQuote.fee,
4712
+ error: null,
4713
+ };
4714
+ } catch {
4715
+ // Fall through to degraded error reporting below.
4716
+ }
4717
+ }
4718
+ if (!tolerateFailure || !isRecoverableSwapFeeEstimateFailure(rpcEstimateError)) {
4719
+ if (tolerateFailure) {
4720
+ return {
4721
+ fee: null,
4722
+ error: {
4723
+ code: normalizeErrorCodeValue(error) || null,
4724
+ message:
4725
+ error instanceof Error
4726
+ ? error.message
4727
+ : String(error),
4728
+ ...(insufficientFundsHint ? insufficientFundsHint : {}),
4729
+ fallbackError: {
4730
+ code: normalizeErrorCodeValue(rpcEstimateError) || null,
4731
+ message:
4732
+ rpcEstimateError instanceof Error
4733
+ ? rpcEstimateError.message
4734
+ : String(rpcEstimateError),
4735
+ },
4736
+ },
4737
+ };
4738
+ }
4739
+ throw rpcEstimateError;
4740
+ }
4741
+ const hint = parseInsufficientFundsHint(rpcEstimateError);
4742
+ return {
4743
+ fee: null,
4744
+ error: {
4745
+ code: normalizeErrorCodeValue(rpcEstimateError) || null,
4746
+ message:
4747
+ rpcEstimateError instanceof Error
4748
+ ? rpcEstimateError.message
4749
+ : String(rpcEstimateError),
4750
+ ...(hint ? hint : {}),
4751
+ },
4752
+ };
4753
+ }
4754
+ }
4755
+ if (!tolerateFailure || !isRecoverableSwapFeeEstimateFailure(error)) {
4756
+ throw error;
4757
+ }
4758
+ if (fallbackGasLimit !== null) {
4759
+ try {
4760
+ const routeQuote = await this.#quotePreparedTransactionFromGasLimit({
4761
+ runtimeConfig,
4762
+ gasLimit: fallbackGasLimit,
4763
+ operationLabel,
4764
+ });
4765
+ return {
4766
+ fee: routeQuote.fee,
4767
+ error: null,
4768
+ };
4769
+ } catch {
4770
+ // Fall through to degraded error reporting below.
4771
+ }
4772
+ }
4773
+ return {
4774
+ fee: null,
4775
+ error: {
4776
+ code: normalizeErrorCodeValue(error) || null,
4777
+ message: error instanceof Error ? error.message : String(error),
4778
+ ...(insufficientFundsHint ? insufficientFundsHint : {}),
4779
+ },
4780
+ };
4781
+ }
4782
+ }
4783
+
4784
+ async #quotePreparedTransactionFromRpc({ runtimeConfig, from, tx, operationLabel = "swap" }) {
4785
+ const gasLimitHex = await rpcRequest(runtimeConfig.providerUrl, "eth_estimateGas", [
4786
+ {
4787
+ from: normalizeAddress(from, "from"),
4788
+ to: normalizeAddress(String(tx.to || ""), "to"),
4789
+ data: assertNonEmptyString(String(tx.data || ""), "data"),
4790
+ value: toRpcHex(tx.value || 0),
4791
+ },
4792
+ ]);
4793
+ const gasLimit = BigInt(gasLimitHex || "0x0");
4794
+ const effectiveFeePerGas = await this.#getEffectiveGasPrice(runtimeConfig);
4795
+ const fee = gasLimit * effectiveFeePerGas;
4796
+ this.#assertMaxFee(runtimeConfig, fee, operationLabel);
4797
+ return {
4798
+ gasLimit,
4799
+ effectiveFeePerGas,
4800
+ fee,
4801
+ };
4802
+ }
4803
+
4804
+ async #quotePreparedTransactionFromGasLimit({
4805
+ runtimeConfig,
4806
+ gasLimit,
4807
+ operationLabel = "swap",
4808
+ }) {
4809
+ const normalizedGasLimit = BigInt(gasLimit);
4810
+ const effectiveFeePerGas = await this.#getEffectiveGasPrice(runtimeConfig);
4811
+ const fee = normalizedGasLimit * effectiveFeePerGas;
4812
+ this.#assertMaxFee(runtimeConfig, fee, operationLabel);
4813
+ return {
4814
+ gasLimit: normalizedGasLimit,
4815
+ effectiveFeePerGas,
4816
+ fee,
4817
+ };
4818
+ }
4819
+
4820
+ async #getEffectiveGasPrice(runtimeConfig) {
4821
+ const gasPriceHex = await rpcRequest(runtimeConfig.providerUrl, "eth_gasPrice", []);
4822
+ const priorityHex = await rpcRequest(
4823
+ runtimeConfig.providerUrl,
4824
+ "eth_maxPriorityFeePerGas",
4825
+ []
4826
+ );
4827
+ const feeHistory = await rpcRequest(
4828
+ runtimeConfig.providerUrl,
4829
+ "eth_feeHistory",
4830
+ ["0x1", "latest", []]
4831
+ );
4832
+ const baseFeeItems = Array.isArray(feeHistory?.baseFeePerGas) ? feeHistory.baseFeePerGas : [];
4833
+ const latestBaseFeeHex = baseFeeItems.length ? baseFeeItems[baseFeeItems.length - 1] : "0x0";
4834
+ const baseFeePerGas = BigInt(latestBaseFeeHex || "0x0");
4835
+ const priorityFeePerGas = BigInt(priorityHex || "0x0");
4836
+ const gasPrice = BigInt(gasPriceHex || "0x0");
4837
+ return gasPrice > baseFeePerGas + priorityFeePerGas
4838
+ ? gasPrice
4839
+ : baseFeePerGas + priorityFeePerGas;
4840
+ }
4841
+
4842
+ async #restoreAllowanceAfterFailedSwap({
4843
+ account,
4844
+ runtimeConfig,
4845
+ tokenAddress,
4846
+ spender,
4847
+ originalAllowance,
4848
+ approvalExecution,
4849
+ }) {
4850
+ if (!approvalExecution?.performed) {
4851
+ return {
4852
+ attempted: false,
4853
+ restored: false,
4854
+ originalAllowance: BigInt(originalAllowance || 0n).toString(),
4855
+ };
4856
+ }
4857
+ const cleanup = {
4858
+ attempted: true,
4859
+ restored: false,
4860
+ originalAllowance: BigInt(originalAllowance || 0n).toString(),
4861
+ restoreHashes: [],
4862
+ restoreSteps: [],
4863
+ error: null,
4864
+ };
4865
+ try {
4866
+ const restorePlan = await this.#buildAllowanceRestorePlan({
4867
+ account,
4868
+ runtimeConfig,
4869
+ tokenAddress,
4870
+ spender,
4871
+ targetAllowance: BigInt(originalAllowance || 0n),
4872
+ operationLabel: "aave",
4873
+ });
4874
+ cleanup.restoreSteps = restorePlan.steps.map((step) => ({ ...step }));
4875
+ if (!restorePlan.required) {
4876
+ cleanup.restored = true;
4877
+ return cleanup;
4878
+ }
4879
+ for (const step of restorePlan.steps) {
4880
+ const result = await account.approve({
4881
+ token: tokenAddress,
4882
+ spender,
4883
+ amount: step.amount,
4884
+ });
4885
+ cleanup.restoreHashes.push({
4886
+ type: step.type,
4887
+ hash: result.hash,
4888
+ fee: BigInt(result.fee || 0).toString(),
4889
+ });
4890
+ await this.#waitForTransactionReceipt(runtimeConfig, result.hash);
4891
+ }
4892
+ const finalAllowance = await account.getAllowance(tokenAddress, spender);
4893
+ cleanup.finalAllowance = finalAllowance.toString();
4894
+ cleanup.restored = finalAllowance === BigInt(originalAllowance || 0n);
4895
+ return cleanup;
4896
+ } catch (cleanupError) {
4897
+ cleanup.error = {
4898
+ message: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
4899
+ code:
4900
+ cleanupError && typeof cleanupError === "object"
4901
+ ? String(cleanupError.errorCode || cleanupError.code || "").trim() || null
4902
+ : null,
4903
+ };
4904
+ return cleanup;
4905
+ }
4906
+ }
4907
+
4908
+ #throwSwapFailureWithCleanup(error, cleanup) {
4909
+ if (cleanup?.attempted && cleanup.restored !== true) {
4910
+ throw createTaggedError(
4911
+ "Swap failed after approval and automatic allowance restore did not complete.",
4912
+ "swap_cleanup_failed",
4913
+ {
4914
+ originalError:
4915
+ error instanceof Error
4916
+ ? {
4917
+ message: error.message,
4918
+ code: String(error.errorCode || error.code || "").trim() || null,
4919
+ }
4920
+ : { message: String(error), code: null },
4921
+ cleanup,
4922
+ }
4923
+ );
4924
+ }
4925
+ throw error;
4926
+ }
4927
+
4928
+ async #simulatePreparedTransaction({ runtimeConfig, from, tx, operationLabel = "Swap" }) {
4929
+ try {
4930
+ await rpcRequest(runtimeConfig.providerUrl, "eth_call", [
4931
+ {
4932
+ from: normalizeAddress(from, "from"),
4933
+ to: normalizeAddress(String(tx.to || ""), "to"),
4934
+ data: assertNonEmptyString(String(tx.data || ""), "data"),
4935
+ value: toRpcHex(tx.value || 0),
4936
+ },
4937
+ "latest",
4938
+ ]);
4939
+ return {
4940
+ ok: true,
4941
+ skipped: false,
4942
+ };
4943
+ } catch (error) {
4944
+ const message = error instanceof Error ? error.message : String(error);
4945
+ return {
4946
+ ok: false,
4947
+ skipped: false,
4948
+ message: `${operationLabel} simulation failed: ${message}`,
4949
+ details:
4950
+ error && typeof error === "object" && error.errorDetails && typeof error.errorDetails === "object"
4951
+ ? { ...error.errorDetails }
4952
+ : {},
4953
+ };
4954
+ }
4955
+ }
4956
+
4957
+ async #waitForTransactionReceipt(runtimeConfig, txHash) {
4958
+ for (let attempt = 0; attempt < 30; attempt += 1) {
4959
+ const receipt = await rpcRequest(runtimeConfig.providerUrl, "eth_getTransactionReceipt", [txHash]);
4960
+ if (receipt) {
4961
+ const status = String(receipt.status || "").toLowerCase();
4962
+ if (status === "0x0") {
4963
+ throw createTaggedError("Approval transaction reverted onchain.", "swap_approval_failed", {
4964
+ txHash,
4965
+ network: runtimeConfig.network,
4966
+ });
4967
+ }
4968
+ return receipt;
4969
+ }
4970
+ await new Promise((resolve) => setTimeout(resolve, 1000));
4971
+ }
4972
+ throw createTaggedError(
4973
+ "Timed out waiting for approval transaction confirmation.",
4974
+ "swap_approval_timeout",
4975
+ {
4976
+ txHash,
4977
+ network: runtimeConfig.network,
4978
+ }
4979
+ );
4980
+ }
4981
+ }