@stablechain/sdk 1.0.2 → 1.0.4
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 +753 -30
- package/dist/index.d.cts +98 -2
- package/dist/index.d.ts +98 -2
- package/dist/index.js +775 -52
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// src/stable.ts
|
|
2
2
|
import {
|
|
3
|
-
http as
|
|
4
|
-
createPublicClient as
|
|
5
|
-
createWalletClient as
|
|
6
|
-
erc20Abi as
|
|
7
|
-
isAddress as
|
|
8
|
-
parseUnits as
|
|
3
|
+
http as http6,
|
|
4
|
+
createPublicClient as createPublicClient6,
|
|
5
|
+
createWalletClient as createWalletClient5,
|
|
6
|
+
erc20Abi as erc20Abi5,
|
|
7
|
+
isAddress as isAddress5,
|
|
8
|
+
parseUnits as parseUnits5
|
|
9
9
|
} from "viem";
|
|
10
10
|
import { stable as stableMainnet2, stableTestnet as stableTestnet2 } from "viem/chains";
|
|
11
11
|
|
|
@@ -126,7 +126,11 @@ var HttpError = class extends Error {
|
|
|
126
126
|
};
|
|
127
127
|
function isRetryableError(err) {
|
|
128
128
|
if (err instanceof HttpError) return err.retryable;
|
|
129
|
-
|
|
129
|
+
if (err instanceof TypeError) {
|
|
130
|
+
const msg = err.message.toLowerCase();
|
|
131
|
+
return !(msg.includes("cors") || msg.includes("blocked"));
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
130
134
|
}
|
|
131
135
|
async function retryWithBackoff(fn, isRetryable = isRetryableError, opts = {}) {
|
|
132
136
|
const maxAttempts = opts.maxAttempts ?? 3;
|
|
@@ -166,6 +170,7 @@ var MorphoService = class {
|
|
|
166
170
|
})
|
|
167
171
|
});
|
|
168
172
|
this.morpho = new MorphoClient(morphoWc, { supportSignature: true });
|
|
173
|
+
this.morphoNoSig = new MorphoClient(morphoWc, { supportSignature: false });
|
|
169
174
|
this.pc = createPublicClient({
|
|
170
175
|
chain: this.chain,
|
|
171
176
|
transport: http(config.rpc)
|
|
@@ -303,6 +308,109 @@ var MorphoService = class {
|
|
|
303
308
|
() => this._forceRedeem(params)
|
|
304
309
|
);
|
|
305
310
|
}
|
|
311
|
+
// ─── Calldata preparation (no signing) ───────────────────────────────────
|
|
312
|
+
async prepareDepositCalldata(params) {
|
|
313
|
+
this._validateAmount(params.amount, "prepareDepositCalldata");
|
|
314
|
+
if (params.depositBuffer !== void 0 && (params.depositBuffer <= 0 || params.depositBuffer > 1)) {
|
|
315
|
+
throw new StableValidationError(
|
|
316
|
+
"depositBuffer",
|
|
317
|
+
params.depositBuffer,
|
|
318
|
+
"prepareDepositCalldata: depositBuffer must be > 0 and \u2264 1"
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
const { amount, tokenDecimals, slippageTolerance, depositBuffer } = params;
|
|
322
|
+
const from = this._requireFrom();
|
|
323
|
+
const vaultAddr = params.vault ?? this.config.vault;
|
|
324
|
+
const vault = this.morphoNoSig.vaultV2(vaultAddr, this.config.chainId);
|
|
325
|
+
const vaultData = await vault.getData();
|
|
326
|
+
const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "prepareDepositCalldata");
|
|
327
|
+
const assetsRaw = parseUnits(String(amount), decimals);
|
|
328
|
+
if (depositBuffer !== void 0) {
|
|
329
|
+
const bps = BigInt(Math.round(depositBuffer * 1e4));
|
|
330
|
+
const maxAllowed = vaultData.assetBalance * bps / 10000n;
|
|
331
|
+
if (assetsRaw > maxAllowed) {
|
|
332
|
+
const maxFormatted = Number(formatUnits(maxAllowed, decimals));
|
|
333
|
+
const idleFormatted = Number(formatUnits(vaultData.assetBalance, decimals));
|
|
334
|
+
throw new StableValidationError(
|
|
335
|
+
"amount",
|
|
336
|
+
amount,
|
|
337
|
+
`prepareDepositCalldata: amount ${amount} exceeds deposit buffer \u2014 max ${maxFormatted} (${depositBuffer * 100}% of ${idleFormatted} idle liquidity)`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const depositAction = vault.deposit({
|
|
342
|
+
amount: assetsRaw,
|
|
343
|
+
userAddress: from,
|
|
344
|
+
accrualVault: vaultData,
|
|
345
|
+
slippageTolerance: slippageTolerance !== void 0 ? parseUnits(String(slippageTolerance), 18) : void 0
|
|
346
|
+
});
|
|
347
|
+
const requirements = await depositAction.getRequirements();
|
|
348
|
+
const steps = [];
|
|
349
|
+
for (const req of requirements) {
|
|
350
|
+
if (isRequirementApproval(req)) {
|
|
351
|
+
steps.push({ to: req.to, data: req.data, value: req.value });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const tx = depositAction.buildTx(void 0);
|
|
355
|
+
steps.push({ to: tx.to, data: tx.data, value: tx.value });
|
|
356
|
+
return { steps, chainId: this.config.chainId };
|
|
357
|
+
}
|
|
358
|
+
async prepareWithdrawCalldata(params) {
|
|
359
|
+
this._validateAmount(params.amount, "prepareWithdrawCalldata");
|
|
360
|
+
const { amount, tokenDecimals } = params;
|
|
361
|
+
const from = this._requireFrom();
|
|
362
|
+
const vaultAddr = this.config.vault;
|
|
363
|
+
const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
|
|
364
|
+
const vaultData = await vault.getData();
|
|
365
|
+
const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "prepareWithdrawCalldata");
|
|
366
|
+
const assetsRaw = parseUnits(String(amount), decimals);
|
|
367
|
+
const tx = vaultData.assetBalance >= assetsRaw ? vault.withdraw({ amount: assetsRaw, userAddress: from }).buildTx() : vault.forceWithdraw({
|
|
368
|
+
deallocations: this._buildAutoDeallocs(
|
|
369
|
+
vaultData.accrualAdapters,
|
|
370
|
+
assetsRaw - vaultData.assetBalance,
|
|
371
|
+
amount
|
|
372
|
+
),
|
|
373
|
+
withdraw: { amount: assetsRaw },
|
|
374
|
+
userAddress: from
|
|
375
|
+
}).buildTx();
|
|
376
|
+
return { to: tx.to, data: tx.data, value: tx.value, chainId: this.config.chainId };
|
|
377
|
+
}
|
|
378
|
+
async prepareRedeemCalldata(params) {
|
|
379
|
+
this._validateShares(params.shares, "prepareRedeemCalldata");
|
|
380
|
+
const { shares, shareDecimals } = params;
|
|
381
|
+
const from = this._requireFrom();
|
|
382
|
+
const vaultAddr = this.config.vault;
|
|
383
|
+
let decimals = shareDecimals;
|
|
384
|
+
if (decimals === void 0) {
|
|
385
|
+
try {
|
|
386
|
+
decimals = await this.pc.readContract({
|
|
387
|
+
address: vaultAddr,
|
|
388
|
+
abi: erc20Abi,
|
|
389
|
+
functionName: "decimals"
|
|
390
|
+
});
|
|
391
|
+
} catch {
|
|
392
|
+
throw new StableValidationError(
|
|
393
|
+
"shareDecimals",
|
|
394
|
+
void 0,
|
|
395
|
+
`prepareRedeemCalldata: could not fetch share decimals for vault "${vaultAddr}" \u2014 pass shareDecimals explicitly`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const sharesRaw = parseUnits(String(shares), decimals);
|
|
400
|
+
const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
|
|
401
|
+
const vaultData = await vault.getData();
|
|
402
|
+
const assetsEquivalent = vaultData.toAssets(sharesRaw);
|
|
403
|
+
const tx = vaultData.assetBalance >= assetsEquivalent ? vault.redeem({ shares: sharesRaw, userAddress: from }).buildTx() : vault.forceRedeem({
|
|
404
|
+
deallocations: this._buildAutoDeallocs(
|
|
405
|
+
vaultData.accrualAdapters,
|
|
406
|
+
assetsEquivalent - vaultData.assetBalance,
|
|
407
|
+
shares
|
|
408
|
+
),
|
|
409
|
+
redeem: { shares: sharesRaw },
|
|
410
|
+
userAddress: from
|
|
411
|
+
}).buildTx();
|
|
412
|
+
return { to: tx.to, data: tx.data, value: tx.value, chainId: this.config.chainId };
|
|
413
|
+
}
|
|
306
414
|
// ─── Private operations ──────────────────────────────────────────────────
|
|
307
415
|
async _deposit(params) {
|
|
308
416
|
const {
|
|
@@ -390,7 +498,15 @@ var MorphoService = class {
|
|
|
390
498
|
const vaultData = await vault.getData();
|
|
391
499
|
const decimals = tokenDecimals ?? await this._resolveDecimals(vaultData.asset, "earn withdraw");
|
|
392
500
|
const assetsRaw = parseUnits(String(amount), decimals);
|
|
393
|
-
const tx = vault.withdraw({ amount: assetsRaw, userAddress: from }).buildTx()
|
|
501
|
+
const tx = vaultData.assetBalance >= assetsRaw ? vault.withdraw({ amount: assetsRaw, userAddress: from }).buildTx() : vault.forceWithdraw({
|
|
502
|
+
deallocations: this._buildAutoDeallocs(
|
|
503
|
+
vaultData.accrualAdapters,
|
|
504
|
+
assetsRaw - vaultData.assetBalance,
|
|
505
|
+
amount
|
|
506
|
+
),
|
|
507
|
+
withdraw: { amount: assetsRaw },
|
|
508
|
+
userAddress: from
|
|
509
|
+
}).buildTx();
|
|
394
510
|
const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
|
|
395
511
|
const receipt = await this.pc.waitForTransactionReceipt({ hash });
|
|
396
512
|
if (receipt.status === "reverted") {
|
|
@@ -426,7 +542,17 @@ var MorphoService = class {
|
|
|
426
542
|
}
|
|
427
543
|
const sharesRaw = parseUnits(String(shares), decimals);
|
|
428
544
|
const vault = this.morpho.vaultV2(vaultAddr, this.config.chainId);
|
|
429
|
-
const
|
|
545
|
+
const vaultData = await vault.getData();
|
|
546
|
+
const assetsEquivalent = vaultData.toAssets(sharesRaw);
|
|
547
|
+
const tx = vaultData.assetBalance >= assetsEquivalent ? vault.redeem({ shares: sharesRaw, userAddress: from }).buildTx() : vault.forceRedeem({
|
|
548
|
+
deallocations: this._buildAutoDeallocs(
|
|
549
|
+
vaultData.accrualAdapters,
|
|
550
|
+
assetsEquivalent - vaultData.assetBalance,
|
|
551
|
+
shares
|
|
552
|
+
),
|
|
553
|
+
redeem: { shares: sharesRaw },
|
|
554
|
+
userAddress: from
|
|
555
|
+
}).buildTx();
|
|
430
556
|
const hash = await this._sendTx(tx.to, tx.data, tx.value, from);
|
|
431
557
|
const receipt = await this.pc.waitForTransactionReceipt({ hash });
|
|
432
558
|
if (receipt.status === "reverted") {
|
|
@@ -523,6 +649,51 @@ var MorphoService = class {
|
|
|
523
649
|
}
|
|
524
650
|
return { txHash: receipt.transactionHash, idempotencyKey };
|
|
525
651
|
}
|
|
652
|
+
// Automatically derives the minimum set of adapter deallocations needed to cover `deficit`
|
|
653
|
+
// when the vault has insufficient idle cash for a direct withdraw/redeem. Adds a 0.5%
|
|
654
|
+
// buffer to account for interest accrual between vaultData read and tx execution.
|
|
655
|
+
_buildAutoDeallocs(adapters, deficit, amount) {
|
|
656
|
+
const needed = deficit + deficit * 50n / 10000n;
|
|
657
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
658
|
+
const result = [];
|
|
659
|
+
let remaining = needed;
|
|
660
|
+
for (const adapter of adapters) {
|
|
661
|
+
if (remaining === 0n) break;
|
|
662
|
+
remaining = this._pullFromAdapter(adapter, remaining, now, result);
|
|
663
|
+
}
|
|
664
|
+
if (remaining > 0n) {
|
|
665
|
+
throw new StableValidationError(
|
|
666
|
+
"amount",
|
|
667
|
+
amount,
|
|
668
|
+
"earn withdraw: insufficient adapter liquidity \u2014 reduce the amount or wait for market liquidity to free up"
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
_pullFromAdapter(adapter, remaining, now, result) {
|
|
674
|
+
if (adapter.type !== "VaultV2MorphoMarketV1AdapterV2") {
|
|
675
|
+
const available = adapter.realAssets(now);
|
|
676
|
+
if (available === 0n) return remaining;
|
|
677
|
+
const pull = available < remaining ? available : remaining;
|
|
678
|
+
result.push({ adapter: adapter.address, amount: pull });
|
|
679
|
+
return remaining - pull;
|
|
680
|
+
}
|
|
681
|
+
const a = adapter;
|
|
682
|
+
return this._pullFromV2Markets(adapter.address, a.markets, a.supplyShares, remaining, result);
|
|
683
|
+
}
|
|
684
|
+
_pullFromV2Markets(adapterAddress, markets, supplyShares, remaining, result) {
|
|
685
|
+
let rem = remaining;
|
|
686
|
+
for (const mkt of markets) {
|
|
687
|
+
if (rem === 0n) break;
|
|
688
|
+
const adapterShares = supplyShares[mkt.id] ?? 0n;
|
|
689
|
+
const available = mkt.totalSupplyShares > 0n ? adapterShares * mkt.totalSupplyAssets / mkt.totalSupplyShares : 0n;
|
|
690
|
+
if (available === 0n) continue;
|
|
691
|
+
const pull = available < rem ? available : rem;
|
|
692
|
+
result.push({ adapter: adapterAddress, marketParams: mkt.params, amount: pull });
|
|
693
|
+
rem -= pull;
|
|
694
|
+
}
|
|
695
|
+
return rem;
|
|
696
|
+
}
|
|
526
697
|
_buildDeallocs(deallocations, assetDecimals) {
|
|
527
698
|
return deallocations.map((d) => ({
|
|
528
699
|
adapter: d.adapter,
|
|
@@ -636,19 +807,19 @@ import {
|
|
|
636
807
|
import { stable as stableMainnet, stableTestnet } from "viem/chains";
|
|
637
808
|
|
|
638
809
|
// src/types.ts
|
|
639
|
-
var Chain = /* @__PURE__ */ ((
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
return
|
|
810
|
+
var Chain = /* @__PURE__ */ ((Chain3) => {
|
|
811
|
+
Chain3["Sepolia"] = "sepolia";
|
|
812
|
+
Chain3["StableTestnet"] = "stable-testnet";
|
|
813
|
+
Chain3["Stable"] = "stable";
|
|
814
|
+
Chain3["Ethereum"] = "ethereum";
|
|
815
|
+
Chain3["Arbitrum"] = "arbitrum";
|
|
816
|
+
Chain3["Ink"] = "ink";
|
|
817
|
+
Chain3["Bera"] = "bera";
|
|
818
|
+
Chain3["MegaETH"] = "megaeth";
|
|
819
|
+
Chain3["Base"] = "base";
|
|
820
|
+
Chain3["BSC"] = "bsc";
|
|
821
|
+
Chain3["HyperEVM"] = "hyperevm";
|
|
822
|
+
return Chain3;
|
|
652
823
|
})(Chain || {});
|
|
653
824
|
var Network = /* @__PURE__ */ ((Network2) => {
|
|
654
825
|
Network2["Mainnet"] = "mainnet";
|
|
@@ -843,14 +1014,241 @@ var EarnReader = class {
|
|
|
843
1014
|
}
|
|
844
1015
|
};
|
|
845
1016
|
|
|
846
|
-
// src/
|
|
1017
|
+
// src/earn/merkl.ts
|
|
847
1018
|
import {
|
|
848
1019
|
http as http3,
|
|
849
1020
|
createPublicClient as createPublicClient3,
|
|
850
1021
|
createWalletClient as createWalletClient2,
|
|
851
|
-
|
|
1022
|
+
encodeFunctionData,
|
|
852
1023
|
formatUnits as formatUnits3,
|
|
853
|
-
isAddress as isAddress2
|
|
1024
|
+
isAddress as isAddress2
|
|
1025
|
+
} from "viem";
|
|
1026
|
+
var MERKL_API_BASE_DEFAULT = "https://api.merkl.xyz";
|
|
1027
|
+
var DISTRIBUTOR_ADDRESS = "0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae";
|
|
1028
|
+
var DISTRIBUTOR_ABI = [
|
|
1029
|
+
{
|
|
1030
|
+
inputs: [
|
|
1031
|
+
{ name: "users", type: "address[]" },
|
|
1032
|
+
{ name: "tokens", type: "address[]" },
|
|
1033
|
+
{ name: "amounts", type: "uint256[]" },
|
|
1034
|
+
{ name: "proofs", type: "bytes32[][]" }
|
|
1035
|
+
],
|
|
1036
|
+
name: "claim",
|
|
1037
|
+
outputs: [],
|
|
1038
|
+
stateMutability: "nonpayable",
|
|
1039
|
+
type: "function"
|
|
1040
|
+
}
|
|
1041
|
+
];
|
|
1042
|
+
var MerklService = class {
|
|
1043
|
+
constructor(config) {
|
|
1044
|
+
this.config = config;
|
|
1045
|
+
}
|
|
1046
|
+
async fetchRewards(address) {
|
|
1047
|
+
if (!isAddress2(address)) {
|
|
1048
|
+
throw new StableValidationError(
|
|
1049
|
+
"address",
|
|
1050
|
+
address,
|
|
1051
|
+
`merkl: invalid address "${address}"`
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
const base = (this.config.merklApiBase ?? MERKL_API_BASE_DEFAULT).replace(/\/$/, "");
|
|
1055
|
+
const url = `${base}/v4/users/${address}/rewards?chainId=${this.config.chainId}`;
|
|
1056
|
+
const data = await this._fetch(url);
|
|
1057
|
+
return this._parseRewards(data);
|
|
1058
|
+
}
|
|
1059
|
+
async claim() {
|
|
1060
|
+
const address = this._requireFrom();
|
|
1061
|
+
const { rewards } = await this.fetchRewards(address);
|
|
1062
|
+
const claimable = rewards.filter(
|
|
1063
|
+
(r) => r.rawAmount !== "0" && r.proofs.length > 0
|
|
1064
|
+
);
|
|
1065
|
+
if (claimable.length === 0) {
|
|
1066
|
+
throw new StableValidationError(
|
|
1067
|
+
"rewards",
|
|
1068
|
+
[],
|
|
1069
|
+
rewards.length > 0 ? "merkl: rewards are pending the next merkle tree update \u2014 try again in ~2 hours" : "merkl: no rewards to claim on this chain"
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
const chain = makeChain(this.config.chainId, this.config.rpc);
|
|
1073
|
+
const wc = this.config.walletClient ?? createWalletClient2({
|
|
1074
|
+
account: this.config.account,
|
|
1075
|
+
chain,
|
|
1076
|
+
transport: this.config.transport ?? http3(this.config.rpc)
|
|
1077
|
+
});
|
|
1078
|
+
try {
|
|
1079
|
+
await wc.switchChain({ id: this.config.chainId });
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
throw new StableTransactionError({
|
|
1082
|
+
phase: "switch_chain",
|
|
1083
|
+
shortMessage: `merkl: wallet rejected or failed to switch to chain ${this.config.chainId}`,
|
|
1084
|
+
chainId: this.config.chainId,
|
|
1085
|
+
cause: err
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
const users = claimable.map(() => address);
|
|
1089
|
+
const tokens = claimable.map(
|
|
1090
|
+
(r) => r.token.address
|
|
1091
|
+
);
|
|
1092
|
+
const amounts = claimable.map((r) => BigInt(r.rawAmount));
|
|
1093
|
+
const proofs = claimable.map((r) => r.proofs);
|
|
1094
|
+
let hash;
|
|
1095
|
+
try {
|
|
1096
|
+
hash = await wc.writeContract({
|
|
1097
|
+
address: DISTRIBUTOR_ADDRESS,
|
|
1098
|
+
abi: DISTRIBUTOR_ABI,
|
|
1099
|
+
functionName: "claim",
|
|
1100
|
+
args: [users, tokens, amounts, proofs],
|
|
1101
|
+
account: wc.account,
|
|
1102
|
+
chain
|
|
1103
|
+
});
|
|
1104
|
+
} catch (err) {
|
|
1105
|
+
throw new StableTransactionError({
|
|
1106
|
+
phase: "send",
|
|
1107
|
+
shortMessage: "merkl: claim transaction failed",
|
|
1108
|
+
chainId: this.config.chainId,
|
|
1109
|
+
cause: err
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
const pc = createPublicClient3({
|
|
1113
|
+
chain,
|
|
1114
|
+
transport: http3(this.config.rpc)
|
|
1115
|
+
});
|
|
1116
|
+
const receipt = await pc.waitForTransactionReceipt({ hash });
|
|
1117
|
+
if (receipt.status === "reverted") {
|
|
1118
|
+
throw new StableTransactionError({
|
|
1119
|
+
phase: "receipt",
|
|
1120
|
+
shortMessage: "merkl: claim transaction reverted on-chain",
|
|
1121
|
+
txHash: receipt.transactionHash,
|
|
1122
|
+
chainId: this.config.chainId
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
return {
|
|
1126
|
+
txHash: receipt.transactionHash,
|
|
1127
|
+
tokenCount: claimable.length
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
async prepareClaimCalldata(address) {
|
|
1131
|
+
if (!isAddress2(address)) {
|
|
1132
|
+
throw new StableValidationError(
|
|
1133
|
+
"address",
|
|
1134
|
+
address,
|
|
1135
|
+
`merkl: invalid address "${address}"`
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
const { rewards } = await this.fetchRewards(address);
|
|
1139
|
+
const claimable = rewards.filter(
|
|
1140
|
+
(r) => r.rawAmount !== "0" && r.proofs.length > 0
|
|
1141
|
+
);
|
|
1142
|
+
if (claimable.length === 0) {
|
|
1143
|
+
throw new StableValidationError(
|
|
1144
|
+
"rewards",
|
|
1145
|
+
[],
|
|
1146
|
+
rewards.length > 0 ? "merkl: rewards are pending the next merkle tree update \u2014 try again in ~2 hours" : "merkl: no rewards to claim on this chain"
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
const users = claimable.map(() => address);
|
|
1150
|
+
const tokens = claimable.map((r) => r.token.address);
|
|
1151
|
+
const amounts = claimable.map((r) => BigInt(r.rawAmount));
|
|
1152
|
+
const proofs = claimable.map((r) => r.proofs);
|
|
1153
|
+
const data = encodeFunctionData({
|
|
1154
|
+
abi: DISTRIBUTOR_ABI,
|
|
1155
|
+
functionName: "claim",
|
|
1156
|
+
args: [users, tokens, amounts, proofs]
|
|
1157
|
+
});
|
|
1158
|
+
return {
|
|
1159
|
+
to: DISTRIBUTOR_ADDRESS,
|
|
1160
|
+
data,
|
|
1161
|
+
chainId: this.config.chainId,
|
|
1162
|
+
rewards: claimable
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
_requireFrom() {
|
|
1166
|
+
const from = this.config.walletClient?.account?.address ?? this.config.account?.address;
|
|
1167
|
+
if (!from) {
|
|
1168
|
+
throw new StableValidationError(
|
|
1169
|
+
"signer",
|
|
1170
|
+
void 0,
|
|
1171
|
+
"merkl.claim: requires a signer \u2014 set walletClient or account in StableConfig"
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
return from;
|
|
1175
|
+
}
|
|
1176
|
+
_parseRewards(data) {
|
|
1177
|
+
const { chainId } = this.config;
|
|
1178
|
+
const rewards = [];
|
|
1179
|
+
const entries = Array.isArray(data) ? data : [data];
|
|
1180
|
+
for (const entry of entries) {
|
|
1181
|
+
if (entry?.chain?.id !== chainId) continue;
|
|
1182
|
+
for (const reward of entry.rewards ?? []) {
|
|
1183
|
+
const parsed = this._parseReward(reward);
|
|
1184
|
+
if (parsed) rewards.push(parsed);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return { chainId, rewards };
|
|
1188
|
+
}
|
|
1189
|
+
_parseReward(reward) {
|
|
1190
|
+
if (!reward?.token?.address) return null;
|
|
1191
|
+
const net = BigInt(reward.amount ?? "0") - BigInt(reward.claimed ?? "0");
|
|
1192
|
+
if (net <= 0n) return null;
|
|
1193
|
+
const rawAmount = net.toString();
|
|
1194
|
+
const decimals = reward.token.decimals ?? 18;
|
|
1195
|
+
return {
|
|
1196
|
+
token: {
|
|
1197
|
+
address: reward.token.address,
|
|
1198
|
+
symbol: reward.token.symbol ?? "",
|
|
1199
|
+
decimals
|
|
1200
|
+
},
|
|
1201
|
+
rawAmount,
|
|
1202
|
+
amount: Number(formatUnits3(BigInt(rawAmount), decimals)),
|
|
1203
|
+
proofs: reward.proofs ?? []
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
async _fetch(url) {
|
|
1207
|
+
try {
|
|
1208
|
+
return await retryWithBackoff(() => this._fetchOnce(url));
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
if (err instanceof StableNetworkError) throw err;
|
|
1211
|
+
if (err instanceof HttpError) {
|
|
1212
|
+
throw new StableNetworkError({
|
|
1213
|
+
shortMessage: `merkl: request failed \u2014 ${err.message}`,
|
|
1214
|
+
url,
|
|
1215
|
+
cause: err
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
throw new StableNetworkError({
|
|
1219
|
+
shortMessage: "merkl: request failed before reaching the server",
|
|
1220
|
+
url,
|
|
1221
|
+
cause: err
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
async _fetchOnce(url) {
|
|
1226
|
+
const res = await fetch(url);
|
|
1227
|
+
if (!res.ok) {
|
|
1228
|
+
if (res.status === 429 || res.status >= 500) {
|
|
1229
|
+
throw new HttpError(
|
|
1230
|
+
`merkl: ${res.statusText}`,
|
|
1231
|
+
res.status,
|
|
1232
|
+
true
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
throw new StableNetworkError({
|
|
1236
|
+
shortMessage: `merkl: API error ${res.status} ${res.statusText}`,
|
|
1237
|
+
url
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
return res.json();
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
// src/services/lifi.ts
|
|
1245
|
+
import {
|
|
1246
|
+
http as http4,
|
|
1247
|
+
createPublicClient as createPublicClient4,
|
|
1248
|
+
createWalletClient as createWalletClient3,
|
|
1249
|
+
erc20Abi as erc20Abi3,
|
|
1250
|
+
formatUnits as formatUnits4,
|
|
1251
|
+
isAddress as isAddress3,
|
|
854
1252
|
parseUnits as parseUnits3
|
|
855
1253
|
} from "viem";
|
|
856
1254
|
|
|
@@ -1055,14 +1453,14 @@ var LiFiService = class {
|
|
|
1055
1453
|
amount,
|
|
1056
1454
|
toAddress
|
|
1057
1455
|
}) {
|
|
1058
|
-
if (!
|
|
1456
|
+
if (!isAddress3(fromToken)) {
|
|
1059
1457
|
throw new StableValidationError(
|
|
1060
1458
|
"fromToken",
|
|
1061
1459
|
fromToken,
|
|
1062
1460
|
`swap: invalid fromToken address "${fromToken}"`
|
|
1063
1461
|
);
|
|
1064
1462
|
}
|
|
1065
|
-
if (!
|
|
1463
|
+
if (!isAddress3(toToken)) {
|
|
1066
1464
|
throw new StableValidationError(
|
|
1067
1465
|
"toToken",
|
|
1068
1466
|
toToken,
|
|
@@ -1083,7 +1481,7 @@ var LiFiService = class {
|
|
|
1083
1481
|
"swap: amount must be a finite positive number"
|
|
1084
1482
|
);
|
|
1085
1483
|
}
|
|
1086
|
-
if (toAddress !== void 0 && !
|
|
1484
|
+
if (toAddress !== void 0 && !isAddress3(toAddress)) {
|
|
1087
1485
|
throw new StableValidationError(
|
|
1088
1486
|
"toAddress",
|
|
1089
1487
|
toAddress,
|
|
@@ -1099,14 +1497,14 @@ var LiFiService = class {
|
|
|
1099
1497
|
amount,
|
|
1100
1498
|
recipient
|
|
1101
1499
|
}) {
|
|
1102
|
-
if (!
|
|
1500
|
+
if (!isAddress3(fromToken)) {
|
|
1103
1501
|
throw new StableValidationError(
|
|
1104
1502
|
"fromToken",
|
|
1105
1503
|
fromToken,
|
|
1106
1504
|
`LI.FI bridge: invalid fromToken address "${fromToken}"`
|
|
1107
1505
|
);
|
|
1108
1506
|
}
|
|
1109
|
-
if (!
|
|
1507
|
+
if (!isAddress3(toToken)) {
|
|
1110
1508
|
throw new StableValidationError(
|
|
1111
1509
|
"toToken",
|
|
1112
1510
|
toToken,
|
|
@@ -1120,7 +1518,7 @@ var LiFiService = class {
|
|
|
1120
1518
|
"LI.FI bridge: amount must be a finite positive number"
|
|
1121
1519
|
);
|
|
1122
1520
|
}
|
|
1123
|
-
if (recipient !== void 0 && !
|
|
1521
|
+
if (recipient !== void 0 && !isAddress3(recipient)) {
|
|
1124
1522
|
throw new StableValidationError(
|
|
1125
1523
|
"recipient",
|
|
1126
1524
|
recipient,
|
|
@@ -1201,7 +1599,7 @@ var LiFiService = class {
|
|
|
1201
1599
|
_toSwapQuote(quote) {
|
|
1202
1600
|
return {
|
|
1203
1601
|
toAmount: Number(
|
|
1204
|
-
|
|
1602
|
+
formatUnits4(
|
|
1205
1603
|
BigInt(quote.estimate.toAmount),
|
|
1206
1604
|
quote.action.toToken.decimals
|
|
1207
1605
|
)
|
|
@@ -1225,10 +1623,10 @@ var LiFiService = class {
|
|
|
1225
1623
|
});
|
|
1226
1624
|
}
|
|
1227
1625
|
const srcChain = makeChain(srcChainId, srcRpc);
|
|
1228
|
-
const wc = this.config.walletClient ??
|
|
1626
|
+
const wc = this.config.walletClient ?? createWalletClient3({
|
|
1229
1627
|
account: this.config.account,
|
|
1230
1628
|
chain: srcChain,
|
|
1231
|
-
transport: this.config.transport ??
|
|
1629
|
+
transport: this.config.transport ?? http4(srcRpc)
|
|
1232
1630
|
});
|
|
1233
1631
|
try {
|
|
1234
1632
|
await wc.switchChain({ id: srcChainId });
|
|
@@ -1272,9 +1670,9 @@ var LiFiService = class {
|
|
|
1272
1670
|
const tokenAddr = quote.fromToken;
|
|
1273
1671
|
const isNative = tokenAddr.toLowerCase() === "0x0000000000000000000000000000000000000000" || tokenAddr.toLowerCase() === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
1274
1672
|
if (isNative || !quote.approvalAddress) return;
|
|
1275
|
-
const pc =
|
|
1673
|
+
const pc = createPublicClient4({
|
|
1276
1674
|
chain: srcChain,
|
|
1277
|
-
transport:
|
|
1675
|
+
transport: http4(srcRpc)
|
|
1278
1676
|
});
|
|
1279
1677
|
const allowance = await pc.readContract({
|
|
1280
1678
|
address: tokenAddr,
|
|
@@ -1325,14 +1723,311 @@ function isLiFiQuoteShape(v) {
|
|
|
1325
1723
|
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);
|
|
1326
1724
|
}
|
|
1327
1725
|
|
|
1726
|
+
// src/services/layerzero.ts
|
|
1727
|
+
import {
|
|
1728
|
+
concat,
|
|
1729
|
+
createPublicClient as createPublicClient5,
|
|
1730
|
+
createWalletClient as createWalletClient4,
|
|
1731
|
+
erc20Abi as erc20Abi4,
|
|
1732
|
+
http as http5,
|
|
1733
|
+
isAddress as isAddress4,
|
|
1734
|
+
numberToHex,
|
|
1735
|
+
padHex,
|
|
1736
|
+
parseUnits as parseUnits4
|
|
1737
|
+
} from "viem";
|
|
1738
|
+
var C = CHAIN_CONFIGS;
|
|
1739
|
+
var BRIDGE_CHAIN_CONFIGS = {
|
|
1740
|
+
// ─── Testnet ───────────────────────────────────────────────────────────────
|
|
1741
|
+
["sepolia" /* Sepolia */]: { ...C["sepolia" /* Sepolia */], lzEid: 40161, oftAdapter: "0xc099cD946d5efCC35A99D64E808c1430cEf08126" },
|
|
1742
|
+
["stable-testnet" /* StableTestnet */]: { ...C["stable-testnet" /* StableTestnet */], lzEid: 40374, oftAdapter: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736" },
|
|
1743
|
+
// ─── Mainnet ──────────────────────────────────────────────────────────────
|
|
1744
|
+
["stable" /* Stable */]: { ...C["stable" /* Stable */], lzEid: 30396, oftAdapter: "0xedaba024be4d87974d5aB11C6Dd586963CcCB027" },
|
|
1745
|
+
["ethereum" /* Ethereum */]: { ...C["ethereum" /* Ethereum */], lzEid: 30101, oftAdapter: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee" },
|
|
1746
|
+
["arbitrum" /* Arbitrum */]: { ...C["arbitrum" /* Arbitrum */], lzEid: 30110, oftAdapter: "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92" },
|
|
1747
|
+
["ink" /* Ink */]: { ...C["ink" /* Ink */], lzEid: 30339, oftAdapter: "0x1cB6De532588fCA4a21B7209DE7C456AF8434A65" },
|
|
1748
|
+
["bera" /* Bera */]: { ...C["bera" /* Bera */], lzEid: 30362, oftAdapter: "0x3Dc96399109df5ceb2C226664A086140bD0379cB" },
|
|
1749
|
+
["megaeth" /* MegaETH */]: { ...C["megaeth" /* MegaETH */], lzEid: 30398, oftAdapter: "0x9151434b16b9763660705744891fa906f660ecc5" },
|
|
1750
|
+
["hyperevm" /* HyperEVM */]: { ...C["hyperevm" /* HyperEVM */], lzEid: 30367, oftAdapter: "0x904861a24F30EC96ea7CFC3bE9EA4B476d237e98" }
|
|
1751
|
+
};
|
|
1752
|
+
var OFT_ADAPTER_ABI = [
|
|
1753
|
+
{
|
|
1754
|
+
name: "quoteSend",
|
|
1755
|
+
type: "function",
|
|
1756
|
+
inputs: [
|
|
1757
|
+
{
|
|
1758
|
+
name: "_sendParam",
|
|
1759
|
+
type: "tuple",
|
|
1760
|
+
components: [
|
|
1761
|
+
{ name: "dstEid", type: "uint32" },
|
|
1762
|
+
{ name: "to", type: "bytes32" },
|
|
1763
|
+
{ name: "amountLD", type: "uint256" },
|
|
1764
|
+
{ name: "minAmountLD", type: "uint256" },
|
|
1765
|
+
{ name: "extraOptions", type: "bytes" },
|
|
1766
|
+
{ name: "composeMsg", type: "bytes" },
|
|
1767
|
+
{ name: "oftCmd", type: "bytes" }
|
|
1768
|
+
]
|
|
1769
|
+
},
|
|
1770
|
+
{ name: "_payInLzToken", type: "bool" }
|
|
1771
|
+
],
|
|
1772
|
+
outputs: [
|
|
1773
|
+
{
|
|
1774
|
+
name: "msgFee",
|
|
1775
|
+
type: "tuple",
|
|
1776
|
+
components: [
|
|
1777
|
+
{ name: "nativeFee", type: "uint256" },
|
|
1778
|
+
{ name: "lzTokenFee", type: "uint256" }
|
|
1779
|
+
]
|
|
1780
|
+
}
|
|
1781
|
+
],
|
|
1782
|
+
stateMutability: "view"
|
|
1783
|
+
},
|
|
1784
|
+
{
|
|
1785
|
+
name: "send",
|
|
1786
|
+
type: "function",
|
|
1787
|
+
inputs: [
|
|
1788
|
+
{
|
|
1789
|
+
name: "_sendParam",
|
|
1790
|
+
type: "tuple",
|
|
1791
|
+
components: [
|
|
1792
|
+
{ name: "dstEid", type: "uint32" },
|
|
1793
|
+
{ name: "to", type: "bytes32" },
|
|
1794
|
+
{ name: "amountLD", type: "uint256" },
|
|
1795
|
+
{ name: "minAmountLD", type: "uint256" },
|
|
1796
|
+
{ name: "extraOptions", type: "bytes" },
|
|
1797
|
+
{ name: "composeMsg", type: "bytes" },
|
|
1798
|
+
{ name: "oftCmd", type: "bytes" }
|
|
1799
|
+
]
|
|
1800
|
+
},
|
|
1801
|
+
{
|
|
1802
|
+
name: "_fee",
|
|
1803
|
+
type: "tuple",
|
|
1804
|
+
components: [
|
|
1805
|
+
{ name: "nativeFee", type: "uint256" },
|
|
1806
|
+
{ name: "lzTokenFee", type: "uint256" }
|
|
1807
|
+
]
|
|
1808
|
+
},
|
|
1809
|
+
{ name: "_refundAddress", type: "address" }
|
|
1810
|
+
],
|
|
1811
|
+
outputs: [],
|
|
1812
|
+
stateMutability: "payable"
|
|
1813
|
+
}
|
|
1814
|
+
];
|
|
1815
|
+
function buildLzOptions(gasLimit = 0n) {
|
|
1816
|
+
return concat([
|
|
1817
|
+
numberToHex(3, { size: 2 }),
|
|
1818
|
+
// TYPE_3 header
|
|
1819
|
+
numberToHex(1, { size: 1 }),
|
|
1820
|
+
// Executor worker ID
|
|
1821
|
+
numberToHex(17, { size: 2 }),
|
|
1822
|
+
// option payload length = 17 bytes
|
|
1823
|
+
numberToHex(1, { size: 1 }),
|
|
1824
|
+
// LzReceive option type
|
|
1825
|
+
numberToHex(gasLimit, { size: 16 })
|
|
1826
|
+
// gas limit as uint128
|
|
1827
|
+
]);
|
|
1828
|
+
}
|
|
1829
|
+
var LayerZeroService = class {
|
|
1830
|
+
constructor(config) {
|
|
1831
|
+
this.config = config;
|
|
1832
|
+
}
|
|
1833
|
+
async quoteBridge({ fromChain, toChain, fromToken, toToken, amount, recipient }) {
|
|
1834
|
+
const recipientAddr = recipient ?? "0x0000000000000000000000000000000000000000";
|
|
1835
|
+
this.validateBridgeParams({ fromChain, toChain, amount, recipient: recipientAddr }, "quoteBridge");
|
|
1836
|
+
const { src, dst } = this.resolveChains(fromChain, toChain, fromToken, toToken);
|
|
1837
|
+
const oftAdapter = src.oftAdapter;
|
|
1838
|
+
const amountLD = parseUnits4(String(amount), src.decimals);
|
|
1839
|
+
try {
|
|
1840
|
+
const { nativeFee, lzTokenFee } = await createPublicClient5({
|
|
1841
|
+
chain: makeChain(src.id, src.rpc),
|
|
1842
|
+
transport: http5(src.rpc)
|
|
1843
|
+
}).readContract({
|
|
1844
|
+
address: oftAdapter,
|
|
1845
|
+
abi: OFT_ADAPTER_ABI,
|
|
1846
|
+
functionName: "quoteSend",
|
|
1847
|
+
args: [this.buildSendParams(dst, amountLD, recipientAddr), false]
|
|
1848
|
+
});
|
|
1849
|
+
return { nativeFee, lzTokenFee, toAmount: amount };
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
throw new StableQuoteError({
|
|
1852
|
+
provider: "layerzero",
|
|
1853
|
+
shortMessage: "quoteBridge: quoteSend call failed",
|
|
1854
|
+
cause: err
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
async bridge({ fromChain, toChain, fromToken, toToken, amount, recipient, quote }) {
|
|
1859
|
+
const from = this.config.walletClient?.account?.address ?? this.config.account?.address;
|
|
1860
|
+
if (!from) {
|
|
1861
|
+
throw new StableValidationError(
|
|
1862
|
+
"signer",
|
|
1863
|
+
void 0,
|
|
1864
|
+
"bridge: requires a signer \u2014 set walletClient or account in StableConfig"
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
const recipientAddr = recipient ?? from;
|
|
1868
|
+
this.validateBridgeParams({ fromChain, toChain, amount, recipient: recipientAddr }, "bridge");
|
|
1869
|
+
const { src, dst } = this.resolveChains(fromChain, toChain, fromToken, toToken);
|
|
1870
|
+
const oftAdapter = src.oftAdapter;
|
|
1871
|
+
const srcChain = makeChain(src.id, src.rpc);
|
|
1872
|
+
const wc = this.config.walletClient ?? createWalletClient4({
|
|
1873
|
+
account: this.config.account,
|
|
1874
|
+
chain: srcChain,
|
|
1875
|
+
transport: this.config.transport ?? http5(src.rpc)
|
|
1876
|
+
});
|
|
1877
|
+
const pc = createPublicClient5({ chain: srcChain, transport: http5(src.rpc) });
|
|
1878
|
+
try {
|
|
1879
|
+
await wc.switchChain({ id: src.id });
|
|
1880
|
+
} catch (err) {
|
|
1881
|
+
throw new StableTransactionError({
|
|
1882
|
+
phase: "switch_chain",
|
|
1883
|
+
shortMessage: `bridge: wallet rejected or failed to switch to chain ${src.id}`,
|
|
1884
|
+
chainId: src.id,
|
|
1885
|
+
cause: err
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
const amountLD = parseUnits4(String(amount), src.decimals);
|
|
1889
|
+
const sendParams = this.buildSendParams(dst, amountLD, recipientAddr);
|
|
1890
|
+
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 });
|
|
1891
|
+
const { nativeFee = 0n, lzTokenFee = 0n } = lzFee;
|
|
1892
|
+
const allowance = await pc.readContract({
|
|
1893
|
+
address: src.usdt,
|
|
1894
|
+
abi: erc20Abi4,
|
|
1895
|
+
functionName: "allowance",
|
|
1896
|
+
args: [from, oftAdapter]
|
|
1897
|
+
});
|
|
1898
|
+
if (allowance < amountLD) {
|
|
1899
|
+
let approveHash;
|
|
1900
|
+
try {
|
|
1901
|
+
approveHash = await wc.writeContract({
|
|
1902
|
+
address: src.usdt,
|
|
1903
|
+
abi: erc20Abi4,
|
|
1904
|
+
functionName: "approve",
|
|
1905
|
+
args: [oftAdapter, amountLD],
|
|
1906
|
+
account: wc.account,
|
|
1907
|
+
chain: srcChain
|
|
1908
|
+
});
|
|
1909
|
+
} catch (err) {
|
|
1910
|
+
throw new StableTransactionError({
|
|
1911
|
+
phase: "approve",
|
|
1912
|
+
shortMessage: "bridge: USDT0 approve failed",
|
|
1913
|
+
chainId: src.id,
|
|
1914
|
+
cause: err
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
const approveReceipt = await pc.waitForTransactionReceipt({ hash: approveHash });
|
|
1918
|
+
if (approveReceipt.status === "reverted") {
|
|
1919
|
+
throw new StableTransactionError({
|
|
1920
|
+
phase: "approve",
|
|
1921
|
+
shortMessage: "bridge: USDT0 approve reverted on-chain",
|
|
1922
|
+
txHash: approveReceipt.transactionHash,
|
|
1923
|
+
chainId: src.id
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
try {
|
|
1928
|
+
const hash = await wc.writeContract({
|
|
1929
|
+
address: oftAdapter,
|
|
1930
|
+
abi: OFT_ADAPTER_ABI,
|
|
1931
|
+
functionName: "send",
|
|
1932
|
+
args: [sendParams, { nativeFee, lzTokenFee }, from],
|
|
1933
|
+
value: nativeFee,
|
|
1934
|
+
account: wc.account,
|
|
1935
|
+
chain: srcChain
|
|
1936
|
+
});
|
|
1937
|
+
return { txHash: hash, toAmount: amount };
|
|
1938
|
+
} catch (err) {
|
|
1939
|
+
throw new StableTransactionError({
|
|
1940
|
+
phase: "send",
|
|
1941
|
+
shortMessage: "bridge: OFT send failed",
|
|
1942
|
+
chainId: src.id,
|
|
1943
|
+
cause: err
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
validateBridgeParams(params, method) {
|
|
1948
|
+
if (!isAddress4(params.recipient)) {
|
|
1949
|
+
throw new StableValidationError(
|
|
1950
|
+
"recipient",
|
|
1951
|
+
params.recipient,
|
|
1952
|
+
`${method}: invalid recipient address "${params.recipient}"`
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
if (!Number.isFinite(params.amount) || params.amount <= 0) {
|
|
1956
|
+
throw new StableValidationError(
|
|
1957
|
+
"amount",
|
|
1958
|
+
params.amount,
|
|
1959
|
+
`${method}: amount must be a finite positive number`
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
if (params.fromChain === params.toChain) {
|
|
1963
|
+
throw new StableValidationError(
|
|
1964
|
+
"toChain",
|
|
1965
|
+
params.toChain,
|
|
1966
|
+
`${method}: fromChain and toChain must differ`
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
resolveChains(fromChain, toChain, fromToken, toToken) {
|
|
1971
|
+
const src = BRIDGE_CHAIN_CONFIGS[fromChain];
|
|
1972
|
+
const dst = BRIDGE_CHAIN_CONFIGS[toChain];
|
|
1973
|
+
if (!src) throw new StableValidationError("fromChain", fromChain, `Unsupported source chain: "${fromChain}"`);
|
|
1974
|
+
if (!dst) throw new StableValidationError("toChain", toChain, `Unsupported destination chain: "${toChain}"`);
|
|
1975
|
+
if (!src.oftAdapter) {
|
|
1976
|
+
throw new StableValidationError("fromChain", fromChain, `No OFT adapter configured for "${fromChain}"`);
|
|
1977
|
+
}
|
|
1978
|
+
if (fromToken.toLowerCase() !== src.usdt.toLowerCase()) {
|
|
1979
|
+
throw new StableValidationError(
|
|
1980
|
+
"fromToken",
|
|
1981
|
+
fromToken,
|
|
1982
|
+
`USDT0 bridge only moves USDT0 \u2014 fromToken must be ${src.usdt} on "${fromChain}", got "${fromToken}". Use route "lifi" to bridge other tokens.`
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
if (toToken.toLowerCase() !== dst.usdt.toLowerCase()) {
|
|
1986
|
+
throw new StableValidationError(
|
|
1987
|
+
"toToken",
|
|
1988
|
+
toToken,
|
|
1989
|
+
`USDT0 bridge only moves USDT0 \u2014 toToken must be ${dst.usdt} on "${toChain}", got "${toToken}". Use route "lifi" to bridge other tokens.`
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
return { src, dst };
|
|
1993
|
+
}
|
|
1994
|
+
buildSendParams(dst, amountLD, recipient) {
|
|
1995
|
+
return {
|
|
1996
|
+
dstEid: dst.lzEid,
|
|
1997
|
+
to: padHex(recipient, { dir: "left", size: 32 }),
|
|
1998
|
+
amountLD,
|
|
1999
|
+
minAmountLD: amountLD,
|
|
2000
|
+
extraOptions: buildLzOptions(),
|
|
2001
|
+
composeMsg: "0x",
|
|
2002
|
+
oftCmd: "0x"
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
|
|
1328
2007
|
// src/stable.ts
|
|
2008
|
+
function isUsdt0Route(fromChain, toChain, fromToken, toToken) {
|
|
2009
|
+
const src = BRIDGE_CHAIN_CONFIGS[fromChain];
|
|
2010
|
+
const dst = BRIDGE_CHAIN_CONFIGS[toChain];
|
|
2011
|
+
return !!src && !!dst && !!src.oftAdapter && src.lzOutbound !== false && src.usdt.toLowerCase() === fromToken.toLowerCase() && dst.usdt.toLowerCase() === toToken.toLowerCase();
|
|
2012
|
+
}
|
|
2013
|
+
function selectBridge(params) {
|
|
2014
|
+
const route = params.route ?? "auto";
|
|
2015
|
+
if (route === "usdt0") return "usdt0";
|
|
2016
|
+
if (route === "lifi") return "lifi";
|
|
2017
|
+
return isUsdt0Route(
|
|
2018
|
+
params.fromChain,
|
|
2019
|
+
params.toChain,
|
|
2020
|
+
params.fromToken,
|
|
2021
|
+
params.toToken
|
|
2022
|
+
) ? "usdt0" : "lifi";
|
|
2023
|
+
}
|
|
1329
2024
|
function validateTransferParams({
|
|
1330
2025
|
to,
|
|
1331
2026
|
amount,
|
|
1332
2027
|
token,
|
|
1333
2028
|
tokenDecimals
|
|
1334
2029
|
}) {
|
|
1335
|
-
if (!
|
|
2030
|
+
if (!isAddress5(to)) {
|
|
1336
2031
|
throw new StableValidationError(
|
|
1337
2032
|
"to",
|
|
1338
2033
|
to,
|
|
@@ -1346,7 +2041,7 @@ function validateTransferParams({
|
|
|
1346
2041
|
"transfer: amount must be a finite positive number"
|
|
1347
2042
|
);
|
|
1348
2043
|
}
|
|
1349
|
-
if (token !== void 0 && !
|
|
2044
|
+
if (token !== void 0 && !isAddress5(token)) {
|
|
1350
2045
|
throw new StableValidationError(
|
|
1351
2046
|
"token",
|
|
1352
2047
|
token,
|
|
@@ -1366,7 +2061,7 @@ async function resolveTokenDecimals(publicClient, token, tokenDecimals) {
|
|
|
1366
2061
|
try {
|
|
1367
2062
|
return await publicClient.readContract({
|
|
1368
2063
|
address: token,
|
|
1369
|
-
abi:
|
|
2064
|
+
abi: erc20Abi5,
|
|
1370
2065
|
functionName: "decimals"
|
|
1371
2066
|
});
|
|
1372
2067
|
} catch {
|
|
@@ -1393,9 +2088,9 @@ async function executeTokenTransfer(wc, publicClient, token, to, amount, tokenDe
|
|
|
1393
2088
|
);
|
|
1394
2089
|
return wc.writeContract({
|
|
1395
2090
|
address: token,
|
|
1396
|
-
abi:
|
|
2091
|
+
abi: erc20Abi5,
|
|
1397
2092
|
functionName: "transfer",
|
|
1398
|
-
args: [to,
|
|
2093
|
+
args: [to, parseUnits5(String(amount), decimals)],
|
|
1399
2094
|
account: acc,
|
|
1400
2095
|
chain
|
|
1401
2096
|
});
|
|
@@ -1406,7 +2101,8 @@ function createStable({
|
|
|
1406
2101
|
account,
|
|
1407
2102
|
transport,
|
|
1408
2103
|
walletClient,
|
|
1409
|
-
earn: earnConfig
|
|
2104
|
+
earn: earnConfig,
|
|
2105
|
+
merklApiBase
|
|
1410
2106
|
}) {
|
|
1411
2107
|
const chain = network === "mainnet" /* Mainnet */ ? stableMainnet2 : stableTestnet2;
|
|
1412
2108
|
const rpc = (rpcOverride ?? chain.rpcUrls.default.http[0]).replace(
|
|
@@ -1415,6 +2111,8 @@ function createStable({
|
|
|
1415
2111
|
);
|
|
1416
2112
|
const signerConfig = { account, transport, walletClient };
|
|
1417
2113
|
const lifi = new LiFiService({ ...signerConfig, rpc, chainId: chain.id });
|
|
2114
|
+
const lz = new LayerZeroService(signerConfig);
|
|
2115
|
+
const merkl = new MerklService({ ...signerConfig, rpc, chainId: chain.id, merklApiBase });
|
|
1418
2116
|
const morpho = earnConfig ? new MorphoService({
|
|
1419
2117
|
...signerConfig,
|
|
1420
2118
|
rpc,
|
|
@@ -1431,14 +2129,25 @@ function createStable({
|
|
|
1431
2129
|
}
|
|
1432
2130
|
return morpho;
|
|
1433
2131
|
}
|
|
2132
|
+
function requireSignerAddress() {
|
|
2133
|
+
const from = walletClient?.account?.address ?? account?.address;
|
|
2134
|
+
if (!from) {
|
|
2135
|
+
throw new StableValidationError(
|
|
2136
|
+
"signer",
|
|
2137
|
+
void 0,
|
|
2138
|
+
"stable.rewards.*: requires a signer \u2014 set walletClient or account in StableConfig"
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
2141
|
+
return from;
|
|
2142
|
+
}
|
|
1434
2143
|
async function transfer(params) {
|
|
1435
2144
|
validateTransferParams(params);
|
|
1436
2145
|
const { from, to, amount, token, tokenDecimals } = params;
|
|
1437
2146
|
const acc = walletClient?.account ?? account ?? from;
|
|
1438
|
-
const wc = walletClient ??
|
|
2147
|
+
const wc = walletClient ?? createWalletClient5({
|
|
1439
2148
|
account: acc,
|
|
1440
2149
|
chain,
|
|
1441
|
-
transport: transport ??
|
|
2150
|
+
transport: transport ?? http6(rpc)
|
|
1442
2151
|
});
|
|
1443
2152
|
try {
|
|
1444
2153
|
await wc.switchChain({ id: chain.id });
|
|
@@ -1450,9 +2159,9 @@ function createStable({
|
|
|
1450
2159
|
cause: err
|
|
1451
2160
|
});
|
|
1452
2161
|
}
|
|
1453
|
-
const publicClient =
|
|
2162
|
+
const publicClient = createPublicClient6({
|
|
1454
2163
|
chain,
|
|
1455
|
-
transport:
|
|
2164
|
+
transport: http6(rpc)
|
|
1456
2165
|
});
|
|
1457
2166
|
const hash = token !== void 0 ? await executeTokenTransfer(
|
|
1458
2167
|
wc,
|
|
@@ -1470,7 +2179,7 @@ function createStable({
|
|
|
1470
2179
|
account: acc,
|
|
1471
2180
|
chain,
|
|
1472
2181
|
to,
|
|
1473
|
-
value:
|
|
2182
|
+
value: parseUnits5(String(amount), 18)
|
|
1474
2183
|
})
|
|
1475
2184
|
);
|
|
1476
2185
|
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
@@ -1486,8 +2195,8 @@ function createStable({
|
|
|
1486
2195
|
}
|
|
1487
2196
|
return {
|
|
1488
2197
|
transfer,
|
|
1489
|
-
quoteBridge: (params) => lifi.quoteBridge(params),
|
|
1490
|
-
bridge: (params) => lifi.bridge(params),
|
|
2198
|
+
quoteBridge: (params) => selectBridge(params) === "usdt0" ? lz.quoteBridge(params) : lifi.quoteBridge(params),
|
|
2199
|
+
bridge: (params) => selectBridge(params) === "usdt0" ? lz.bridge(params) : lifi.bridge(params),
|
|
1491
2200
|
quoteSwap: (params) => lifi.quoteSwap(params),
|
|
1492
2201
|
swap: (params) => lifi.swap(params),
|
|
1493
2202
|
earn: {
|
|
@@ -1497,26 +2206,40 @@ function createStable({
|
|
|
1497
2206
|
bulkWithdraw: (items, options) => requireEarn().bulkWithdraw(items, options),
|
|
1498
2207
|
redeem: (params) => requireEarn().redeem(params),
|
|
1499
2208
|
forceWithdraw: (params) => requireEarn().forceWithdraw(params),
|
|
1500
|
-
forceRedeem: (params) => requireEarn().forceRedeem(params)
|
|
2209
|
+
forceRedeem: (params) => requireEarn().forceRedeem(params),
|
|
2210
|
+
prepareDepositCalldata: (params) => requireEarn().prepareDepositCalldata(params),
|
|
2211
|
+
prepareWithdrawCalldata: (params) => requireEarn().prepareWithdrawCalldata(params),
|
|
2212
|
+
prepareRedeemCalldata: (params) => requireEarn().prepareRedeemCalldata(params),
|
|
2213
|
+
incentiveRewards: {
|
|
2214
|
+
fetch: () => merkl.fetchRewards(requireSignerAddress()),
|
|
2215
|
+
claim: () => merkl.claim()
|
|
2216
|
+
}
|
|
1501
2217
|
}
|
|
1502
2218
|
};
|
|
1503
2219
|
}
|
|
1504
2220
|
function createStableReader(config) {
|
|
1505
2221
|
const reader = new EarnReader(config);
|
|
2222
|
+
const chain = config.network === "testnet" /* Testnet */ ? stableTestnet2 : stableMainnet2;
|
|
2223
|
+
const rpc = (config.rpc ?? chain.rpcUrls.default.http[0]).replace(/\/$/, "");
|
|
2224
|
+
const merkl = new MerklService({ chainId: chain.id, rpc, merklApiBase: config.merklApiBase });
|
|
1506
2225
|
return {
|
|
1507
2226
|
earn: {
|
|
1508
2227
|
getYield: () => reader.getYield(),
|
|
1509
2228
|
position: () => reader.position(),
|
|
1510
2229
|
preview: (params) => reader.preview(params),
|
|
1511
|
-
withdrawability: (params) => reader.withdrawability(params)
|
|
2230
|
+
withdrawability: (params) => reader.withdrawability(params),
|
|
2231
|
+
incentiveRewards: {
|
|
2232
|
+
fetch: () => merkl.fetchRewards(config.address)
|
|
2233
|
+
}
|
|
1512
2234
|
}
|
|
1513
2235
|
};
|
|
1514
2236
|
}
|
|
1515
2237
|
|
|
1516
2238
|
// src/constants.ts
|
|
1517
2239
|
var STABLE_USDT_ADDRESS = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
|
|
1518
|
-
var STABLE_VAULT_ADDRESS = "
|
|
2240
|
+
var STABLE_VAULT_ADDRESS = "0xb7Df8db22A5DBBFA9ebeb94b3910aec6a4f05c08";
|
|
1519
2241
|
export {
|
|
2242
|
+
BRIDGE_CHAIN_CONFIGS,
|
|
1520
2243
|
CHAIN_CONFIGS,
|
|
1521
2244
|
Chain,
|
|
1522
2245
|
Network,
|