@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.cjs
CHANGED
|
@@ -9,6 +9,18 @@
|
|
|
9
9
|
var _viem = require('viem');
|
|
10
10
|
var _chains = require('viem/chains');
|
|
11
11
|
|
|
12
|
+
// src/earn/reader.ts
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
var _morphosdk = require('@morpho-org/morpho-sdk');
|
|
23
|
+
|
|
12
24
|
// src/errors.ts
|
|
13
25
|
|
|
14
26
|
var StableError = class extends _viem.BaseError {
|
|
@@ -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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
102
|
// src/helpers/chain.ts
|
|
96
103
|
|
|
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 = _nullishCoalesce(opts.maxAttempts, () => ( 3));
|
|
128
|
+
const base = _nullishCoalesce(opts.baseDelayMs, () => ( 250));
|
|
129
|
+
const max = _nullishCoalesce(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 (!_optionalChain([config, 'access', _2 => _2.earn, 'optionalAccess', _3 => _3.vault])) {
|
|
171
|
+
throw new StableValidationError(
|
|
172
|
+
"earn.vault",
|
|
173
|
+
void 0,
|
|
174
|
+
"createStableReader: earn.vault is required"
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (!_viem.isAddress.call(void 0, config.address)) {
|
|
178
|
+
throw new StableValidationError(
|
|
179
|
+
"address",
|
|
180
|
+
config.address,
|
|
181
|
+
`createStableReader: invalid user address "${config.address}"`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (!_viem.isAddress.call(void 0, 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 = _nullishCoalesce(config.network, () => ( "mainnet")) /* Mainnet */;
|
|
192
|
+
const viemChain = network === "mainnet" /* Mainnet */ ? _chains.stable : _chains.stableTestnet;
|
|
193
|
+
const rpc = (_nullishCoalesce(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 = _viem.createPublicClient.call(void 0, { chain, transport: _viem.http.call(void 0, rpc) });
|
|
199
|
+
this.morpho = new (0, _morphosdk.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: _viem.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: _viem.erc20Abi, functionName: "decimals" }),
|
|
214
|
+
this.pc.readContract({ address: this.vaultAddr, abi: _viem.erc20Abi, functionName: "decimals" })
|
|
215
|
+
]);
|
|
216
|
+
const assetsRaw = vaultData.toAssets(shares);
|
|
217
|
+
return {
|
|
218
|
+
shares,
|
|
219
|
+
sharesFormatted: Number(_viem.formatUnits.call(void 0, shares, shareDecimals)),
|
|
220
|
+
shareDecimals,
|
|
221
|
+
assets: Number(_viem.formatUnits.call(void 0, 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 = await _asyncNullishCoalesce(params.tokenDecimals, async () => ( await this.pc.readContract({
|
|
239
|
+
address: vaultData.asset,
|
|
240
|
+
abi: _viem.erc20Abi,
|
|
241
|
+
functionName: "decimals"
|
|
242
|
+
})));
|
|
243
|
+
const requestedRaw = _viem.parseUnits.call(void 0, String(params.amount), tokenDecimals);
|
|
244
|
+
const availableLiquidityRaw = vaultData.assetBalance;
|
|
245
|
+
return {
|
|
246
|
+
isInstant: requestedRaw <= availableLiquidityRaw,
|
|
247
|
+
availableLiquidity: Number(_viem.formatUnits.call(void 0, 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 = _optionalChain([body, 'optionalAccess', _4 => _4.data, 'optionalAccess', _5 => _5.vaultV2ByAddress, 'optionalAccess', _6 => _6.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
|
+
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
|
|
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 _viem.concat.call(void 0, [
|
|
279
|
-
_viem.numberToHex.call(void 0, 3, { size: 2 }),
|
|
280
|
-
// TYPE_3 header
|
|
281
|
-
_viem.numberToHex.call(void 0, 1, { size: 1 }),
|
|
282
|
-
// Executor worker ID
|
|
283
|
-
_viem.numberToHex.call(void 0, 17, { size: 2 }),
|
|
284
|
-
// option payload length = 17 bytes
|
|
285
|
-
_viem.numberToHex.call(void 0, 1, { size: 1 }),
|
|
286
|
-
// LzReceive option type
|
|
287
|
-
_viem.numberToHex.call(void 0, 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 = _nullishCoalesce(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 = _viem.parseUnits.call(void 0, String(amount), src.decimals);
|
|
301
|
-
try {
|
|
302
|
-
const { nativeFee, lzTokenFee } = await _viem.createPublicClient.call(void 0, {
|
|
303
|
-
chain: makeChain(src.id, src.rpc),
|
|
304
|
-
transport: _viem.http.call(void 0, 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 = _nullishCoalesce(_optionalChain([this, 'access', _2 => _2.config, 'access', _3 => _3.walletClient, 'optionalAccess', _4 => _4.account, 'optionalAccess', _5 => _5.address]), () => ( _optionalChain([this, 'access', _6 => _6.config, 'access', _7 => _7.account, 'optionalAccess', _8 => _8.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 = _nullishCoalesce(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 = _nullishCoalesce(this.config.walletClient, () => ( _viem.createWalletClient.call(void 0, {
|
|
335
|
-
account: this.config.account,
|
|
336
|
-
chain: srcChain,
|
|
337
|
-
transport: _nullishCoalesce(this.config.transport, () => ( _viem.http.call(void 0, src.rpc)))
|
|
338
|
-
})));
|
|
339
|
-
const pc = _viem.createPublicClient.call(void 0, { chain: srcChain, transport: _viem.http.call(void 0, 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 = _viem.parseUnits.call(void 0, String(amount), src.decimals);
|
|
351
|
-
const sendParams = this.buildSendParams(dst, amountLD, recipientAddr);
|
|
352
|
-
const lzFee = _optionalChain([quote, 'optionalAccess', _9 => _9.nativeFee]) !== void 0 && _optionalChain([quote, 'optionalAccess', _10 => _10.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: _viem.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: _viem.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 (!_viem.isAddress.call(void 0, 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: _viem.padHex.call(void 0, 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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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 = _nullishCoalesce(opts.maxAttempts, () => ( 3));
|
|
481
|
-
const base = _nullishCoalesce(opts.baseDelayMs, () => ( 250));
|
|
482
|
-
const max = _nullishCoalesce(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) {
|
|
@@ -526,7 +416,7 @@ var LiFiService = class {
|
|
|
526
416
|
async quoteBridge(params) {
|
|
527
417
|
this._validateBridgeParams(params);
|
|
528
418
|
const { fromChain, toChain, fromToken, toToken, amount, fromDecimals = 6, recipient } = params;
|
|
529
|
-
const from = _nullishCoalesce(_nullishCoalesce(_optionalChain([this, 'access',
|
|
419
|
+
const from = _nullishCoalesce(_nullishCoalesce(_optionalChain([this, 'access', _7 => _7.config, 'access', _8 => _8.walletClient, 'optionalAccess', _9 => _9.account, 'optionalAccess', _10 => _10.address]), () => ( _optionalChain([this, 'access', _11 => _11.config, 'access', _12 => _12.account, 'optionalAccess', _13 => _13.address]))), () => ( "0x0000000000000000000000000000000000000000"));
|
|
530
420
|
const src = CHAIN_CONFIGS[fromChain];
|
|
531
421
|
const dst = CHAIN_CONFIGS[toChain];
|
|
532
422
|
if (!src) throw new StableValidationError("fromChain", fromChain, `LI.FI bridge: unsupported source chain "${fromChain}"`);
|
|
@@ -603,7 +493,7 @@ var LiFiService = class {
|
|
|
603
493
|
}
|
|
604
494
|
}
|
|
605
495
|
_requireFrom() {
|
|
606
|
-
const from = _nullishCoalesce(_optionalChain([this, 'access',
|
|
496
|
+
const from = _nullishCoalesce(_optionalChain([this, 'access', _14 => _14.config, 'access', _15 => _15.walletClient, 'optionalAccess', _16 => _16.account, 'optionalAccess', _17 => _17.address]), () => ( _optionalChain([this, 'access', _18 => _18.config, 'access', _19 => _19.account, 'optionalAccess', _20 => _20.address])));
|
|
607
497
|
if (!from) {
|
|
608
498
|
throw new StableValidationError(
|
|
609
499
|
"signer",
|
|
@@ -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
|
+
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
|
|
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 = _nullishCoalesce(config.walletClient, () => ( _viem.createWalletClient.call(void 0, {
|
|
684
|
+
account: config.account,
|
|
685
|
+
chain: this.chain,
|
|
686
|
+
transport: _nullishCoalesce(config.transport, () => ( _viem.http.call(void 0, config.rpc)))
|
|
687
|
+
})));
|
|
688
|
+
const morphoWc = _optionalChain([this, 'access', _21 => _21.wc, 'access', _22 => _22.chain, 'optionalAccess', _23 => _23.id]) === config.chainId ? this.wc : _viem.createWalletClient.call(void 0, {
|
|
689
|
+
account: this.wc.account,
|
|
690
|
+
chain: this.chain,
|
|
691
|
+
transport: _viem.custom.call(void 0, { request: this.wc.request.bind(this.wc) })
|
|
692
|
+
});
|
|
693
|
+
this.morpho = new (0, _morphosdk.MorphoClient)(morphoWc, { supportSignature: true });
|
|
694
|
+
this.pc = _viem.createPublicClient.call(void 0, { chain: this.chain, transport: _viem.http.call(void 0, 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 = _nullishCoalesce(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 = await _asyncNullishCoalesce(tokenDecimals, async () => ( await this._resolveDecimals(vaultData.asset, "earn deposit")));
|
|
752
|
+
const assetsRaw = _viem.parseUnits.call(void 0, String(amount), decimals);
|
|
753
|
+
const depositAction = vault.deposit({
|
|
754
|
+
amount: assetsRaw,
|
|
755
|
+
userAddress: from,
|
|
756
|
+
accrualVault: vaultData,
|
|
757
|
+
slippageTolerance: slippageTolerance !== void 0 ? _viem.parseUnits.call(void 0, String(slippageTolerance), 18) : void 0
|
|
758
|
+
});
|
|
759
|
+
const requirements = await depositAction.getRequirements();
|
|
760
|
+
let sig;
|
|
761
|
+
for (const req of requirements) {
|
|
762
|
+
if (_morphosdk.isRequirementApproval.call(void 0, 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 (_morphosdk.isRequirementSignature.call(void 0, 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 = await _asyncNullishCoalesce(tokenDecimals, async () => ( await this._resolveDecimals(vaultData.asset, "earn withdraw")));
|
|
798
|
+
const assetsRaw = _viem.parseUnits.call(void 0, 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: _viem.erc20Abi, functionName: "decimals" });
|
|
821
|
+
} catch (e2) {
|
|
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 = _viem.parseUnits.call(void 0, 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: _viem.parseUnits.call(void 0, 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: _viem.erc20Abi, functionName: "decimals" });
|
|
882
|
+
} catch (e3) {
|
|
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: _viem.parseUnits.call(void 0, 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: _viem.parseUnits.call(void 0, 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 = _optionalChain([this, 'access', _24 => _24.wc, 'access', _25 => _25.account, 'optionalAccess', _26 => _26.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: _viem.erc20Abi, functionName: "decimals" })
|
|
963
|
+
);
|
|
964
|
+
} catch (e4) {
|
|
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 (!_viem.isAddress.call(void 0, to)) {
|
|
782
995
|
throw new StableValidationError("to", to, `transfer: invalid recipient address "${to}"`);
|
|
@@ -795,7 +1008,7 @@ async function resolveTokenDecimals(publicClient, token, tokenDecimals) {
|
|
|
795
1008
|
if (tokenDecimals !== void 0) return tokenDecimals;
|
|
796
1009
|
try {
|
|
797
1010
|
return await publicClient.readContract({ address: token, abi: _viem.erc20Abi, functionName: "decimals" });
|
|
798
|
-
} catch (
|
|
1011
|
+
} catch (e5) {
|
|
799
1012
|
throw new StableValidationError(
|
|
800
1013
|
"tokenDecimals",
|
|
801
1014
|
void 0,
|
|
@@ -827,17 +1040,28 @@ function createStable({
|
|
|
827
1040
|
rpc: rpcOverride,
|
|
828
1041
|
account,
|
|
829
1042
|
transport,
|
|
830
|
-
walletClient
|
|
1043
|
+
walletClient,
|
|
1044
|
+
earn: earnConfig
|
|
831
1045
|
}) {
|
|
832
1046
|
const chain = network === "mainnet" /* Mainnet */ ? _chains.stable : _chains.stableTestnet;
|
|
833
1047
|
const rpc = (_nullishCoalesce(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;
|
|
840
|
-
const acc = _nullishCoalesce(_nullishCoalesce(_optionalChain([walletClient, 'optionalAccess',
|
|
1064
|
+
const acc = _nullishCoalesce(_nullishCoalesce(_optionalChain([walletClient, 'optionalAccess', _27 => _27.account]), () => ( account)), () => ( from));
|
|
841
1065
|
const wc = _nullishCoalesce(walletClient, () => ( _viem.createWalletClient.call(void 0, {
|
|
842
1066
|
account: acc,
|
|
843
1067
|
chain,
|
|
@@ -866,21 +1090,41 @@ 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
|
+
}
|
|
1107
|
+
};
|
|
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
|
+
}
|
|
881
1117
|
};
|
|
882
1118
|
}
|
|
883
1119
|
|
|
1120
|
+
// src/constants.ts
|
|
1121
|
+
var STABLE_USDT_ADDRESS = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
|
|
1122
|
+
var STABLE_VAULT_ADDRESS = "0x461eF73F640456138242398a39AB48a557A49293";
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
|
|
884
1128
|
|
|
885
1129
|
|
|
886
1130
|
|
|
@@ -890,4 +1134,4 @@ function createStable({
|
|
|
890
1134
|
|
|
891
1135
|
|
|
892
1136
|
|
|
893
|
-
exports.CHAIN_CONFIGS = CHAIN_CONFIGS; exports.Chain = Chain; exports.Network = Network; exports.StableError = StableError; exports.StableNetworkError = StableNetworkError; exports.StableQuoteError = StableQuoteError; exports.StableTransactionError = StableTransactionError; exports.StableValidationError = StableValidationError; exports.createStable = createStable;
|
|
1137
|
+
exports.CHAIN_CONFIGS = CHAIN_CONFIGS; exports.Chain = Chain; exports.Network = Network; exports.STABLE_USDT_ADDRESS = STABLE_USDT_ADDRESS; exports.STABLE_VAULT_ADDRESS = STABLE_VAULT_ADDRESS; exports.StableBulkError = StableBulkError; exports.StableError = StableError; exports.StableNetworkError = StableNetworkError; exports.StableQuoteError = StableQuoteError; exports.StableTransactionError = StableTransactionError; exports.StableValidationError = StableValidationError; exports.createStable = createStable; exports.createStableReader = createStableReader;
|