@stablechain/sdk 0.1.0 → 1.0.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/dist/index.cjs +577 -333
- package/dist/index.d.cts +190 -8
- package/dist/index.d.ts +190 -8
- package/dist/index.js +586 -342
- package/package.json +12 -5
package/dist/index.js
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
// src/stable.ts
|
|
2
2
|
import {
|
|
3
|
-
createPublicClient as
|
|
3
|
+
createPublicClient as createPublicClient4,
|
|
4
4
|
createWalletClient as createWalletClient3,
|
|
5
|
-
erc20Abi as
|
|
6
|
-
http as
|
|
5
|
+
erc20Abi as erc20Abi4,
|
|
6
|
+
http as http4,
|
|
7
7
|
isAddress as isAddress3,
|
|
8
|
-
parseUnits as
|
|
8
|
+
parseUnits as parseUnits4
|
|
9
|
+
} from "viem";
|
|
10
|
+
import { stable as stableMainnet2, stableTestnet as stableTestnet2 } from "viem/chains";
|
|
11
|
+
|
|
12
|
+
// src/earn/reader.ts
|
|
13
|
+
import {
|
|
14
|
+
createPublicClient,
|
|
15
|
+
erc20Abi,
|
|
16
|
+
formatUnits,
|
|
17
|
+
http,
|
|
18
|
+
isAddress,
|
|
19
|
+
parseUnits
|
|
9
20
|
} from "viem";
|
|
10
21
|
import { stable as stableMainnet, stableTestnet } from "viem/chains";
|
|
22
|
+
import { MorphoClient } from "@morpho-org/morpho-sdk";
|
|
11
23
|
|
|
12
24
|
// src/errors.ts
|
|
13
25
|
import { BaseError } from "viem";
|
|
@@ -67,6 +79,14 @@ var StableNetworkError = class extends StableError {
|
|
|
67
79
|
this.url = args.url;
|
|
68
80
|
}
|
|
69
81
|
};
|
|
82
|
+
var StableBulkError = class extends StableError {
|
|
83
|
+
constructor(args) {
|
|
84
|
+
super(args.shortMessage, { name: "StableBulkError" });
|
|
85
|
+
this.results = args.results;
|
|
86
|
+
this.succeeded = args.succeeded;
|
|
87
|
+
this.failed = args.failed;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
70
90
|
function formatValue(v) {
|
|
71
91
|
if (typeof v === "bigint") return `${v}n`;
|
|
72
92
|
if (typeof v === "string") return JSON.stringify(v);
|
|
@@ -79,19 +99,6 @@ function formatValue(v) {
|
|
|
79
99
|
}
|
|
80
100
|
}
|
|
81
101
|
|
|
82
|
-
// src/services/layerzero.ts
|
|
83
|
-
import {
|
|
84
|
-
concat,
|
|
85
|
-
createPublicClient,
|
|
86
|
-
createWalletClient,
|
|
87
|
-
erc20Abi,
|
|
88
|
-
http,
|
|
89
|
-
isAddress,
|
|
90
|
-
numberToHex,
|
|
91
|
-
padHex,
|
|
92
|
-
parseUnits
|
|
93
|
-
} from "viem";
|
|
94
|
-
|
|
95
102
|
// src/helpers/chain.ts
|
|
96
103
|
import { defineChain } from "viem";
|
|
97
104
|
function makeChain(id, rpc) {
|
|
@@ -103,6 +110,38 @@ function makeChain(id, rpc) {
|
|
|
103
110
|
});
|
|
104
111
|
}
|
|
105
112
|
|
|
113
|
+
// src/helpers/retry.ts
|
|
114
|
+
var HttpError = class extends Error {
|
|
115
|
+
constructor(message, status, retryable) {
|
|
116
|
+
super(message);
|
|
117
|
+
this.status = status;
|
|
118
|
+
this.retryable = retryable;
|
|
119
|
+
this.name = "HttpError";
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
function isRetryableError(err) {
|
|
123
|
+
if (err instanceof HttpError) return err.retryable;
|
|
124
|
+
return err instanceof TypeError;
|
|
125
|
+
}
|
|
126
|
+
async function retryWithBackoff(fn, isRetryable = isRetryableError, opts = {}) {
|
|
127
|
+
const maxAttempts = opts.maxAttempts ?? 3;
|
|
128
|
+
const base = opts.baseDelayMs ?? 250;
|
|
129
|
+
const max = opts.maxDelayMs ?? 4e3;
|
|
130
|
+
let lastErr;
|
|
131
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
132
|
+
try {
|
|
133
|
+
return await fn();
|
|
134
|
+
} catch (err) {
|
|
135
|
+
lastErr = err;
|
|
136
|
+
if (attempt === maxAttempts - 1 || !isRetryable(err)) throw err;
|
|
137
|
+
const delay = Math.min(max, base * 2 ** attempt);
|
|
138
|
+
const jittered = delay * (0.75 + Math.random() * 0.5);
|
|
139
|
+
await new Promise((r) => setTimeout(r, jittered));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
throw lastErr;
|
|
143
|
+
}
|
|
144
|
+
|
|
106
145
|
// src/types.ts
|
|
107
146
|
var Chain = /* @__PURE__ */ ((Chain2) => {
|
|
108
147
|
Chain2["Sepolia"] = "sepolia";
|
|
@@ -124,6 +163,156 @@ var Network = /* @__PURE__ */ ((Network2) => {
|
|
|
124
163
|
return Network2;
|
|
125
164
|
})(Network || {});
|
|
126
165
|
|
|
166
|
+
// src/earn/reader.ts
|
|
167
|
+
var MORPHO_API_URL = "https://blue-api.morpho.org/graphql";
|
|
168
|
+
var EarnReader = class {
|
|
169
|
+
constructor(config) {
|
|
170
|
+
if (!config.earn?.vault) {
|
|
171
|
+
throw new StableValidationError(
|
|
172
|
+
"earn.vault",
|
|
173
|
+
void 0,
|
|
174
|
+
"createStableReader: earn.vault is required"
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (!isAddress(config.address)) {
|
|
178
|
+
throw new StableValidationError(
|
|
179
|
+
"address",
|
|
180
|
+
config.address,
|
|
181
|
+
`createStableReader: invalid user address "${config.address}"`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (!isAddress(config.earn.vault)) {
|
|
185
|
+
throw new StableValidationError(
|
|
186
|
+
"earn.vault",
|
|
187
|
+
config.earn.vault,
|
|
188
|
+
`createStableReader: invalid vault address "${config.earn.vault}"`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const network = config.network ?? "mainnet" /* Mainnet */;
|
|
192
|
+
const viemChain = network === "mainnet" /* Mainnet */ ? stableMainnet : stableTestnet;
|
|
193
|
+
const rpc = (config.rpc ?? viemChain.rpcUrls.default.http[0]).replace(/\/$/, "");
|
|
194
|
+
this.chainId = viemChain.id;
|
|
195
|
+
this.vaultAddr = config.earn.vault;
|
|
196
|
+
this.userAddr = config.address;
|
|
197
|
+
const chain = makeChain(viemChain.id, rpc);
|
|
198
|
+
this.pc = createPublicClient({ chain, transport: http(rpc) });
|
|
199
|
+
this.morpho = new MorphoClient(this.pc);
|
|
200
|
+
}
|
|
201
|
+
async position() {
|
|
202
|
+
const vault = this.morpho.vaultV2(this.vaultAddr, this.chainId);
|
|
203
|
+
const [vaultData, shares] = await Promise.all([
|
|
204
|
+
vault.getData(),
|
|
205
|
+
this.pc.readContract({
|
|
206
|
+
address: this.vaultAddr,
|
|
207
|
+
abi: erc20Abi,
|
|
208
|
+
functionName: "balanceOf",
|
|
209
|
+
args: [this.userAddr]
|
|
210
|
+
})
|
|
211
|
+
]);
|
|
212
|
+
const [tokenDecimals, shareDecimals] = await Promise.all([
|
|
213
|
+
this.pc.readContract({ address: vaultData.asset, abi: erc20Abi, functionName: "decimals" }),
|
|
214
|
+
this.pc.readContract({ address: this.vaultAddr, abi: erc20Abi, functionName: "decimals" })
|
|
215
|
+
]);
|
|
216
|
+
const assetsRaw = vaultData.toAssets(shares);
|
|
217
|
+
return {
|
|
218
|
+
shares,
|
|
219
|
+
sharesFormatted: Number(formatUnits(shares, shareDecimals)),
|
|
220
|
+
shareDecimals,
|
|
221
|
+
assets: Number(formatUnits(assetsRaw, tokenDecimals)),
|
|
222
|
+
tokenDecimals,
|
|
223
|
+
assetAddress: vaultData.asset
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
async preview(params) {
|
|
227
|
+
const { amount, horizonDays } = params;
|
|
228
|
+
const apy = await this._fetchNetApy();
|
|
229
|
+
return {
|
|
230
|
+
projectedYield: amount * apy * (horizonDays / 365),
|
|
231
|
+
apy,
|
|
232
|
+
horizonDays
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
async withdrawability(params) {
|
|
236
|
+
const vault = this.morpho.vaultV2(this.vaultAddr, this.chainId);
|
|
237
|
+
const vaultData = await vault.getData();
|
|
238
|
+
const tokenDecimals = params.tokenDecimals ?? await this.pc.readContract({
|
|
239
|
+
address: vaultData.asset,
|
|
240
|
+
abi: erc20Abi,
|
|
241
|
+
functionName: "decimals"
|
|
242
|
+
});
|
|
243
|
+
const requestedRaw = parseUnits(String(params.amount), tokenDecimals);
|
|
244
|
+
const availableLiquidityRaw = vaultData.assetBalance;
|
|
245
|
+
return {
|
|
246
|
+
isInstant: requestedRaw <= availableLiquidityRaw,
|
|
247
|
+
availableLiquidity: Number(formatUnits(availableLiquidityRaw, tokenDecimals)),
|
|
248
|
+
tokenDecimals
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async _fetchNetApy() {
|
|
252
|
+
const query = `
|
|
253
|
+
query VaultNetApy($address: String!, $chainId: Int!) {
|
|
254
|
+
vaultV2ByAddress(address: $address, chainId: $chainId) {
|
|
255
|
+
netApy
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
`;
|
|
259
|
+
let body;
|
|
260
|
+
try {
|
|
261
|
+
body = await retryWithBackoff(
|
|
262
|
+
async () => {
|
|
263
|
+
const res = await fetch(MORPHO_API_URL, {
|
|
264
|
+
method: "POST",
|
|
265
|
+
headers: { "Content-Type": "application/json" },
|
|
266
|
+
body: JSON.stringify({
|
|
267
|
+
query,
|
|
268
|
+
variables: { address: this.vaultAddr, chainId: this.chainId }
|
|
269
|
+
})
|
|
270
|
+
});
|
|
271
|
+
if (!res.ok) {
|
|
272
|
+
const retryable = res.status === 429 || res.status >= 500;
|
|
273
|
+
throw new HttpError(
|
|
274
|
+
`earn preview: Morpho API returned ${res.status} \u2014 ${res.statusText}`,
|
|
275
|
+
res.status,
|
|
276
|
+
retryable
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return res.json().catch(() => void 0);
|
|
280
|
+
},
|
|
281
|
+
(err) => err instanceof HttpError ? err.retryable : err instanceof TypeError
|
|
282
|
+
);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
if (err instanceof TypeError || err instanceof HttpError) {
|
|
285
|
+
throw new StableNetworkError({
|
|
286
|
+
shortMessage: err instanceof HttpError ? `earn preview: Morpho API returned ${err.status} after retries` : `earn preview: failed to reach Morpho API after retries \u2014 ${String(err)}`,
|
|
287
|
+
url: MORPHO_API_URL,
|
|
288
|
+
cause: err
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
const netApy = body?.data?.vaultV2ByAddress?.netApy;
|
|
294
|
+
if (typeof netApy !== "number") {
|
|
295
|
+
throw new StableValidationError(
|
|
296
|
+
"apy",
|
|
297
|
+
void 0,
|
|
298
|
+
"earn preview: vault not found in Morpho API or unexpected response shape"
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return netApy;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// src/services/lifi.ts
|
|
306
|
+
import {
|
|
307
|
+
createPublicClient as createPublicClient2,
|
|
308
|
+
createWalletClient,
|
|
309
|
+
erc20Abi as erc20Abi2,
|
|
310
|
+
formatUnits as formatUnits2,
|
|
311
|
+
http as http2,
|
|
312
|
+
isAddress as isAddress2,
|
|
313
|
+
parseUnits as parseUnits2
|
|
314
|
+
} from "viem";
|
|
315
|
+
|
|
127
316
|
// src/chains.ts
|
|
128
317
|
var CHAIN_CONFIGS = {
|
|
129
318
|
// ─── Testnet ──────────────────────────────────────────────────────────────
|
|
@@ -196,305 +385,6 @@ var CHAIN_CONFIGS = {
|
|
|
196
385
|
}
|
|
197
386
|
};
|
|
198
387
|
|
|
199
|
-
// src/services/layerzero.ts
|
|
200
|
-
var C = CHAIN_CONFIGS;
|
|
201
|
-
var BRIDGE_CHAIN_CONFIGS = {
|
|
202
|
-
// ─── Testnet ───────────────────────────────────────────────────────────────
|
|
203
|
-
["sepolia" /* Sepolia */]: { ...C["sepolia" /* Sepolia */], lzEid: 40161, oftAdapter: "0xc099cD946d5efCC35A99D64E808c1430cEf08126" },
|
|
204
|
-
["stable-testnet" /* StableTestnet */]: { ...C["stable-testnet" /* StableTestnet */], lzEid: 40374, oftAdapter: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736" },
|
|
205
|
-
// ─── Mainnet ──────────────────────────────────────────────────────────────
|
|
206
|
-
["stable" /* Stable */]: { ...C["stable" /* Stable */], lzEid: 30396, oftAdapter: "0xedaba024be4d87974d5aB11C6Dd586963CcCB027" },
|
|
207
|
-
["ethereum" /* Ethereum */]: { ...C["ethereum" /* Ethereum */], lzEid: 30101, oftAdapter: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee" },
|
|
208
|
-
["arbitrum" /* Arbitrum */]: { ...C["arbitrum" /* Arbitrum */], lzEid: 30110, oftAdapter: "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92" },
|
|
209
|
-
["ink" /* Ink */]: { ...C["ink" /* Ink */], lzEid: 30339, oftAdapter: "0x1cB6De532588fCA4a21B7209DE7C456AF8434A65" },
|
|
210
|
-
["bera" /* Bera */]: { ...C["bera" /* Bera */], lzEid: 30362, oftAdapter: "0x3Dc96399109df5ceb2C226664A086140bD0379cB" },
|
|
211
|
-
["megaeth" /* MegaETH */]: { ...C["megaeth" /* MegaETH */], lzEid: 30398, oftAdapter: "0x9151434b16b9763660705744891fa906f660ecc5" },
|
|
212
|
-
["hyperevm" /* HyperEVM */]: { ...C["hyperevm" /* HyperEVM */], lzEid: 30367, oftAdapter: "0x904861a24F30EC96ea7CFC3bE9EA4B476d237e98" }
|
|
213
|
-
};
|
|
214
|
-
var OFT_ADAPTER_ABI = [
|
|
215
|
-
{
|
|
216
|
-
name: "quoteSend",
|
|
217
|
-
type: "function",
|
|
218
|
-
inputs: [
|
|
219
|
-
{
|
|
220
|
-
name: "_sendParam",
|
|
221
|
-
type: "tuple",
|
|
222
|
-
components: [
|
|
223
|
-
{ name: "dstEid", type: "uint32" },
|
|
224
|
-
{ name: "to", type: "bytes32" },
|
|
225
|
-
{ name: "amountLD", type: "uint256" },
|
|
226
|
-
{ name: "minAmountLD", type: "uint256" },
|
|
227
|
-
{ name: "extraOptions", type: "bytes" },
|
|
228
|
-
{ name: "composeMsg", type: "bytes" },
|
|
229
|
-
{ name: "oftCmd", type: "bytes" }
|
|
230
|
-
]
|
|
231
|
-
},
|
|
232
|
-
{ name: "_payInLzToken", type: "bool" }
|
|
233
|
-
],
|
|
234
|
-
outputs: [
|
|
235
|
-
{
|
|
236
|
-
name: "msgFee",
|
|
237
|
-
type: "tuple",
|
|
238
|
-
components: [
|
|
239
|
-
{ name: "nativeFee", type: "uint256" },
|
|
240
|
-
{ name: "lzTokenFee", type: "uint256" }
|
|
241
|
-
]
|
|
242
|
-
}
|
|
243
|
-
],
|
|
244
|
-
stateMutability: "view"
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
name: "send",
|
|
248
|
-
type: "function",
|
|
249
|
-
inputs: [
|
|
250
|
-
{
|
|
251
|
-
name: "_sendParam",
|
|
252
|
-
type: "tuple",
|
|
253
|
-
components: [
|
|
254
|
-
{ name: "dstEid", type: "uint32" },
|
|
255
|
-
{ name: "to", type: "bytes32" },
|
|
256
|
-
{ name: "amountLD", type: "uint256" },
|
|
257
|
-
{ name: "minAmountLD", type: "uint256" },
|
|
258
|
-
{ name: "extraOptions", type: "bytes" },
|
|
259
|
-
{ name: "composeMsg", type: "bytes" },
|
|
260
|
-
{ name: "oftCmd", type: "bytes" }
|
|
261
|
-
]
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
name: "_fee",
|
|
265
|
-
type: "tuple",
|
|
266
|
-
components: [
|
|
267
|
-
{ name: "nativeFee", type: "uint256" },
|
|
268
|
-
{ name: "lzTokenFee", type: "uint256" }
|
|
269
|
-
]
|
|
270
|
-
},
|
|
271
|
-
{ name: "_refundAddress", type: "address" }
|
|
272
|
-
],
|
|
273
|
-
outputs: [],
|
|
274
|
-
stateMutability: "payable"
|
|
275
|
-
}
|
|
276
|
-
];
|
|
277
|
-
function buildLzOptions(gasLimit = 0n) {
|
|
278
|
-
return concat([
|
|
279
|
-
numberToHex(3, { size: 2 }),
|
|
280
|
-
// TYPE_3 header
|
|
281
|
-
numberToHex(1, { size: 1 }),
|
|
282
|
-
// Executor worker ID
|
|
283
|
-
numberToHex(17, { size: 2 }),
|
|
284
|
-
// option payload length = 17 bytes
|
|
285
|
-
numberToHex(1, { size: 1 }),
|
|
286
|
-
// LzReceive option type
|
|
287
|
-
numberToHex(gasLimit, { size: 16 })
|
|
288
|
-
// gas limit as uint128
|
|
289
|
-
]);
|
|
290
|
-
}
|
|
291
|
-
var LayerZeroService = class {
|
|
292
|
-
constructor(config) {
|
|
293
|
-
this.config = config;
|
|
294
|
-
}
|
|
295
|
-
async quoteBridge({ fromChain, toChain, amount, recipient }) {
|
|
296
|
-
const recipientAddr = recipient ?? "0x0000000000000000000000000000000000000000";
|
|
297
|
-
this.validateBridgeParams({ fromChain, toChain, amount, recipient: recipientAddr }, "quoteBridge");
|
|
298
|
-
const { src, dst } = this.resolveChains(fromChain, toChain);
|
|
299
|
-
const oftAdapter = src.oftAdapter;
|
|
300
|
-
const amountLD = parseUnits(String(amount), src.decimals);
|
|
301
|
-
try {
|
|
302
|
-
const { nativeFee, lzTokenFee } = await createPublicClient({
|
|
303
|
-
chain: makeChain(src.id, src.rpc),
|
|
304
|
-
transport: http(src.rpc)
|
|
305
|
-
}).readContract({
|
|
306
|
-
address: oftAdapter,
|
|
307
|
-
abi: OFT_ADAPTER_ABI,
|
|
308
|
-
functionName: "quoteSend",
|
|
309
|
-
args: [this.buildSendParams(dst, amountLD, recipientAddr), false]
|
|
310
|
-
});
|
|
311
|
-
return { nativeFee, lzTokenFee, toAmount: amount };
|
|
312
|
-
} catch (err) {
|
|
313
|
-
throw new StableQuoteError({
|
|
314
|
-
provider: "layerzero",
|
|
315
|
-
shortMessage: "quoteBridge: quoteSend call failed",
|
|
316
|
-
cause: err
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
async bridge({ fromChain, toChain, fromToken, toToken, amount, recipient, quote }) {
|
|
321
|
-
const from = this.config.walletClient?.account?.address ?? this.config.account?.address;
|
|
322
|
-
if (!from) {
|
|
323
|
-
throw new StableValidationError(
|
|
324
|
-
"signer",
|
|
325
|
-
void 0,
|
|
326
|
-
"bridge: requires a signer \u2014 set walletClient or account in StableConfig"
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
const recipientAddr = recipient ?? from;
|
|
330
|
-
this.validateBridgeParams({ fromChain, toChain, amount, recipient: recipientAddr }, "bridge");
|
|
331
|
-
const { src, dst } = this.resolveChains(fromChain, toChain);
|
|
332
|
-
const oftAdapter = src.oftAdapter;
|
|
333
|
-
const srcChain = makeChain(src.id, src.rpc);
|
|
334
|
-
const wc = this.config.walletClient ?? createWalletClient({
|
|
335
|
-
account: this.config.account,
|
|
336
|
-
chain: srcChain,
|
|
337
|
-
transport: this.config.transport ?? http(src.rpc)
|
|
338
|
-
});
|
|
339
|
-
const pc = createPublicClient({ chain: srcChain, transport: http(src.rpc) });
|
|
340
|
-
try {
|
|
341
|
-
await wc.switchChain({ id: src.id });
|
|
342
|
-
} catch (err) {
|
|
343
|
-
throw new StableTransactionError({
|
|
344
|
-
phase: "switch_chain",
|
|
345
|
-
shortMessage: `bridge: wallet rejected or failed to switch to chain ${src.id}`,
|
|
346
|
-
chainId: src.id,
|
|
347
|
-
cause: err
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
const amountLD = parseUnits(String(amount), src.decimals);
|
|
351
|
-
const sendParams = this.buildSendParams(dst, amountLD, recipientAddr);
|
|
352
|
-
const lzFee = quote?.nativeFee !== void 0 && quote?.lzTokenFee !== void 0 ? { nativeFee: quote.nativeFee, lzTokenFee: quote.lzTokenFee } : await this.quoteBridge({ fromChain, toChain, fromToken, toToken, amount, recipient: recipientAddr });
|
|
353
|
-
const { nativeFee = 0n, lzTokenFee = 0n } = lzFee;
|
|
354
|
-
const allowance = await pc.readContract({
|
|
355
|
-
address: src.usdt,
|
|
356
|
-
abi: erc20Abi,
|
|
357
|
-
functionName: "allowance",
|
|
358
|
-
args: [from, oftAdapter]
|
|
359
|
-
});
|
|
360
|
-
if (allowance < amountLD) {
|
|
361
|
-
let approveHash;
|
|
362
|
-
try {
|
|
363
|
-
approveHash = await wc.writeContract({
|
|
364
|
-
address: src.usdt,
|
|
365
|
-
abi: erc20Abi,
|
|
366
|
-
functionName: "approve",
|
|
367
|
-
args: [oftAdapter, amountLD],
|
|
368
|
-
account: wc.account,
|
|
369
|
-
chain: srcChain
|
|
370
|
-
});
|
|
371
|
-
} catch (err) {
|
|
372
|
-
throw new StableTransactionError({
|
|
373
|
-
phase: "approve",
|
|
374
|
-
shortMessage: "bridge: USDT0 approve failed",
|
|
375
|
-
chainId: src.id,
|
|
376
|
-
cause: err
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
const approveReceipt = await pc.waitForTransactionReceipt({ hash: approveHash });
|
|
380
|
-
if (approveReceipt.status === "reverted") {
|
|
381
|
-
throw new StableTransactionError({
|
|
382
|
-
phase: "approve",
|
|
383
|
-
shortMessage: "bridge: USDT0 approve reverted on-chain",
|
|
384
|
-
txHash: approveReceipt.transactionHash,
|
|
385
|
-
chainId: src.id
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
try {
|
|
390
|
-
const hash = await wc.writeContract({
|
|
391
|
-
address: oftAdapter,
|
|
392
|
-
abi: OFT_ADAPTER_ABI,
|
|
393
|
-
functionName: "send",
|
|
394
|
-
args: [sendParams, { nativeFee, lzTokenFee }, from],
|
|
395
|
-
value: nativeFee,
|
|
396
|
-
account: wc.account,
|
|
397
|
-
chain: srcChain
|
|
398
|
-
});
|
|
399
|
-
return { txHash: hash, toAmount: amount };
|
|
400
|
-
} catch (err) {
|
|
401
|
-
throw new StableTransactionError({
|
|
402
|
-
phase: "send",
|
|
403
|
-
shortMessage: "bridge: OFT send failed",
|
|
404
|
-
chainId: src.id,
|
|
405
|
-
cause: err
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
validateBridgeParams(params, method) {
|
|
410
|
-
if (!isAddress(params.recipient)) {
|
|
411
|
-
throw new StableValidationError(
|
|
412
|
-
"recipient",
|
|
413
|
-
params.recipient,
|
|
414
|
-
`${method}: invalid recipient address "${params.recipient}"`
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
if (!Number.isFinite(params.amount) || params.amount <= 0) {
|
|
418
|
-
throw new StableValidationError(
|
|
419
|
-
"amount",
|
|
420
|
-
params.amount,
|
|
421
|
-
`${method}: amount must be a finite positive number`
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
if (params.fromChain === params.toChain) {
|
|
425
|
-
throw new StableValidationError(
|
|
426
|
-
"toChain",
|
|
427
|
-
params.toChain,
|
|
428
|
-
`${method}: fromChain and toChain must differ`
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
resolveChains(fromChain, toChain) {
|
|
433
|
-
const src = BRIDGE_CHAIN_CONFIGS[fromChain];
|
|
434
|
-
const dst = BRIDGE_CHAIN_CONFIGS[toChain];
|
|
435
|
-
if (!src) throw new StableValidationError("fromChain", fromChain, `Unsupported source chain: "${fromChain}"`);
|
|
436
|
-
if (!dst) throw new StableValidationError("toChain", toChain, `Unsupported destination chain: "${toChain}"`);
|
|
437
|
-
if (!src.oftAdapter) {
|
|
438
|
-
throw new StableValidationError("fromChain", fromChain, `No OFT adapter configured for "${fromChain}"`);
|
|
439
|
-
}
|
|
440
|
-
return { src, dst };
|
|
441
|
-
}
|
|
442
|
-
buildSendParams(dst, amountLD, recipient) {
|
|
443
|
-
return {
|
|
444
|
-
dstEid: dst.lzEid,
|
|
445
|
-
to: padHex(recipient, { dir: "left", size: 32 }),
|
|
446
|
-
amountLD,
|
|
447
|
-
minAmountLD: amountLD,
|
|
448
|
-
extraOptions: buildLzOptions(),
|
|
449
|
-
composeMsg: "0x",
|
|
450
|
-
oftCmd: "0x"
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
// src/services/lifi.ts
|
|
456
|
-
import {
|
|
457
|
-
createPublicClient as createPublicClient2,
|
|
458
|
-
createWalletClient as createWalletClient2,
|
|
459
|
-
erc20Abi as erc20Abi2,
|
|
460
|
-
formatUnits,
|
|
461
|
-
http as http2,
|
|
462
|
-
isAddress as isAddress2,
|
|
463
|
-
parseUnits as parseUnits2
|
|
464
|
-
} from "viem";
|
|
465
|
-
|
|
466
|
-
// src/helpers/retry.ts
|
|
467
|
-
var HttpError = class extends Error {
|
|
468
|
-
constructor(message, status, retryable) {
|
|
469
|
-
super(message);
|
|
470
|
-
this.status = status;
|
|
471
|
-
this.retryable = retryable;
|
|
472
|
-
this.name = "HttpError";
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
function isRetryableError(err) {
|
|
476
|
-
if (err instanceof HttpError) return err.retryable;
|
|
477
|
-
return err instanceof TypeError;
|
|
478
|
-
}
|
|
479
|
-
async function retryWithBackoff(fn, isRetryable = isRetryableError, opts = {}) {
|
|
480
|
-
const maxAttempts = opts.maxAttempts ?? 3;
|
|
481
|
-
const base = opts.baseDelayMs ?? 250;
|
|
482
|
-
const max = opts.maxDelayMs ?? 4e3;
|
|
483
|
-
let lastErr;
|
|
484
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
485
|
-
try {
|
|
486
|
-
return await fn();
|
|
487
|
-
} catch (err) {
|
|
488
|
-
lastErr = err;
|
|
489
|
-
if (attempt === maxAttempts - 1 || !isRetryable(err)) throw err;
|
|
490
|
-
const delay = Math.min(max, base * 2 ** attempt);
|
|
491
|
-
const jittered = delay * (0.75 + Math.random() * 0.5);
|
|
492
|
-
await new Promise((r) => setTimeout(r, jittered));
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
throw lastErr;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
388
|
// src/services/lifi.ts
|
|
499
389
|
var LiFiService = class {
|
|
500
390
|
constructor(config) {
|
|
@@ -663,7 +553,7 @@ var LiFiService = class {
|
|
|
663
553
|
}
|
|
664
554
|
_toSwapQuote(quote) {
|
|
665
555
|
return {
|
|
666
|
-
toAmount: Number(
|
|
556
|
+
toAmount: Number(formatUnits2(BigInt(quote.estimate.toAmount), quote.action.toToken.decimals)),
|
|
667
557
|
fromAmount: BigInt(quote.estimate.fromAmount),
|
|
668
558
|
fromToken: quote.action.fromToken.address,
|
|
669
559
|
approvalAddress: quote.estimate.approvalAddress,
|
|
@@ -683,7 +573,7 @@ var LiFiService = class {
|
|
|
683
573
|
});
|
|
684
574
|
}
|
|
685
575
|
const srcChain = makeChain(srcChainId, srcRpc);
|
|
686
|
-
const wc = this.config.walletClient ??
|
|
576
|
+
const wc = this.config.walletClient ?? createWalletClient({
|
|
687
577
|
account: this.config.account,
|
|
688
578
|
chain: srcChain,
|
|
689
579
|
transport: this.config.transport ?? http2(srcRpc)
|
|
@@ -771,12 +661,335 @@ function isLiFiQuoteShape(v) {
|
|
|
771
661
|
return isObject(tx) && typeof tx.to === "string" && typeof tx.data === "string" && typeof tx.chainId === "number" && isObject(est) && typeof est.fromAmount === "string" && typeof est.toAmount === "string" && isObject(act) && isObject(act.fromToken) && isObject(act.toToken);
|
|
772
662
|
}
|
|
773
663
|
|
|
664
|
+
// src/earn/morpho.ts
|
|
665
|
+
import {
|
|
666
|
+
createPublicClient as createPublicClient3,
|
|
667
|
+
createWalletClient as createWalletClient2,
|
|
668
|
+
custom,
|
|
669
|
+
erc20Abi as erc20Abi3,
|
|
670
|
+
http as http3,
|
|
671
|
+
parseUnits as parseUnits3
|
|
672
|
+
} from "viem";
|
|
673
|
+
import {
|
|
674
|
+
MorphoClient as MorphoClient2,
|
|
675
|
+
isRequirementApproval,
|
|
676
|
+
isRequirementSignature
|
|
677
|
+
} from "@morpho-org/morpho-sdk";
|
|
678
|
+
var MorphoService = class {
|
|
679
|
+
constructor(config) {
|
|
680
|
+
this.config = config;
|
|
681
|
+
this.inFlight = /* @__PURE__ */ new Map();
|
|
682
|
+
this.chain = makeChain(config.chainId, config.rpc);
|
|
683
|
+
this.wc = config.walletClient ?? createWalletClient2({
|
|
684
|
+
account: config.account,
|
|
685
|
+
chain: this.chain,
|
|
686
|
+
transport: config.transport ?? http3(config.rpc)
|
|
687
|
+
});
|
|
688
|
+
const morphoWc = this.wc.chain?.id === config.chainId ? this.wc : createWalletClient2({
|
|
689
|
+
account: this.wc.account,
|
|
690
|
+
chain: this.chain,
|
|
691
|
+
transport: custom({ request: this.wc.request.bind(this.wc) })
|
|
692
|
+
});
|
|
693
|
+
this.morpho = new MorphoClient2(morphoWc, { supportSignature: true });
|
|
694
|
+
this.pc = createPublicClient3({ chain: this.chain, transport: http3(config.rpc) });
|
|
695
|
+
}
|
|
696
|
+
deposit(params) {
|
|
697
|
+
this._validateAmount(params.amount, "earn deposit");
|
|
698
|
+
return this._withIdempotency(params.idempotencyKey, () => this._deposit(params));
|
|
699
|
+
}
|
|
700
|
+
async bulkDeposit(items, options = {}) {
|
|
701
|
+
const { idempotencyKey, stopOnError = false } = options;
|
|
702
|
+
const run = async () => {
|
|
703
|
+
const results = [];
|
|
704
|
+
for (const item of items) {
|
|
705
|
+
try {
|
|
706
|
+
const result = await this.deposit(item);
|
|
707
|
+
results.push({ status: "fulfilled", txHash: result.txHash, vault: item.vault, idempotencyKey: item.idempotencyKey });
|
|
708
|
+
} catch (err) {
|
|
709
|
+
const entry = { status: "rejected", error: String(err), vault: item.vault, idempotencyKey: item.idempotencyKey };
|
|
710
|
+
results.push(entry);
|
|
711
|
+
if (stopOnError) {
|
|
712
|
+
const succeeded2 = results.filter((r) => r.status === "fulfilled").length;
|
|
713
|
+
throw new StableBulkError({
|
|
714
|
+
shortMessage: "bulkDeposit halted on first failure",
|
|
715
|
+
results,
|
|
716
|
+
succeeded: succeeded2,
|
|
717
|
+
failed: results.length - succeeded2
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const succeeded = results.filter((r) => r.status === "fulfilled").length;
|
|
723
|
+
return { results, succeeded, failed: results.length - succeeded, idempotencyKey };
|
|
724
|
+
};
|
|
725
|
+
return this._withIdempotency(idempotencyKey, run);
|
|
726
|
+
}
|
|
727
|
+
withdraw(params) {
|
|
728
|
+
this._validateAmount(params.amount, "earn withdraw");
|
|
729
|
+
return this._withIdempotency(params.idempotencyKey, () => this._withdraw(params));
|
|
730
|
+
}
|
|
731
|
+
redeem(params) {
|
|
732
|
+
this._validateShares(params.shares, "earn redeem");
|
|
733
|
+
return this._withIdempotency(params.idempotencyKey, () => this._redeem(params));
|
|
734
|
+
}
|
|
735
|
+
forceWithdraw(params) {
|
|
736
|
+
this._validateAmount(params.amount, "earn forceWithdraw");
|
|
737
|
+
return this._withIdempotency(params.idempotencyKey, () => this._forceWithdraw(params));
|
|
738
|
+
}
|
|
739
|
+
forceRedeem(params) {
|
|
740
|
+
this._validateShares(params.shares, "earn forceRedeem");
|
|
741
|
+
return this._withIdempotency(params.idempotencyKey, () => this._forceRedeem(params));
|
|
742
|
+
}
|
|
743
|
+
// ─── Private operations ──────────────────────────────────────────────────
|
|
744
|
+
async _deposit(params) {
|
|
745
|
+
const { amount, tokenDecimals, slippageTolerance, idempotencyKey } = params;
|
|
746
|
+
const from = this._requireFrom();
|
|
747
|
+
const vaultAddr = params.vault ?? this.config.vault;
|
|
748
|
+
await this._switchChain();
|
|
749
|
+
const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
|
|
750
|
+
const vaultData = await vault.getData();
|
|
751
|
+
const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn deposit");
|
|
752
|
+
const assetsRaw = parseUnits3(String(amount), decimals);
|
|
753
|
+
const depositAction = vault.deposit({
|
|
754
|
+
amount: assetsRaw,
|
|
755
|
+
userAddress: from,
|
|
756
|
+
accrualVault: vaultData,
|
|
757
|
+
slippageTolerance: slippageTolerance !== void 0 ? parseUnits3(String(slippageTolerance), 18) : void 0
|
|
758
|
+
});
|
|
759
|
+
const requirements = await depositAction.getRequirements();
|
|
760
|
+
let sig;
|
|
761
|
+
for (const req of requirements) {
|
|
762
|
+
if (isRequirementApproval(req)) {
|
|
763
|
+
const approveHash = await this._sendTx(req.to, req.data, req.value, from);
|
|
764
|
+
const approveReceipt = await this.pc.waitForTransactionReceipt({ hash: approveHash });
|
|
765
|
+
if (approveReceipt.status === "reverted") {
|
|
766
|
+
throw new StableTransactionError({
|
|
767
|
+
phase: "approve",
|
|
768
|
+
shortMessage: "earn deposit: token approval reverted on-chain",
|
|
769
|
+
txHash: approveReceipt.transactionHash,
|
|
770
|
+
chainId: this.config.chainId
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
} else if (isRequirementSignature(req)) {
|
|
774
|
+
sig = await req.sign(this.wc, from);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const tx = depositAction.buildTx(sig);
|
|
778
|
+
const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
|
|
779
|
+
const receipt = await this.pc.waitForTransactionReceipt({ hash });
|
|
780
|
+
if (receipt.status === "reverted") {
|
|
781
|
+
throw new StableTransactionError({
|
|
782
|
+
phase: "receipt",
|
|
783
|
+
shortMessage: "earn deposit: vault deposit reverted on-chain",
|
|
784
|
+
txHash: receipt.transactionHash,
|
|
785
|
+
chainId: this.config.chainId
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
return { txHash: receipt.transactionHash, idempotencyKey };
|
|
789
|
+
}
|
|
790
|
+
async _withdraw(params) {
|
|
791
|
+
const { amount, tokenDecimals, idempotencyKey } = params;
|
|
792
|
+
const from = this._requireFrom();
|
|
793
|
+
const vaultAddr = this.config.vault;
|
|
794
|
+
await this._switchChain();
|
|
795
|
+
const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
|
|
796
|
+
const vaultData = await vault.getData();
|
|
797
|
+
const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn withdraw");
|
|
798
|
+
const assetsRaw = parseUnits3(String(amount), decimals);
|
|
799
|
+
const tx = vault.withdraw({ amount: assetsRaw, userAddress: from }).buildTx();
|
|
800
|
+
const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
|
|
801
|
+
const receipt = await this.pc.waitForTransactionReceipt({ hash });
|
|
802
|
+
if (receipt.status === "reverted") {
|
|
803
|
+
throw new StableTransactionError({
|
|
804
|
+
phase: "receipt",
|
|
805
|
+
shortMessage: "earn withdraw: vault withdraw reverted on-chain",
|
|
806
|
+
txHash: receipt.transactionHash,
|
|
807
|
+
chainId: this.config.chainId
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
return { txHash: receipt.transactionHash, idempotencyKey };
|
|
811
|
+
}
|
|
812
|
+
async _redeem(params) {
|
|
813
|
+
const { shares, shareDecimals, idempotencyKey } = params;
|
|
814
|
+
const from = this._requireFrom();
|
|
815
|
+
const vaultAddr = this.config.vault;
|
|
816
|
+
await this._switchChain();
|
|
817
|
+
let decimals = shareDecimals;
|
|
818
|
+
if (decimals === void 0) {
|
|
819
|
+
try {
|
|
820
|
+
decimals = await this.pc.readContract({ address: vaultAddr, abi: erc20Abi3, functionName: "decimals" });
|
|
821
|
+
} catch {
|
|
822
|
+
throw new StableValidationError(
|
|
823
|
+
"shareDecimals",
|
|
824
|
+
void 0,
|
|
825
|
+
`earn redeem: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const sharesRaw = parseUnits3(String(shares), decimals);
|
|
830
|
+
const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
|
|
831
|
+
const tx = vault.redeem({ shares: sharesRaw, userAddress: from }).buildTx();
|
|
832
|
+
const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
|
|
833
|
+
const receipt = await this.pc.waitForTransactionReceipt({ hash });
|
|
834
|
+
if (receipt.status === "reverted") {
|
|
835
|
+
throw new StableTransactionError({
|
|
836
|
+
phase: "receipt",
|
|
837
|
+
shortMessage: "earn redeem: vault redeem reverted on-chain",
|
|
838
|
+
txHash: receipt.transactionHash,
|
|
839
|
+
chainId: this.config.chainId
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
return { txHash: receipt.transactionHash, idempotencyKey };
|
|
843
|
+
}
|
|
844
|
+
async _forceWithdraw(params) {
|
|
845
|
+
const { amount, tokenDecimals, deallocations, idempotencyKey } = params;
|
|
846
|
+
const from = this._requireFrom();
|
|
847
|
+
const vaultAddr = this.config.vault;
|
|
848
|
+
await this._switchChain();
|
|
849
|
+
const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
|
|
850
|
+
let decimals = tokenDecimals;
|
|
851
|
+
if (decimals === void 0) {
|
|
852
|
+
const vaultData = await vault.getData();
|
|
853
|
+
decimals = await this._resolveDecimals(vaultData.asset, "earn forceWithdraw");
|
|
854
|
+
}
|
|
855
|
+
const tx = vault.forceWithdraw({
|
|
856
|
+
deallocations: this._buildDeallocs(deallocations, decimals),
|
|
857
|
+
withdraw: { amount: parseUnits3(String(amount), decimals) },
|
|
858
|
+
userAddress: from
|
|
859
|
+
}).buildTx();
|
|
860
|
+
const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
|
|
861
|
+
const receipt = await this.pc.waitForTransactionReceipt({ hash });
|
|
862
|
+
if (receipt.status === "reverted") {
|
|
863
|
+
throw new StableTransactionError({
|
|
864
|
+
phase: "receipt",
|
|
865
|
+
shortMessage: "earn forceWithdraw: vault forceWithdraw reverted on-chain",
|
|
866
|
+
txHash: receipt.transactionHash,
|
|
867
|
+
chainId: this.config.chainId
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
return { txHash: receipt.transactionHash, idempotencyKey };
|
|
871
|
+
}
|
|
872
|
+
async _forceRedeem(params) {
|
|
873
|
+
const { shares, shareDecimals, tokenDecimals, deallocations, idempotencyKey } = params;
|
|
874
|
+
const from = this._requireFrom();
|
|
875
|
+
const vaultAddr = this.config.vault;
|
|
876
|
+
await this._switchChain();
|
|
877
|
+
const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
|
|
878
|
+
let shareDec = shareDecimals;
|
|
879
|
+
if (shareDec === void 0) {
|
|
880
|
+
try {
|
|
881
|
+
shareDec = await this.pc.readContract({ address: vaultAddr, abi: erc20Abi3, functionName: "decimals" });
|
|
882
|
+
} catch {
|
|
883
|
+
throw new StableValidationError(
|
|
884
|
+
"shareDecimals",
|
|
885
|
+
void 0,
|
|
886
|
+
`earn forceRedeem: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
let assetDec = tokenDecimals;
|
|
891
|
+
if (assetDec === void 0) {
|
|
892
|
+
const vaultData = await vault.getData();
|
|
893
|
+
assetDec = await this._resolveDecimals(vaultData.asset, "earn forceRedeem");
|
|
894
|
+
}
|
|
895
|
+
const tx = vault.forceRedeem({
|
|
896
|
+
deallocations: this._buildDeallocs(deallocations, assetDec),
|
|
897
|
+
redeem: { shares: parseUnits3(String(shares), shareDec) },
|
|
898
|
+
userAddress: from
|
|
899
|
+
}).buildTx();
|
|
900
|
+
const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
|
|
901
|
+
const receipt = await this.pc.waitForTransactionReceipt({ hash });
|
|
902
|
+
if (receipt.status === "reverted") {
|
|
903
|
+
throw new StableTransactionError({
|
|
904
|
+
phase: "receipt",
|
|
905
|
+
shortMessage: "earn forceRedeem: vault forceRedeem reverted on-chain",
|
|
906
|
+
txHash: receipt.transactionHash,
|
|
907
|
+
chainId: this.config.chainId
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
return { txHash: receipt.transactionHash, idempotencyKey };
|
|
911
|
+
}
|
|
912
|
+
_buildDeallocs(deallocations, assetDecimals) {
|
|
913
|
+
return deallocations.map((d) => ({
|
|
914
|
+
adapter: d.adapter,
|
|
915
|
+
amount: parseUnits3(String(d.amount), assetDecimals),
|
|
916
|
+
// MarketParams in the Morpho SDK has computed fields (id, liquidationIncentiveFactor)
|
|
917
|
+
// not present in our user-facing type; the plain object is structurally compatible for ABI encoding.
|
|
918
|
+
...d.marketParams && { marketParams: d.marketParams }
|
|
919
|
+
}));
|
|
920
|
+
}
|
|
921
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
922
|
+
_requireFrom() {
|
|
923
|
+
const from = this.wc.account?.address;
|
|
924
|
+
if (!from) {
|
|
925
|
+
throw new StableValidationError(
|
|
926
|
+
"signer",
|
|
927
|
+
void 0,
|
|
928
|
+
"earn: requires a signer \u2014 set walletClient or account in StableConfig"
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
return from;
|
|
932
|
+
}
|
|
933
|
+
async _switchChain() {
|
|
934
|
+
try {
|
|
935
|
+
await this.wc.switchChain({ id: this.config.chainId });
|
|
936
|
+
} catch (err) {
|
|
937
|
+
if (err.code === 4001) {
|
|
938
|
+
throw new StableTransactionError({
|
|
939
|
+
phase: "switch_chain",
|
|
940
|
+
shortMessage: `earn: wallet rejected switch to chain ${this.config.chainId}`,
|
|
941
|
+
chainId: this.config.chainId,
|
|
942
|
+
cause: err
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
async _sendTx(to, data, value, from) {
|
|
948
|
+
try {
|
|
949
|
+
return await this.wc.sendTransaction({ to, data, value, account: from, chain: this.chain });
|
|
950
|
+
} catch (err) {
|
|
951
|
+
throw new StableTransactionError({
|
|
952
|
+
phase: "send",
|
|
953
|
+
shortMessage: "earn: transaction send failed",
|
|
954
|
+
chainId: this.config.chainId,
|
|
955
|
+
cause: err
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
async _resolveDecimals(asset, op) {
|
|
960
|
+
try {
|
|
961
|
+
return await retryWithBackoff(
|
|
962
|
+
() => this.pc.readContract({ address: asset, abi: erc20Abi3, functionName: "decimals" })
|
|
963
|
+
);
|
|
964
|
+
} catch {
|
|
965
|
+
throw new StableValidationError(
|
|
966
|
+
"tokenDecimals",
|
|
967
|
+
void 0,
|
|
968
|
+
`${op}: could not fetch decimals for asset "${asset}" \u2014 pass tokenDecimals explicitly`
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
_withIdempotency(key, fn) {
|
|
973
|
+
if (!key) return fn();
|
|
974
|
+
const existing = this.inFlight.get(key);
|
|
975
|
+
if (existing) return existing;
|
|
976
|
+
const promise = fn().finally(() => this.inFlight.delete(key));
|
|
977
|
+
this.inFlight.set(key, promise);
|
|
978
|
+
return promise;
|
|
979
|
+
}
|
|
980
|
+
_validateAmount(amount, op) {
|
|
981
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
982
|
+
throw new StableValidationError("amount", amount, `${op}: amount must be a finite positive number`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
_validateShares(shares, op) {
|
|
986
|
+
if (!Number.isFinite(shares) || shares <= 0) {
|
|
987
|
+
throw new StableValidationError("shares", shares, `${op}: shares must be a finite positive number`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
|
|
774
992
|
// src/stable.ts
|
|
775
|
-
function isUsdt0Route(fromChain, toChain, fromToken, toToken) {
|
|
776
|
-
const src = BRIDGE_CHAIN_CONFIGS[fromChain];
|
|
777
|
-
const dst = BRIDGE_CHAIN_CONFIGS[toChain];
|
|
778
|
-
return !!src && !!dst && src.lzOutbound !== false && src.usdt.toLowerCase() === fromToken.toLowerCase() && dst.usdt.toLowerCase() === toToken.toLowerCase();
|
|
779
|
-
}
|
|
780
993
|
function validateTransferParams({ to, amount, token, tokenDecimals }) {
|
|
781
994
|
if (!isAddress3(to)) {
|
|
782
995
|
throw new StableValidationError("to", to, `transfer: invalid recipient address "${to}"`);
|
|
@@ -794,7 +1007,7 @@ function validateTransferParams({ to, amount, token, tokenDecimals }) {
|
|
|
794
1007
|
async function resolveTokenDecimals(publicClient, token, tokenDecimals) {
|
|
795
1008
|
if (tokenDecimals !== void 0) return tokenDecimals;
|
|
796
1009
|
try {
|
|
797
|
-
return await publicClient.readContract({ address: token, abi:
|
|
1010
|
+
return await publicClient.readContract({ address: token, abi: erc20Abi4, functionName: "decimals" });
|
|
798
1011
|
} catch {
|
|
799
1012
|
throw new StableValidationError(
|
|
800
1013
|
"tokenDecimals",
|
|
@@ -815,9 +1028,9 @@ async function executeTokenTransfer(wc, publicClient, token, to, amount, tokenDe
|
|
|
815
1028
|
const decimals = await resolveTokenDecimals(publicClient, token, tokenDecimals);
|
|
816
1029
|
return wc.writeContract({
|
|
817
1030
|
address: token,
|
|
818
|
-
abi:
|
|
1031
|
+
abi: erc20Abi4,
|
|
819
1032
|
functionName: "transfer",
|
|
820
|
-
args: [to,
|
|
1033
|
+
args: [to, parseUnits4(String(amount), decimals)],
|
|
821
1034
|
account: acc,
|
|
822
1035
|
chain
|
|
823
1036
|
});
|
|
@@ -827,13 +1040,24 @@ function createStable({
|
|
|
827
1040
|
rpc: rpcOverride,
|
|
828
1041
|
account,
|
|
829
1042
|
transport,
|
|
830
|
-
walletClient
|
|
1043
|
+
walletClient,
|
|
1044
|
+
earn: earnConfig
|
|
831
1045
|
}) {
|
|
832
|
-
const chain = network === "mainnet" /* Mainnet */ ?
|
|
1046
|
+
const chain = network === "mainnet" /* Mainnet */ ? stableMainnet2 : stableTestnet2;
|
|
833
1047
|
const rpc = (rpcOverride ?? chain.rpcUrls.default.http[0]).replace(/\/$/, "");
|
|
834
1048
|
const signerConfig = { account, transport, walletClient };
|
|
835
|
-
const lz = new LayerZeroService(signerConfig);
|
|
836
1049
|
const lifi = new LiFiService({ ...signerConfig, rpc, chainId: chain.id });
|
|
1050
|
+
const morpho = earnConfig ? new MorphoService({ ...signerConfig, rpc, chainId: chain.id, vault: earnConfig.vault }) : null;
|
|
1051
|
+
function requireEarn() {
|
|
1052
|
+
if (!morpho) {
|
|
1053
|
+
throw new StableValidationError(
|
|
1054
|
+
"earn",
|
|
1055
|
+
void 0,
|
|
1056
|
+
"stable.earn.*: pass earn: { vault: '0x...' } to createStable() to enable earn operations"
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
return morpho;
|
|
1060
|
+
}
|
|
837
1061
|
async function transfer(params) {
|
|
838
1062
|
validateTransferParams(params);
|
|
839
1063
|
const { from, to, amount, token, tokenDecimals } = params;
|
|
@@ -841,7 +1065,7 @@ function createStable({
|
|
|
841
1065
|
const wc = walletClient ?? createWalletClient3({
|
|
842
1066
|
account: acc,
|
|
843
1067
|
chain,
|
|
844
|
-
transport: transport ??
|
|
1068
|
+
transport: transport ?? http4(rpc)
|
|
845
1069
|
});
|
|
846
1070
|
try {
|
|
847
1071
|
await wc.switchChain({ id: chain.id });
|
|
@@ -853,8 +1077,8 @@ function createStable({
|
|
|
853
1077
|
cause: err
|
|
854
1078
|
});
|
|
855
1079
|
}
|
|
856
|
-
const publicClient =
|
|
857
|
-
const hash = token !== void 0 ? await executeTokenTransfer(wc, publicClient, token, to, amount, tokenDecimals, acc, chain) : await wc.sendTransaction({ account: acc, chain, to, value:
|
|
1080
|
+
const publicClient = createPublicClient4({ chain, transport: http4(rpc) });
|
|
1081
|
+
const hash = token !== void 0 ? await executeTokenTransfer(wc, publicClient, token, to, amount, tokenDecimals, acc, chain) : await wc.sendTransaction({ account: acc, chain, to, value: parseUnits4(String(amount), 18) });
|
|
858
1082
|
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
859
1083
|
if (receipt.status === "reverted") {
|
|
860
1084
|
throw new StableTransactionError({
|
|
@@ -866,28 +1090,48 @@ function createStable({
|
|
|
866
1090
|
}
|
|
867
1091
|
return { txHash: receipt.transactionHash };
|
|
868
1092
|
}
|
|
869
|
-
function quoteBridge(params) {
|
|
870
|
-
return isUsdt0Route(params.fromChain, params.toChain, params.fromToken, params.toToken) ? lz.quoteBridge(params) : lifi.quoteBridge(params);
|
|
871
|
-
}
|
|
872
|
-
function bridge(params) {
|
|
873
|
-
return isUsdt0Route(params.fromChain, params.toChain, params.fromToken, params.toToken) ? lz.bridge(params) : lifi.bridge(params);
|
|
874
|
-
}
|
|
875
1093
|
return {
|
|
876
1094
|
transfer,
|
|
877
|
-
quoteBridge,
|
|
878
|
-
bridge,
|
|
1095
|
+
quoteBridge: (params) => lifi.quoteBridge(params),
|
|
1096
|
+
bridge: (params) => lifi.bridge(params),
|
|
879
1097
|
quoteSwap: (params) => lifi.quoteSwap(params),
|
|
880
|
-
swap: (params) => lifi.swap(params)
|
|
1098
|
+
swap: (params) => lifi.swap(params),
|
|
1099
|
+
earn: {
|
|
1100
|
+
deposit: (params) => requireEarn().deposit(params),
|
|
1101
|
+
bulkDeposit: (items, options) => requireEarn().bulkDeposit(items, options),
|
|
1102
|
+
withdraw: (params) => requireEarn().withdraw(params),
|
|
1103
|
+
redeem: (params) => requireEarn().redeem(params),
|
|
1104
|
+
forceWithdraw: (params) => requireEarn().forceWithdraw(params),
|
|
1105
|
+
forceRedeem: (params) => requireEarn().forceRedeem(params)
|
|
1106
|
+
}
|
|
881
1107
|
};
|
|
882
1108
|
}
|
|
1109
|
+
function createStableReader(config) {
|
|
1110
|
+
const reader = new EarnReader(config);
|
|
1111
|
+
return {
|
|
1112
|
+
earn: {
|
|
1113
|
+
position: () => reader.position(),
|
|
1114
|
+
preview: (params) => reader.preview(params),
|
|
1115
|
+
withdrawability: (params) => reader.withdrawability(params)
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/constants.ts
|
|
1121
|
+
var STABLE_USDT_ADDRESS = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
|
|
1122
|
+
var STABLE_VAULT_ADDRESS = "0x461eF73F640456138242398a39AB48a557A49293";
|
|
883
1123
|
export {
|
|
884
1124
|
CHAIN_CONFIGS,
|
|
885
1125
|
Chain,
|
|
886
1126
|
Network,
|
|
1127
|
+
STABLE_USDT_ADDRESS,
|
|
1128
|
+
STABLE_VAULT_ADDRESS,
|
|
1129
|
+
StableBulkError,
|
|
887
1130
|
StableError,
|
|
888
1131
|
StableNetworkError,
|
|
889
1132
|
StableQuoteError,
|
|
890
1133
|
StableTransactionError,
|
|
891
1134
|
StableValidationError,
|
|
892
|
-
createStable
|
|
1135
|
+
createStable,
|
|
1136
|
+
createStableReader
|
|
893
1137
|
};
|