@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.
- package/.openclaw/AGENTS.md +98 -0
- package/.openclaw/extensions/agent-wallet/README.md +127 -0
- package/.openclaw/extensions/agent-wallet/index.ts +1520 -0
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +184 -0
- package/.openclaw/extensions/agent-wallet/package.json +11 -0
- package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +20 -0
- package/CHANGELOG.md +42 -0
- package/LICENSE +104 -0
- package/README.md +332 -0
- package/RELEASING.md +204 -0
- package/agent-wallet/.env.example +62 -0
- package/agent-wallet/AGENTS.md +129 -0
- package/agent-wallet/README.md +527 -0
- package/agent-wallet/agent_wallet/__init__.py +11 -0
- package/agent-wallet/agent_wallet/approval.py +161 -0
- package/agent-wallet/agent_wallet/bootstrap.py +178 -0
- package/agent-wallet/agent_wallet/btc_user_wallets.py +217 -0
- package/agent-wallet/agent_wallet/config.py +382 -0
- package/agent-wallet/agent_wallet/encrypted_storage.py +161 -0
- package/agent-wallet/agent_wallet/evm_user_wallets.py +370 -0
- package/agent-wallet/agent_wallet/exceptions.py +9 -0
- package/agent-wallet/agent_wallet/file_ops.py +34 -0
- package/agent-wallet/agent_wallet/http_client.py +25 -0
- package/agent-wallet/agent_wallet/models.py +66 -0
- package/agent-wallet/agent_wallet/nonce_registry.py +59 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +5128 -0
- package/agent-wallet/agent_wallet/openclaw_cli.py +626 -0
- package/agent-wallet/agent_wallet/openclaw_runtime.py +272 -0
- package/agent-wallet/agent_wallet/plugin_bundle.py +42 -0
- package/agent-wallet/agent_wallet/providers/__init__.py +1 -0
- package/agent-wallet/agent_wallet/providers/bags.py +259 -0
- package/agent-wallet/agent_wallet/providers/evm_portfolio.py +470 -0
- package/agent-wallet/agent_wallet/providers/jupiter.py +567 -0
- package/agent-wallet/agent_wallet/providers/kamino.py +215 -0
- package/agent-wallet/agent_wallet/providers/lifi.py +277 -0
- package/agent-wallet/agent_wallet/providers/solana_rpc.py +470 -0
- package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +114 -0
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +205 -0
- package/agent-wallet/agent_wallet/sealed_keys.py +61 -0
- package/agent-wallet/agent_wallet/solana_stake.py +103 -0
- package/agent-wallet/agent_wallet/solana_tx.py +93 -0
- package/agent-wallet/agent_wallet/spending_limits.py +101 -0
- package/agent-wallet/agent_wallet/transaction_policy.py +518 -0
- package/agent-wallet/agent_wallet/user_wallets.py +355 -0
- package/agent-wallet/agent_wallet/validation.py +31 -0
- package/agent-wallet/agent_wallet/wallet_layer/__init__.py +1 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +808 -0
- package/agent-wallet/agent_wallet/wallet_layer/base58.py +44 -0
- package/agent-wallet/agent_wallet/wallet_layer/factory.py +102 -0
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +4252 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +272 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +1628 -0
- package/agent-wallet/examples/bootstrap_wallet.py +21 -0
- package/agent-wallet/examples/openclaw_runtime_onboarding.py +28 -0
- package/agent-wallet/examples/openclaw_user_wallet_example.py +31 -0
- package/agent-wallet/examples/openclaw_wallet_adapter_example.py +33 -0
- package/agent-wallet/openclaw.plugin.json +138 -0
- package/agent-wallet/pyproject.toml +31 -0
- package/agent-wallet/scripts/bootstrap_openclaw_btc.py +278 -0
- package/agent-wallet/scripts/build_release_bundle.py +188 -0
- package/agent-wallet/scripts/finalize_openclaw_local_wallet_config.py +121 -0
- package/agent-wallet/scripts/install_agent_wallet.py +505 -0
- package/agent-wallet/scripts/install_openclaw_local_config.py +226 -0
- package/agent-wallet/scripts/install_openclaw_sealed_keys.py +105 -0
- package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +244 -0
- package/agent-wallet/scripts/reveal_btc_seed.sh +130 -0
- package/agent-wallet/scripts/security_utils.py +37 -0
- package/agent-wallet/scripts/setup_btc_wallet.sh +146 -0
- package/agent-wallet/scripts/switch_openclaw_wallet_network.py +106 -0
- package/agent-wallet/skills/wallet-operator/SKILL.md +128 -0
- package/bin/openclaw-agent-wallet.mjs +487 -0
- package/install-from-github.sh +134 -0
- package/package.json +61 -0
- package/setup.sh +40 -0
- package/wdk-btc-wallet/README.md +325 -0
- package/wdk-btc-wallet/bootstrap.sh +22 -0
- package/wdk-btc-wallet/package-lock.json +1839 -0
- package/wdk-btc-wallet/package.json +18 -0
- package/wdk-btc-wallet/run-local.sh +21 -0
- package/wdk-btc-wallet/src/config.js +160 -0
- package/wdk-btc-wallet/src/json.js +35 -0
- package/wdk-btc-wallet/src/local_vault.js +432 -0
- package/wdk-btc-wallet/src/network_state.js +84 -0
- package/wdk-btc-wallet/src/server.js +257 -0
- package/wdk-btc-wallet/src/wdk_btc_wallet.js +332 -0
- package/wdk-evm-wallet/README.md +183 -0
- package/wdk-evm-wallet/bootstrap.sh +8 -0
- package/wdk-evm-wallet/package-lock.json +2340 -0
- package/wdk-evm-wallet/package.json +23 -0
- package/wdk-evm-wallet/run-local.sh +12 -0
- package/wdk-evm-wallet/src/config.js +274 -0
- package/wdk-evm-wallet/src/json.js +35 -0
- package/wdk-evm-wallet/src/local_vault.js +430 -0
- package/wdk-evm-wallet/src/network_state.js +92 -0
- package/wdk-evm-wallet/src/server.js +575 -0
- 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
|
+
}
|