@agether/sdk 2.16.1 → 2.17.1
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/cli.js +270 -20
- package/dist/index.d.mts +76 -4
- package/dist/index.d.ts +76 -4
- package/dist/index.js +265 -20
- package/dist/index.mjs +265 -20
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -340,7 +340,7 @@ var init_MorphoClient = __esm({
|
|
|
340
340
|
MODE_BATCH = "0x0100000000000000000000000000000000000000000000000000000000000000";
|
|
341
341
|
morphoIface = new import_ethers.ethers.Interface(MORPHO_BLUE_ABI);
|
|
342
342
|
erc20Iface = new import_ethers.ethers.Interface(ERC20_ABI);
|
|
343
|
-
MorphoClient = class {
|
|
343
|
+
MorphoClient = class _MorphoClient {
|
|
344
344
|
constructor(config) {
|
|
345
345
|
/** Market params cache: keyed by market uniqueKey (bytes32 hash) */
|
|
346
346
|
this._marketCache = /* @__PURE__ */ new Map();
|
|
@@ -1716,9 +1716,17 @@ var init_MorphoClient = __esm({
|
|
|
1716
1716
|
const estimated = totalBorrowShares > 0n ? repayShares * totalBorrowAssets / totalBorrowShares + 10n : 0n;
|
|
1717
1717
|
approveAmount = estimated > 0n ? estimated : import_ethers.ethers.parseUnits("1", loanDecimals);
|
|
1718
1718
|
} else {
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1719
|
+
const marketId = import_ethers.ethers.keccak256(import_ethers.ethers.AbiCoder.defaultAbiCoder().encode(
|
|
1720
|
+
["address", "address", "address", "address", "uint256"],
|
|
1721
|
+
[params.loanToken, params.collateralToken, params.oracle, params.irm, params.lltv]
|
|
1722
|
+
));
|
|
1723
|
+
const pos = await this.morphoBlue.position(marketId, acctAddr);
|
|
1724
|
+
repayShares = BigInt(pos.borrowShares);
|
|
1725
|
+
repayAssets = 0n;
|
|
1726
|
+
const onChainMkt = await this.morphoBlue.market(marketId);
|
|
1727
|
+
const totalBorrowAssets = BigInt(onChainMkt.totalBorrowAssets);
|
|
1728
|
+
const totalBorrowShares = BigInt(onChainMkt.totalBorrowShares);
|
|
1729
|
+
approveAmount = totalBorrowShares > 0n ? repayShares * totalBorrowAssets / totalBorrowShares + 10n : import_ethers.ethers.parseUnits("1", loanDecimals);
|
|
1722
1730
|
}
|
|
1723
1731
|
} else {
|
|
1724
1732
|
repayAssets = import_ethers.ethers.parseUnits(amount, loanDecimals);
|
|
@@ -1727,20 +1735,32 @@ var init_MorphoClient = __esm({
|
|
|
1727
1735
|
}
|
|
1728
1736
|
const loanContract = new import_ethers.Contract(loanTokenAddr, ERC20_ABI, this._signer);
|
|
1729
1737
|
const acctBalance = await loanContract.balanceOf(acctAddr);
|
|
1730
|
-
|
|
1731
|
-
|
|
1738
|
+
const checkAmount = repayShares > 0n && repayAssets === 0n ? approveAmount * 1005n / 1000n : approveAmount;
|
|
1739
|
+
if (acctBalance < checkAmount) {
|
|
1740
|
+
const shortfall = checkAmount - acctBalance;
|
|
1732
1741
|
const eoaBalance = await loanContract.balanceOf(await this.getSignerAddress());
|
|
1733
1742
|
if (eoaBalance < shortfall) {
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1743
|
+
if (repayShares > 0n && acctBalance + eoaBalance > 0n) {
|
|
1744
|
+
if (eoaBalance > 0n) {
|
|
1745
|
+
const transferTx = await loanContract.transfer(acctAddr, eoaBalance);
|
|
1746
|
+
await transferTx.wait();
|
|
1747
|
+
this._refreshSigner();
|
|
1748
|
+
}
|
|
1749
|
+
const newBalance = await loanContract.balanceOf(acctAddr);
|
|
1750
|
+
approveAmount = newBalance;
|
|
1751
|
+
} else {
|
|
1752
|
+
const loanInfo = this._tokenCache.get(loanTokenAddr.toLowerCase());
|
|
1753
|
+
const loanSymbol = loanInfo?.symbol ?? "loan token";
|
|
1754
|
+
throw new AgetherError(
|
|
1755
|
+
`Insufficient ${loanSymbol} for repay. Need ~${import_ethers.ethers.formatUnits(checkAmount, loanDecimals)}, AgentAccount has ${import_ethers.ethers.formatUnits(acctBalance, loanDecimals)}, EOA has ${import_ethers.ethers.formatUnits(eoaBalance, loanDecimals)}.`,
|
|
1756
|
+
"INSUFFICIENT_BALANCE"
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
} else {
|
|
1760
|
+
const transferTx = await loanContract.transfer(acctAddr, shortfall);
|
|
1761
|
+
await transferTx.wait();
|
|
1762
|
+
this._refreshSigner();
|
|
1740
1763
|
}
|
|
1741
|
-
const transferTx = await loanContract.transfer(acctAddr, shortfall);
|
|
1742
|
-
await transferTx.wait();
|
|
1743
|
-
this._refreshSigner();
|
|
1744
1764
|
}
|
|
1745
1765
|
const targets = [loanTokenAddr, morphoAddr];
|
|
1746
1766
|
const values = [0n, 0n];
|
|
@@ -1757,20 +1777,250 @@ var init_MorphoClient = __esm({
|
|
|
1757
1777
|
const receipt = await this.batch(targets, values, datas);
|
|
1758
1778
|
let remainingDebt = "0";
|
|
1759
1779
|
try {
|
|
1760
|
-
const
|
|
1761
|
-
|
|
1780
|
+
const markets = await this.getMarkets(true);
|
|
1781
|
+
const mkt = markets.find(
|
|
1782
|
+
(m) => m.collateralAsset?.address.toLowerCase() === params.collateralToken.toLowerCase() && m.loanAsset?.address.toLowerCase() === params.loanToken.toLowerCase()
|
|
1783
|
+
);
|
|
1784
|
+
if (mkt) {
|
|
1785
|
+
const pos = await this.morphoBlue.position(mkt.uniqueKey, acctAddr);
|
|
1786
|
+
const shares = BigInt(pos.borrowShares);
|
|
1787
|
+
if (shares > 0n) {
|
|
1788
|
+
const onChainMkt = await this.morphoBlue.market(mkt.uniqueKey);
|
|
1789
|
+
const totalAssets = BigInt(onChainMkt.totalBorrowAssets);
|
|
1790
|
+
const totalShares = BigInt(onChainMkt.totalBorrowShares);
|
|
1791
|
+
const debtWei = totalShares > 0n ? shares * totalAssets / totalShares : 0n;
|
|
1792
|
+
remainingDebt = import_ethers.ethers.formatUnits(debtWei, loanDecimals);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1762
1795
|
} catch (e) {
|
|
1763
1796
|
console.warn("[agether] failed to read remaining debt after repay:", e instanceof Error ? e.message : e);
|
|
1764
1797
|
}
|
|
1765
1798
|
return { tx: receipt.hash, amount, remainingDebt };
|
|
1766
1799
|
}
|
|
1800
|
+
static {
|
|
1801
|
+
// ════════════════════════════════════════════════════════
|
|
1802
|
+
// Yield Spread Analysis & Leverage
|
|
1803
|
+
// ════════════════════════════════════════════════════════
|
|
1804
|
+
/**
|
|
1805
|
+
* Mapping from Morpho collateral token symbols to DeFi Llama project IDs.
|
|
1806
|
+
* Used to look up native staking/restaking yields from the DeFi Llama Yields API.
|
|
1807
|
+
*/
|
|
1808
|
+
this.LST_PROJECT_MAP = {
|
|
1809
|
+
"wstETH": { project: "lido", symbol: "STETH" },
|
|
1810
|
+
"cbETH": { project: "coinbase-wrapped-staked-eth", symbol: "CBETH" },
|
|
1811
|
+
"rETH": { project: "rocket-pool", symbol: "RETH" },
|
|
1812
|
+
"weETH": { project: "ether.fi-stake", symbol: "WEETH" },
|
|
1813
|
+
"ezETH": { project: "renzo", symbol: "EZETH" },
|
|
1814
|
+
"rsETH": { project: "kelp", symbol: "RSETH" },
|
|
1815
|
+
"swETH": { project: "swell-liquid-staking", symbol: "SWETH" },
|
|
1816
|
+
"mETH": { project: "meth-protocol", symbol: "METH" },
|
|
1817
|
+
"sfrxETH": { project: "frax-ether", symbol: "SFRXETH" },
|
|
1818
|
+
"oETH": { project: "origin-ether", symbol: "OETH" },
|
|
1819
|
+
"ETHx": { project: "stader", symbol: "ETHX" },
|
|
1820
|
+
"wBETH": { project: "binance-staked-eth", symbol: "WBETH" }
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
static {
|
|
1824
|
+
/** Cache for LST yields fetched from DeFi Llama. TTL: 30 minutes. */
|
|
1825
|
+
this._lstYieldCache = null;
|
|
1826
|
+
}
|
|
1827
|
+
static {
|
|
1828
|
+
this.LST_CACHE_TTL = 30 * 60 * 1e3;
|
|
1829
|
+
}
|
|
1830
|
+
// 30 min
|
|
1831
|
+
/**
|
|
1832
|
+
* Fetch native staking/restaking yields for all known LST/LRT tokens
|
|
1833
|
+
* from the DeFi Llama Yields API. Results are cached for 30 minutes.
|
|
1834
|
+
*
|
|
1835
|
+
* @returns Map of token symbol → APY (e.g. { wstETH: 4.56, cbETH: 3.61 })
|
|
1836
|
+
*/
|
|
1837
|
+
async _getLstYields() {
|
|
1838
|
+
if (_MorphoClient._lstYieldCache && Date.now() - _MorphoClient._lstYieldCache.ts < _MorphoClient.LST_CACHE_TTL) {
|
|
1839
|
+
return _MorphoClient._lstYieldCache.data;
|
|
1840
|
+
}
|
|
1841
|
+
const yields = {};
|
|
1842
|
+
try {
|
|
1843
|
+
const resp = await fetch("https://yields.llama.fi/pools", {
|
|
1844
|
+
headers: { "User-Agent": "agether-sdk/1.0" },
|
|
1845
|
+
signal: AbortSignal.timeout(1e4)
|
|
1846
|
+
});
|
|
1847
|
+
if (!resp.ok) {
|
|
1848
|
+
console.warn("[agether] DeFi Llama API returned", resp.status);
|
|
1849
|
+
return yields;
|
|
1850
|
+
}
|
|
1851
|
+
const data = await resp.json();
|
|
1852
|
+
const pools = data?.data ?? [];
|
|
1853
|
+
for (const [morphoSymbol, mapping] of Object.entries(_MorphoClient.LST_PROJECT_MAP)) {
|
|
1854
|
+
const matchingPools = pools.filter(
|
|
1855
|
+
(p) => p.project === mapping.project && p.symbol?.toUpperCase() === mapping.symbol && ["Ethereum", "Base"].includes(p.chain)
|
|
1856
|
+
);
|
|
1857
|
+
if (matchingPools.length > 0) {
|
|
1858
|
+
const best = matchingPools.reduce(
|
|
1859
|
+
(a, b) => (b.tvlUsd ?? 0) > (a.tvlUsd ?? 0) ? b : a
|
|
1860
|
+
);
|
|
1861
|
+
if (best.apy !== void 0 && best.apy > 0) {
|
|
1862
|
+
yields[morphoSymbol] = parseFloat(best.apy.toFixed(4));
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
const knownProjects = new Set(Object.values(_MorphoClient.LST_PROJECT_MAP).map((m) => m.project));
|
|
1867
|
+
const stakingProjects = pools.filter(
|
|
1868
|
+
(p) => p.symbol?.match(/^(ST|WST|CB|R|WE|EZ|RS|SW|M|OS|SFR|O|W?B)ETH$/i) && !knownProjects.has(p.project) && p.tvlUsd > 1e7 && p.apy > 0.5 && ["Ethereum", "Base"].includes(p.chain)
|
|
1869
|
+
);
|
|
1870
|
+
for (const p of stakingProjects) {
|
|
1871
|
+
const sym = p.symbol;
|
|
1872
|
+
if (!yields[sym]) {
|
|
1873
|
+
yields[sym] = parseFloat(p.apy.toFixed(4));
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
} catch (e) {
|
|
1877
|
+
console.warn("[agether] Failed to fetch LST yields:", e instanceof Error ? e.message : e);
|
|
1878
|
+
}
|
|
1879
|
+
_MorphoClient._lstYieldCache = { data: yields, ts: Date.now() };
|
|
1880
|
+
return yields;
|
|
1881
|
+
}
|
|
1767
1882
|
/**
|
|
1768
|
-
*
|
|
1883
|
+
* Analyze yield spread between LST/LRT collateral yield and Morpho borrow rates.
|
|
1884
|
+
* Positive spread = profitable carry trade (collateral earns more than debt costs).
|
|
1769
1885
|
*
|
|
1770
|
-
*
|
|
1886
|
+
* @returns Array of markets with yield analysis, sorted by net spread descending.
|
|
1887
|
+
*/
|
|
1888
|
+
async getYieldSpread() {
|
|
1889
|
+
const lstYields = await this._getLstYields();
|
|
1890
|
+
if (Object.keys(lstYields).length === 0) {
|
|
1891
|
+
console.warn("[agether] No LST yield data available \u2014 DeFi Llama may be unreachable");
|
|
1892
|
+
return [];
|
|
1893
|
+
}
|
|
1894
|
+
const lstTokens = Object.keys(lstYields);
|
|
1895
|
+
const allMarkets = [];
|
|
1896
|
+
for (const token of lstTokens) {
|
|
1897
|
+
try {
|
|
1898
|
+
const markets2 = await this.getMarketRates(token);
|
|
1899
|
+
allMarkets.push(...markets2);
|
|
1900
|
+
} catch {
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1904
|
+
const markets = allMarkets.filter((m) => {
|
|
1905
|
+
if (seen.has(m.marketId)) return false;
|
|
1906
|
+
seen.add(m.marketId);
|
|
1907
|
+
return true;
|
|
1908
|
+
});
|
|
1909
|
+
const results = [];
|
|
1910
|
+
for (const mkt of markets) {
|
|
1911
|
+
const collateralYield = lstYields[mkt.collateralToken];
|
|
1912
|
+
if (collateralYield === void 0) continue;
|
|
1913
|
+
const borrowRate = mkt.borrowApy;
|
|
1914
|
+
const netSpread = collateralYield - borrowRate;
|
|
1915
|
+
const lltv = parseFloat(mkt.lltv) / 100;
|
|
1916
|
+
const safeLtv = lltv * 0.8;
|
|
1917
|
+
const maxLeverage = 1 / (1 - safeLtv);
|
|
1918
|
+
const leveragedNetApy = collateralYield * maxLeverage - borrowRate * (maxLeverage - 1);
|
|
1919
|
+
results.push({
|
|
1920
|
+
collateralToken: mkt.collateralToken,
|
|
1921
|
+
loanToken: mkt.loanToken,
|
|
1922
|
+
collateralYield,
|
|
1923
|
+
borrowRate,
|
|
1924
|
+
netSpread,
|
|
1925
|
+
profitable: netSpread > 0,
|
|
1926
|
+
lltv,
|
|
1927
|
+
maxSafeLeverage: parseFloat(maxLeverage.toFixed(2)),
|
|
1928
|
+
leveragedNetApy: parseFloat(leveragedNetApy.toFixed(2)),
|
|
1929
|
+
liquidity: mkt.totalSupplyUsd - mkt.totalBorrowUsd,
|
|
1930
|
+
marketId: mkt.marketId
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
return results.sort((a, b) => b.netSpread - a.netSpread);
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Execute a leverage loop: deposit collateral → borrow → swap to collateral → deposit again.
|
|
1937
|
+
* Repeats N times to achieve target leverage.
|
|
1938
|
+
*
|
|
1939
|
+
* Example: 1 wstETH at 3x leverage = deposit 1 → borrow → swap to ~0.77 wstETH → deposit →
|
|
1940
|
+
* borrow → swap to ~0.6 wstETH → deposit. Total exposure: ~2.37 wstETH, debt: ~1.37 ETH worth.
|
|
1771
1941
|
*
|
|
1772
|
-
* @param
|
|
1942
|
+
* @param collateralToken - LST token to deposit (e.g. 'wstETH')
|
|
1943
|
+
* @param amount - Initial deposit amount
|
|
1944
|
+
* @param targetLeverage - Target leverage multiplier (e.g. 2.0 for 2x)
|
|
1945
|
+
* @param loanToken - Token to borrow (default: inferred from market)
|
|
1946
|
+
* @param maxIterations - Max loop iterations (default: 5, safety cap)
|
|
1773
1947
|
*/
|
|
1948
|
+
async leverageLoop(collateralToken, amount, targetLeverage, loanToken, maxIterations = 5) {
|
|
1949
|
+
if (targetLeverage < 1.1 || targetLeverage > 10) {
|
|
1950
|
+
throw new AgetherError(
|
|
1951
|
+
"Target leverage must be between 1.1x and 10x",
|
|
1952
|
+
"INVALID_LEVERAGE"
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
const spreads = await this.getYieldSpread();
|
|
1956
|
+
const matchingMarket = spreads.find(
|
|
1957
|
+
(s) => s.collateralToken.toLowerCase() === collateralToken.toLowerCase() && (!loanToken || s.loanToken.toLowerCase() === loanToken.toLowerCase())
|
|
1958
|
+
);
|
|
1959
|
+
if (!matchingMarket) {
|
|
1960
|
+
throw new AgetherError(
|
|
1961
|
+
`No LST market found for ${collateralToken}`,
|
|
1962
|
+
"MARKET_NOT_FOUND"
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
if (!matchingMarket) {
|
|
1966
|
+
throw new AgetherError(
|
|
1967
|
+
`No yield data available for ${collateralToken}. Ensure DeFi Llama API is reachable.`,
|
|
1968
|
+
"NO_YIELD_DATA"
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
if (targetLeverage > matchingMarket.maxSafeLeverage) {
|
|
1972
|
+
throw new AgetherError(
|
|
1973
|
+
`Target leverage ${targetLeverage}x exceeds max safe leverage ${matchingMarket.maxSafeLeverage}x for ${collateralToken}. Max LLTV is ${(matchingMarket.lltv * 100).toFixed(0)}%.`,
|
|
1974
|
+
"LEVERAGE_TOO_HIGH"
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
const initialAmount = parseFloat(amount);
|
|
1978
|
+
const iterations = [];
|
|
1979
|
+
let totalCollateral = initialAmount;
|
|
1980
|
+
let totalDebt = 0;
|
|
1981
|
+
let currentAmount = initialAmount;
|
|
1982
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
1983
|
+
const currentLeverage = totalDebt > 0 ? totalCollateral / (totalCollateral - totalDebt) : 1;
|
|
1984
|
+
if (currentLeverage >= targetLeverage * 0.98) break;
|
|
1985
|
+
const borrowAmount = currentAmount * matchingMarket.lltv * 0.8;
|
|
1986
|
+
const swapOutput = borrowAmount * 0.997;
|
|
1987
|
+
iterations.push({
|
|
1988
|
+
iteration: i + 1,
|
|
1989
|
+
deposit: currentAmount,
|
|
1990
|
+
borrow: borrowAmount,
|
|
1991
|
+
swapOutput
|
|
1992
|
+
});
|
|
1993
|
+
totalDebt += borrowAmount;
|
|
1994
|
+
totalCollateral += swapOutput;
|
|
1995
|
+
currentAmount = swapOutput;
|
|
1996
|
+
}
|
|
1997
|
+
const finalLeverage = totalDebt > 0 ? totalCollateral / (totalCollateral - totalDebt) : 1;
|
|
1998
|
+
return {
|
|
1999
|
+
strategy: "leverage_loop",
|
|
2000
|
+
collateralToken: matchingMarket.collateralToken,
|
|
2001
|
+
loanToken: matchingMarket.loanToken,
|
|
2002
|
+
initialDeposit: amount,
|
|
2003
|
+
targetLeverage,
|
|
2004
|
+
achievedLeverage: parseFloat(finalLeverage.toFixed(2)),
|
|
2005
|
+
totalCollateral: parseFloat(totalCollateral.toFixed(6)),
|
|
2006
|
+
totalDebt: parseFloat(totalDebt.toFixed(6)),
|
|
2007
|
+
iterations,
|
|
2008
|
+
estimatedNetApy: parseFloat(
|
|
2009
|
+
(matchingMarket.collateralYield * finalLeverage - matchingMarket.borrowRate * (finalLeverage - 1)).toFixed(2)
|
|
2010
|
+
),
|
|
2011
|
+
healthFactor: parseFloat(
|
|
2012
|
+
(totalCollateral * matchingMarket.lltv / totalDebt).toFixed(2)
|
|
2013
|
+
),
|
|
2014
|
+
warning: "This is a simulation. Actual execution requires DEX swap integration. Real results may differ due to slippage and price movements."
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Withdraw collateral from Morpho Blue.
|
|
2019
|
+
*
|
|
2020
|
+
* AgentAccount.execute: Morpho.withdrawCollateral(params, amount, account, receiver)
|
|
2021
|
+
*
|
|
2022
|
+
* @param receiver - defaults to EOA wallet
|
|
2023
|
+
*/
|
|
1774
2024
|
async withdrawCollateral(tokenSymbol, amount, marketParams, receiver) {
|
|
1775
2025
|
const acctAddr = await this.getAccountAddress();
|
|
1776
2026
|
const colInfo = await this._resolveToken(tokenSymbol);
|
package/dist/index.d.mts
CHANGED
|
@@ -569,6 +569,42 @@ interface PayFromYieldResult {
|
|
|
569
569
|
remainingYield: string;
|
|
570
570
|
remainingSupply: string;
|
|
571
571
|
}
|
|
572
|
+
/** Yield spread analysis result for a single market. */
|
|
573
|
+
interface YieldSpreadResult {
|
|
574
|
+
collateralToken: string;
|
|
575
|
+
loanToken: string;
|
|
576
|
+
collateralYield: number;
|
|
577
|
+
borrowRate: number;
|
|
578
|
+
netSpread: number;
|
|
579
|
+
profitable: boolean;
|
|
580
|
+
lltv: number;
|
|
581
|
+
maxSafeLeverage: number;
|
|
582
|
+
leveragedNetApy: number;
|
|
583
|
+
liquidity: number;
|
|
584
|
+
marketId: string;
|
|
585
|
+
}
|
|
586
|
+
/** Single iteration in a leverage loop. */
|
|
587
|
+
interface LeverageIteration {
|
|
588
|
+
iteration: number;
|
|
589
|
+
deposit: number;
|
|
590
|
+
borrow: number;
|
|
591
|
+
swapOutput: number;
|
|
592
|
+
}
|
|
593
|
+
/** Result of a leverage loop simulation. */
|
|
594
|
+
interface LeverageResult {
|
|
595
|
+
strategy: string;
|
|
596
|
+
collateralToken: string;
|
|
597
|
+
loanToken: string;
|
|
598
|
+
initialDeposit: string;
|
|
599
|
+
targetLeverage: number;
|
|
600
|
+
achievedLeverage: number;
|
|
601
|
+
totalCollateral: number;
|
|
602
|
+
totalDebt: number;
|
|
603
|
+
iterations: LeverageIteration[];
|
|
604
|
+
estimatedNetApy: number;
|
|
605
|
+
healthFactor: number;
|
|
606
|
+
warning: string;
|
|
607
|
+
}
|
|
572
608
|
declare class MorphoClient {
|
|
573
609
|
private _signer;
|
|
574
610
|
private provider;
|
|
@@ -898,12 +934,48 @@ declare class MorphoClient {
|
|
|
898
934
|
*/
|
|
899
935
|
repay(amount: string, tokenSymbol?: string, marketParams?: MorphoMarketParams, loanTokenSymbol?: string): Promise<RepayResult>;
|
|
900
936
|
/**
|
|
901
|
-
*
|
|
937
|
+
* Mapping from Morpho collateral token symbols to DeFi Llama project IDs.
|
|
938
|
+
* Used to look up native staking/restaking yields from the DeFi Llama Yields API.
|
|
939
|
+
*/
|
|
940
|
+
private static readonly LST_PROJECT_MAP;
|
|
941
|
+
/** Cache for LST yields fetched from DeFi Llama. TTL: 30 minutes. */
|
|
942
|
+
private static _lstYieldCache;
|
|
943
|
+
private static readonly LST_CACHE_TTL;
|
|
944
|
+
/**
|
|
945
|
+
* Fetch native staking/restaking yields for all known LST/LRT tokens
|
|
946
|
+
* from the DeFi Llama Yields API. Results are cached for 30 minutes.
|
|
947
|
+
*
|
|
948
|
+
* @returns Map of token symbol → APY (e.g. { wstETH: 4.56, cbETH: 3.61 })
|
|
949
|
+
*/
|
|
950
|
+
private _getLstYields;
|
|
951
|
+
/**
|
|
952
|
+
* Analyze yield spread between LST/LRT collateral yield and Morpho borrow rates.
|
|
953
|
+
* Positive spread = profitable carry trade (collateral earns more than debt costs).
|
|
902
954
|
*
|
|
903
|
-
*
|
|
955
|
+
* @returns Array of markets with yield analysis, sorted by net spread descending.
|
|
956
|
+
*/
|
|
957
|
+
getYieldSpread(): Promise<YieldSpreadResult[]>;
|
|
958
|
+
/**
|
|
959
|
+
* Execute a leverage loop: deposit collateral → borrow → swap to collateral → deposit again.
|
|
960
|
+
* Repeats N times to achieve target leverage.
|
|
904
961
|
*
|
|
905
|
-
*
|
|
962
|
+
* Example: 1 wstETH at 3x leverage = deposit 1 → borrow → swap to ~0.77 wstETH → deposit →
|
|
963
|
+
* borrow → swap to ~0.6 wstETH → deposit. Total exposure: ~2.37 wstETH, debt: ~1.37 ETH worth.
|
|
964
|
+
*
|
|
965
|
+
* @param collateralToken - LST token to deposit (e.g. 'wstETH')
|
|
966
|
+
* @param amount - Initial deposit amount
|
|
967
|
+
* @param targetLeverage - Target leverage multiplier (e.g. 2.0 for 2x)
|
|
968
|
+
* @param loanToken - Token to borrow (default: inferred from market)
|
|
969
|
+
* @param maxIterations - Max loop iterations (default: 5, safety cap)
|
|
906
970
|
*/
|
|
971
|
+
leverageLoop(collateralToken: string, amount: string, targetLeverage: number, loanToken?: string, maxIterations?: number): Promise<LeverageResult>;
|
|
972
|
+
/**
|
|
973
|
+
* Withdraw collateral from Morpho Blue.
|
|
974
|
+
*
|
|
975
|
+
* AgentAccount.execute: Morpho.withdrawCollateral(params, amount, account, receiver)
|
|
976
|
+
*
|
|
977
|
+
* @param receiver - defaults to EOA wallet
|
|
978
|
+
*/
|
|
907
979
|
withdrawCollateral(tokenSymbol: string, amount: string, marketParams?: MorphoMarketParams, receiver?: string): Promise<WithdrawResult>;
|
|
908
980
|
/**
|
|
909
981
|
* Refresh the signer and re-bind contract instances.
|
|
@@ -1597,4 +1669,4 @@ interface RetryOptions {
|
|
|
1597
1669
|
*/
|
|
1598
1670
|
declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
1599
1671
|
|
|
1600
|
-
export { ACCOUNT_FACTORY_ABI, AGENT_REPUTATION_ABI, AGETHER_4337_FACTORY_ABI, AGETHER_8004_SCORER_ABI, AGETHER_8004_VALIDATION_MODULE_ABI, AGETHER_HOOK_MULTIPLEXER_ABI, AgentIdentityClient, type AgentIdentityClientOptions, AgentNotApprovedError, AgetherClient, type AgetherClientOptions, type AgetherConfig, AgetherError, type AgetherSigner, type AgetherViemWallet, type BalancesResult, type BorrowResult, ChainId, type ContractAddresses, type DepositAndBorrowResult, type DepositResult, ENTRYPOINT_V07_ABI, ERC20_ABI, ERC8004_VALIDATION_MODULE_ABI, HOOK_MULTIPLEXER_ABI, IDENTITY_REGISTRY_ABI, InsufficientBalanceError, MORPHO_BLUE_ABI, type MarketFilter, MorphoClient, type MorphoClientConfig, type MorphoMarketInfo, type MorphoMarketParams, type MorphoPosition, type PayFromYieldResult, type PaymentRequirements, type PositionResult, type RegisterResult, type RepayResult, type RetryOptions, SAFE7579_ACCOUNT_ABI, SAFE_AGENT_FACTORY_ABI, type ScoreAttestation, type ScoreResult, ScoringClient, type ScoringClientConfig, ScoringRejectedError, type SpendingTracker, type StatusResult, type SupplyAssetResult, type SupplyPositionResult, type TransactionResult, VALIDATION_REGISTRY_ABI, type WithdrawFromAccountResult, type WithdrawResult, type WithdrawSupplyResult, X402Client, type X402Config, type X402PaymentRequest, type X402PaymentResult, type X402Response, bpsToRate, createConfig, formatAPR, formatAddress, formatHealthFactor, formatPercent, formatTimestamp, formatUSD, formatUnits, getContractAddresses, getDefaultConfig, getMorphoBlueAddress, getUSDCAddress, parseUnits, rateToBps, withRetry };
|
|
1672
|
+
export { ACCOUNT_FACTORY_ABI, AGENT_REPUTATION_ABI, AGETHER_4337_FACTORY_ABI, AGETHER_8004_SCORER_ABI, AGETHER_8004_VALIDATION_MODULE_ABI, AGETHER_HOOK_MULTIPLEXER_ABI, AgentIdentityClient, type AgentIdentityClientOptions, AgentNotApprovedError, AgetherClient, type AgetherClientOptions, type AgetherConfig, AgetherError, type AgetherSigner, type AgetherViemWallet, type BalancesResult, type BorrowResult, ChainId, type ContractAddresses, type DepositAndBorrowResult, type DepositResult, ENTRYPOINT_V07_ABI, ERC20_ABI, ERC8004_VALIDATION_MODULE_ABI, HOOK_MULTIPLEXER_ABI, IDENTITY_REGISTRY_ABI, InsufficientBalanceError, type LeverageIteration, type LeverageResult, MORPHO_BLUE_ABI, type MarketFilter, MorphoClient, type MorphoClientConfig, type MorphoMarketInfo, type MorphoMarketParams, type MorphoPosition, type PayFromYieldResult, type PaymentRequirements, type PositionResult, type RegisterResult, type RepayResult, type RetryOptions, SAFE7579_ACCOUNT_ABI, SAFE_AGENT_FACTORY_ABI, type ScoreAttestation, type ScoreResult, ScoringClient, type ScoringClientConfig, ScoringRejectedError, type SpendingTracker, type StatusResult, type SupplyAssetResult, type SupplyPositionResult, type TransactionResult, VALIDATION_REGISTRY_ABI, type WithdrawFromAccountResult, type WithdrawResult, type WithdrawSupplyResult, X402Client, type X402Config, type X402PaymentRequest, type X402PaymentResult, type X402Response, type YieldSpreadResult, bpsToRate, createConfig, formatAPR, formatAddress, formatHealthFactor, formatPercent, formatTimestamp, formatUSD, formatUnits, getContractAddresses, getDefaultConfig, getMorphoBlueAddress, getUSDCAddress, parseUnits, rateToBps, withRetry };
|
package/dist/index.d.ts
CHANGED
|
@@ -569,6 +569,42 @@ interface PayFromYieldResult {
|
|
|
569
569
|
remainingYield: string;
|
|
570
570
|
remainingSupply: string;
|
|
571
571
|
}
|
|
572
|
+
/** Yield spread analysis result for a single market. */
|
|
573
|
+
interface YieldSpreadResult {
|
|
574
|
+
collateralToken: string;
|
|
575
|
+
loanToken: string;
|
|
576
|
+
collateralYield: number;
|
|
577
|
+
borrowRate: number;
|
|
578
|
+
netSpread: number;
|
|
579
|
+
profitable: boolean;
|
|
580
|
+
lltv: number;
|
|
581
|
+
maxSafeLeverage: number;
|
|
582
|
+
leveragedNetApy: number;
|
|
583
|
+
liquidity: number;
|
|
584
|
+
marketId: string;
|
|
585
|
+
}
|
|
586
|
+
/** Single iteration in a leverage loop. */
|
|
587
|
+
interface LeverageIteration {
|
|
588
|
+
iteration: number;
|
|
589
|
+
deposit: number;
|
|
590
|
+
borrow: number;
|
|
591
|
+
swapOutput: number;
|
|
592
|
+
}
|
|
593
|
+
/** Result of a leverage loop simulation. */
|
|
594
|
+
interface LeverageResult {
|
|
595
|
+
strategy: string;
|
|
596
|
+
collateralToken: string;
|
|
597
|
+
loanToken: string;
|
|
598
|
+
initialDeposit: string;
|
|
599
|
+
targetLeverage: number;
|
|
600
|
+
achievedLeverage: number;
|
|
601
|
+
totalCollateral: number;
|
|
602
|
+
totalDebt: number;
|
|
603
|
+
iterations: LeverageIteration[];
|
|
604
|
+
estimatedNetApy: number;
|
|
605
|
+
healthFactor: number;
|
|
606
|
+
warning: string;
|
|
607
|
+
}
|
|
572
608
|
declare class MorphoClient {
|
|
573
609
|
private _signer;
|
|
574
610
|
private provider;
|
|
@@ -898,12 +934,48 @@ declare class MorphoClient {
|
|
|
898
934
|
*/
|
|
899
935
|
repay(amount: string, tokenSymbol?: string, marketParams?: MorphoMarketParams, loanTokenSymbol?: string): Promise<RepayResult>;
|
|
900
936
|
/**
|
|
901
|
-
*
|
|
937
|
+
* Mapping from Morpho collateral token symbols to DeFi Llama project IDs.
|
|
938
|
+
* Used to look up native staking/restaking yields from the DeFi Llama Yields API.
|
|
939
|
+
*/
|
|
940
|
+
private static readonly LST_PROJECT_MAP;
|
|
941
|
+
/** Cache for LST yields fetched from DeFi Llama. TTL: 30 minutes. */
|
|
942
|
+
private static _lstYieldCache;
|
|
943
|
+
private static readonly LST_CACHE_TTL;
|
|
944
|
+
/**
|
|
945
|
+
* Fetch native staking/restaking yields for all known LST/LRT tokens
|
|
946
|
+
* from the DeFi Llama Yields API. Results are cached for 30 minutes.
|
|
947
|
+
*
|
|
948
|
+
* @returns Map of token symbol → APY (e.g. { wstETH: 4.56, cbETH: 3.61 })
|
|
949
|
+
*/
|
|
950
|
+
private _getLstYields;
|
|
951
|
+
/**
|
|
952
|
+
* Analyze yield spread between LST/LRT collateral yield and Morpho borrow rates.
|
|
953
|
+
* Positive spread = profitable carry trade (collateral earns more than debt costs).
|
|
902
954
|
*
|
|
903
|
-
*
|
|
955
|
+
* @returns Array of markets with yield analysis, sorted by net spread descending.
|
|
956
|
+
*/
|
|
957
|
+
getYieldSpread(): Promise<YieldSpreadResult[]>;
|
|
958
|
+
/**
|
|
959
|
+
* Execute a leverage loop: deposit collateral → borrow → swap to collateral → deposit again.
|
|
960
|
+
* Repeats N times to achieve target leverage.
|
|
904
961
|
*
|
|
905
|
-
*
|
|
962
|
+
* Example: 1 wstETH at 3x leverage = deposit 1 → borrow → swap to ~0.77 wstETH → deposit →
|
|
963
|
+
* borrow → swap to ~0.6 wstETH → deposit. Total exposure: ~2.37 wstETH, debt: ~1.37 ETH worth.
|
|
964
|
+
*
|
|
965
|
+
* @param collateralToken - LST token to deposit (e.g. 'wstETH')
|
|
966
|
+
* @param amount - Initial deposit amount
|
|
967
|
+
* @param targetLeverage - Target leverage multiplier (e.g. 2.0 for 2x)
|
|
968
|
+
* @param loanToken - Token to borrow (default: inferred from market)
|
|
969
|
+
* @param maxIterations - Max loop iterations (default: 5, safety cap)
|
|
906
970
|
*/
|
|
971
|
+
leverageLoop(collateralToken: string, amount: string, targetLeverage: number, loanToken?: string, maxIterations?: number): Promise<LeverageResult>;
|
|
972
|
+
/**
|
|
973
|
+
* Withdraw collateral from Morpho Blue.
|
|
974
|
+
*
|
|
975
|
+
* AgentAccount.execute: Morpho.withdrawCollateral(params, amount, account, receiver)
|
|
976
|
+
*
|
|
977
|
+
* @param receiver - defaults to EOA wallet
|
|
978
|
+
*/
|
|
907
979
|
withdrawCollateral(tokenSymbol: string, amount: string, marketParams?: MorphoMarketParams, receiver?: string): Promise<WithdrawResult>;
|
|
908
980
|
/**
|
|
909
981
|
* Refresh the signer and re-bind contract instances.
|
|
@@ -1597,4 +1669,4 @@ interface RetryOptions {
|
|
|
1597
1669
|
*/
|
|
1598
1670
|
declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
1599
1671
|
|
|
1600
|
-
export { ACCOUNT_FACTORY_ABI, AGENT_REPUTATION_ABI, AGETHER_4337_FACTORY_ABI, AGETHER_8004_SCORER_ABI, AGETHER_8004_VALIDATION_MODULE_ABI, AGETHER_HOOK_MULTIPLEXER_ABI, AgentIdentityClient, type AgentIdentityClientOptions, AgentNotApprovedError, AgetherClient, type AgetherClientOptions, type AgetherConfig, AgetherError, type AgetherSigner, type AgetherViemWallet, type BalancesResult, type BorrowResult, ChainId, type ContractAddresses, type DepositAndBorrowResult, type DepositResult, ENTRYPOINT_V07_ABI, ERC20_ABI, ERC8004_VALIDATION_MODULE_ABI, HOOK_MULTIPLEXER_ABI, IDENTITY_REGISTRY_ABI, InsufficientBalanceError, MORPHO_BLUE_ABI, type MarketFilter, MorphoClient, type MorphoClientConfig, type MorphoMarketInfo, type MorphoMarketParams, type MorphoPosition, type PayFromYieldResult, type PaymentRequirements, type PositionResult, type RegisterResult, type RepayResult, type RetryOptions, SAFE7579_ACCOUNT_ABI, SAFE_AGENT_FACTORY_ABI, type ScoreAttestation, type ScoreResult, ScoringClient, type ScoringClientConfig, ScoringRejectedError, type SpendingTracker, type StatusResult, type SupplyAssetResult, type SupplyPositionResult, type TransactionResult, VALIDATION_REGISTRY_ABI, type WithdrawFromAccountResult, type WithdrawResult, type WithdrawSupplyResult, X402Client, type X402Config, type X402PaymentRequest, type X402PaymentResult, type X402Response, bpsToRate, createConfig, formatAPR, formatAddress, formatHealthFactor, formatPercent, formatTimestamp, formatUSD, formatUnits, getContractAddresses, getDefaultConfig, getMorphoBlueAddress, getUSDCAddress, parseUnits, rateToBps, withRetry };
|
|
1672
|
+
export { ACCOUNT_FACTORY_ABI, AGENT_REPUTATION_ABI, AGETHER_4337_FACTORY_ABI, AGETHER_8004_SCORER_ABI, AGETHER_8004_VALIDATION_MODULE_ABI, AGETHER_HOOK_MULTIPLEXER_ABI, AgentIdentityClient, type AgentIdentityClientOptions, AgentNotApprovedError, AgetherClient, type AgetherClientOptions, type AgetherConfig, AgetherError, type AgetherSigner, type AgetherViemWallet, type BalancesResult, type BorrowResult, ChainId, type ContractAddresses, type DepositAndBorrowResult, type DepositResult, ENTRYPOINT_V07_ABI, ERC20_ABI, ERC8004_VALIDATION_MODULE_ABI, HOOK_MULTIPLEXER_ABI, IDENTITY_REGISTRY_ABI, InsufficientBalanceError, type LeverageIteration, type LeverageResult, MORPHO_BLUE_ABI, type MarketFilter, MorphoClient, type MorphoClientConfig, type MorphoMarketInfo, type MorphoMarketParams, type MorphoPosition, type PayFromYieldResult, type PaymentRequirements, type PositionResult, type RegisterResult, type RepayResult, type RetryOptions, SAFE7579_ACCOUNT_ABI, SAFE_AGENT_FACTORY_ABI, type ScoreAttestation, type ScoreResult, ScoringClient, type ScoringClientConfig, ScoringRejectedError, type SpendingTracker, type StatusResult, type SupplyAssetResult, type SupplyPositionResult, type TransactionResult, VALIDATION_REGISTRY_ABI, type WithdrawFromAccountResult, type WithdrawResult, type WithdrawSupplyResult, X402Client, type X402Config, type X402PaymentRequest, type X402PaymentResult, type X402Response, type YieldSpreadResult, bpsToRate, createConfig, formatAPR, formatAddress, formatHealthFactor, formatPercent, formatTimestamp, formatUSD, formatUnits, getContractAddresses, getDefaultConfig, getMorphoBlueAddress, getUSDCAddress, parseUnits, rateToBps, withRetry };
|
package/dist/index.js
CHANGED
|
@@ -1315,7 +1315,7 @@ var MODE_SINGLE2 = "0x0000000000000000000000000000000000000000000000000000000000
|
|
|
1315
1315
|
var MODE_BATCH = "0x0100000000000000000000000000000000000000000000000000000000000000";
|
|
1316
1316
|
var morphoIface = new import_ethers2.ethers.Interface(MORPHO_BLUE_ABI);
|
|
1317
1317
|
var erc20Iface2 = new import_ethers2.ethers.Interface(ERC20_ABI);
|
|
1318
|
-
var
|
|
1318
|
+
var _MorphoClient = class _MorphoClient {
|
|
1319
1319
|
constructor(config) {
|
|
1320
1320
|
/** Market params cache: keyed by market uniqueKey (bytes32 hash) */
|
|
1321
1321
|
this._marketCache = /* @__PURE__ */ new Map();
|
|
@@ -2691,9 +2691,17 @@ var MorphoClient = class {
|
|
|
2691
2691
|
const estimated = totalBorrowShares > 0n ? repayShares * totalBorrowAssets / totalBorrowShares + 10n : 0n;
|
|
2692
2692
|
approveAmount = estimated > 0n ? estimated : import_ethers2.ethers.parseUnits("1", loanDecimals);
|
|
2693
2693
|
} else {
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2694
|
+
const marketId = import_ethers2.ethers.keccak256(import_ethers2.ethers.AbiCoder.defaultAbiCoder().encode(
|
|
2695
|
+
["address", "address", "address", "address", "uint256"],
|
|
2696
|
+
[params.loanToken, params.collateralToken, params.oracle, params.irm, params.lltv]
|
|
2697
|
+
));
|
|
2698
|
+
const pos = await this.morphoBlue.position(marketId, acctAddr);
|
|
2699
|
+
repayShares = BigInt(pos.borrowShares);
|
|
2700
|
+
repayAssets = 0n;
|
|
2701
|
+
const onChainMkt = await this.morphoBlue.market(marketId);
|
|
2702
|
+
const totalBorrowAssets = BigInt(onChainMkt.totalBorrowAssets);
|
|
2703
|
+
const totalBorrowShares = BigInt(onChainMkt.totalBorrowShares);
|
|
2704
|
+
approveAmount = totalBorrowShares > 0n ? repayShares * totalBorrowAssets / totalBorrowShares + 10n : import_ethers2.ethers.parseUnits("1", loanDecimals);
|
|
2697
2705
|
}
|
|
2698
2706
|
} else {
|
|
2699
2707
|
repayAssets = import_ethers2.ethers.parseUnits(amount, loanDecimals);
|
|
@@ -2702,20 +2710,32 @@ var MorphoClient = class {
|
|
|
2702
2710
|
}
|
|
2703
2711
|
const loanContract = new import_ethers2.Contract(loanTokenAddr, ERC20_ABI, this._signer);
|
|
2704
2712
|
const acctBalance = await loanContract.balanceOf(acctAddr);
|
|
2705
|
-
|
|
2706
|
-
|
|
2713
|
+
const checkAmount = repayShares > 0n && repayAssets === 0n ? approveAmount * 1005n / 1000n : approveAmount;
|
|
2714
|
+
if (acctBalance < checkAmount) {
|
|
2715
|
+
const shortfall = checkAmount - acctBalance;
|
|
2707
2716
|
const eoaBalance = await loanContract.balanceOf(await this.getSignerAddress());
|
|
2708
2717
|
if (eoaBalance < shortfall) {
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2718
|
+
if (repayShares > 0n && acctBalance + eoaBalance > 0n) {
|
|
2719
|
+
if (eoaBalance > 0n) {
|
|
2720
|
+
const transferTx = await loanContract.transfer(acctAddr, eoaBalance);
|
|
2721
|
+
await transferTx.wait();
|
|
2722
|
+
this._refreshSigner();
|
|
2723
|
+
}
|
|
2724
|
+
const newBalance = await loanContract.balanceOf(acctAddr);
|
|
2725
|
+
approveAmount = newBalance;
|
|
2726
|
+
} else {
|
|
2727
|
+
const loanInfo = this._tokenCache.get(loanTokenAddr.toLowerCase());
|
|
2728
|
+
const loanSymbol = loanInfo?.symbol ?? "loan token";
|
|
2729
|
+
throw new AgetherError(
|
|
2730
|
+
`Insufficient ${loanSymbol} for repay. Need ~${import_ethers2.ethers.formatUnits(checkAmount, loanDecimals)}, AgentAccount has ${import_ethers2.ethers.formatUnits(acctBalance, loanDecimals)}, EOA has ${import_ethers2.ethers.formatUnits(eoaBalance, loanDecimals)}.`,
|
|
2731
|
+
"INSUFFICIENT_BALANCE"
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
} else {
|
|
2735
|
+
const transferTx = await loanContract.transfer(acctAddr, shortfall);
|
|
2736
|
+
await transferTx.wait();
|
|
2737
|
+
this._refreshSigner();
|
|
2715
2738
|
}
|
|
2716
|
-
const transferTx = await loanContract.transfer(acctAddr, shortfall);
|
|
2717
|
-
await transferTx.wait();
|
|
2718
|
-
this._refreshSigner();
|
|
2719
2739
|
}
|
|
2720
2740
|
const targets = [loanTokenAddr, morphoAddr];
|
|
2721
2741
|
const values = [0n, 0n];
|
|
@@ -2732,20 +2752,220 @@ var MorphoClient = class {
|
|
|
2732
2752
|
const receipt = await this.batch(targets, values, datas);
|
|
2733
2753
|
let remainingDebt = "0";
|
|
2734
2754
|
try {
|
|
2735
|
-
const
|
|
2736
|
-
|
|
2755
|
+
const markets = await this.getMarkets(true);
|
|
2756
|
+
const mkt = markets.find(
|
|
2757
|
+
(m) => m.collateralAsset?.address.toLowerCase() === params.collateralToken.toLowerCase() && m.loanAsset?.address.toLowerCase() === params.loanToken.toLowerCase()
|
|
2758
|
+
);
|
|
2759
|
+
if (mkt) {
|
|
2760
|
+
const pos = await this.morphoBlue.position(mkt.uniqueKey, acctAddr);
|
|
2761
|
+
const shares = BigInt(pos.borrowShares);
|
|
2762
|
+
if (shares > 0n) {
|
|
2763
|
+
const onChainMkt = await this.morphoBlue.market(mkt.uniqueKey);
|
|
2764
|
+
const totalAssets = BigInt(onChainMkt.totalBorrowAssets);
|
|
2765
|
+
const totalShares = BigInt(onChainMkt.totalBorrowShares);
|
|
2766
|
+
const debtWei = totalShares > 0n ? shares * totalAssets / totalShares : 0n;
|
|
2767
|
+
remainingDebt = import_ethers2.ethers.formatUnits(debtWei, loanDecimals);
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2737
2770
|
} catch (e) {
|
|
2738
2771
|
console.warn("[agether] failed to read remaining debt after repay:", e instanceof Error ? e.message : e);
|
|
2739
2772
|
}
|
|
2740
2773
|
return { tx: receipt.hash, amount, remainingDebt };
|
|
2741
2774
|
}
|
|
2775
|
+
// 30 min
|
|
2742
2776
|
/**
|
|
2743
|
-
*
|
|
2777
|
+
* Fetch native staking/restaking yields for all known LST/LRT tokens
|
|
2778
|
+
* from the DeFi Llama Yields API. Results are cached for 30 minutes.
|
|
2744
2779
|
*
|
|
2745
|
-
*
|
|
2780
|
+
* @returns Map of token symbol → APY (e.g. { wstETH: 4.56, cbETH: 3.61 })
|
|
2781
|
+
*/
|
|
2782
|
+
async _getLstYields() {
|
|
2783
|
+
if (_MorphoClient._lstYieldCache && Date.now() - _MorphoClient._lstYieldCache.ts < _MorphoClient.LST_CACHE_TTL) {
|
|
2784
|
+
return _MorphoClient._lstYieldCache.data;
|
|
2785
|
+
}
|
|
2786
|
+
const yields = {};
|
|
2787
|
+
try {
|
|
2788
|
+
const resp = await fetch("https://yields.llama.fi/pools", {
|
|
2789
|
+
headers: { "User-Agent": "agether-sdk/1.0" },
|
|
2790
|
+
signal: AbortSignal.timeout(1e4)
|
|
2791
|
+
});
|
|
2792
|
+
if (!resp.ok) {
|
|
2793
|
+
console.warn("[agether] DeFi Llama API returned", resp.status);
|
|
2794
|
+
return yields;
|
|
2795
|
+
}
|
|
2796
|
+
const data = await resp.json();
|
|
2797
|
+
const pools = data?.data ?? [];
|
|
2798
|
+
for (const [morphoSymbol, mapping] of Object.entries(_MorphoClient.LST_PROJECT_MAP)) {
|
|
2799
|
+
const matchingPools = pools.filter(
|
|
2800
|
+
(p) => p.project === mapping.project && p.symbol?.toUpperCase() === mapping.symbol && ["Ethereum", "Base"].includes(p.chain)
|
|
2801
|
+
);
|
|
2802
|
+
if (matchingPools.length > 0) {
|
|
2803
|
+
const best = matchingPools.reduce(
|
|
2804
|
+
(a, b) => (b.tvlUsd ?? 0) > (a.tvlUsd ?? 0) ? b : a
|
|
2805
|
+
);
|
|
2806
|
+
if (best.apy !== void 0 && best.apy > 0) {
|
|
2807
|
+
yields[morphoSymbol] = parseFloat(best.apy.toFixed(4));
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
const knownProjects = new Set(Object.values(_MorphoClient.LST_PROJECT_MAP).map((m) => m.project));
|
|
2812
|
+
const stakingProjects = pools.filter(
|
|
2813
|
+
(p) => p.symbol?.match(/^(ST|WST|CB|R|WE|EZ|RS|SW|M|OS|SFR|O|W?B)ETH$/i) && !knownProjects.has(p.project) && p.tvlUsd > 1e7 && p.apy > 0.5 && ["Ethereum", "Base"].includes(p.chain)
|
|
2814
|
+
);
|
|
2815
|
+
for (const p of stakingProjects) {
|
|
2816
|
+
const sym = p.symbol;
|
|
2817
|
+
if (!yields[sym]) {
|
|
2818
|
+
yields[sym] = parseFloat(p.apy.toFixed(4));
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
} catch (e) {
|
|
2822
|
+
console.warn("[agether] Failed to fetch LST yields:", e instanceof Error ? e.message : e);
|
|
2823
|
+
}
|
|
2824
|
+
_MorphoClient._lstYieldCache = { data: yields, ts: Date.now() };
|
|
2825
|
+
return yields;
|
|
2826
|
+
}
|
|
2827
|
+
/**
|
|
2828
|
+
* Analyze yield spread between LST/LRT collateral yield and Morpho borrow rates.
|
|
2829
|
+
* Positive spread = profitable carry trade (collateral earns more than debt costs).
|
|
2830
|
+
*
|
|
2831
|
+
* @returns Array of markets with yield analysis, sorted by net spread descending.
|
|
2832
|
+
*/
|
|
2833
|
+
async getYieldSpread() {
|
|
2834
|
+
const lstYields = await this._getLstYields();
|
|
2835
|
+
if (Object.keys(lstYields).length === 0) {
|
|
2836
|
+
console.warn("[agether] No LST yield data available \u2014 DeFi Llama may be unreachable");
|
|
2837
|
+
return [];
|
|
2838
|
+
}
|
|
2839
|
+
const lstTokens = Object.keys(lstYields);
|
|
2840
|
+
const allMarkets = [];
|
|
2841
|
+
for (const token of lstTokens) {
|
|
2842
|
+
try {
|
|
2843
|
+
const markets2 = await this.getMarketRates(token);
|
|
2844
|
+
allMarkets.push(...markets2);
|
|
2845
|
+
} catch {
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2849
|
+
const markets = allMarkets.filter((m) => {
|
|
2850
|
+
if (seen.has(m.marketId)) return false;
|
|
2851
|
+
seen.add(m.marketId);
|
|
2852
|
+
return true;
|
|
2853
|
+
});
|
|
2854
|
+
const results = [];
|
|
2855
|
+
for (const mkt of markets) {
|
|
2856
|
+
const collateralYield = lstYields[mkt.collateralToken];
|
|
2857
|
+
if (collateralYield === void 0) continue;
|
|
2858
|
+
const borrowRate = mkt.borrowApy;
|
|
2859
|
+
const netSpread = collateralYield - borrowRate;
|
|
2860
|
+
const lltv = parseFloat(mkt.lltv) / 100;
|
|
2861
|
+
const safeLtv = lltv * 0.8;
|
|
2862
|
+
const maxLeverage = 1 / (1 - safeLtv);
|
|
2863
|
+
const leveragedNetApy = collateralYield * maxLeverage - borrowRate * (maxLeverage - 1);
|
|
2864
|
+
results.push({
|
|
2865
|
+
collateralToken: mkt.collateralToken,
|
|
2866
|
+
loanToken: mkt.loanToken,
|
|
2867
|
+
collateralYield,
|
|
2868
|
+
borrowRate,
|
|
2869
|
+
netSpread,
|
|
2870
|
+
profitable: netSpread > 0,
|
|
2871
|
+
lltv,
|
|
2872
|
+
maxSafeLeverage: parseFloat(maxLeverage.toFixed(2)),
|
|
2873
|
+
leveragedNetApy: parseFloat(leveragedNetApy.toFixed(2)),
|
|
2874
|
+
liquidity: mkt.totalSupplyUsd - mkt.totalBorrowUsd,
|
|
2875
|
+
marketId: mkt.marketId
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2878
|
+
return results.sort((a, b) => b.netSpread - a.netSpread);
|
|
2879
|
+
}
|
|
2880
|
+
/**
|
|
2881
|
+
* Execute a leverage loop: deposit collateral → borrow → swap to collateral → deposit again.
|
|
2882
|
+
* Repeats N times to achieve target leverage.
|
|
2883
|
+
*
|
|
2884
|
+
* Example: 1 wstETH at 3x leverage = deposit 1 → borrow → swap to ~0.77 wstETH → deposit →
|
|
2885
|
+
* borrow → swap to ~0.6 wstETH → deposit. Total exposure: ~2.37 wstETH, debt: ~1.37 ETH worth.
|
|
2746
2886
|
*
|
|
2747
|
-
* @param
|
|
2887
|
+
* @param collateralToken - LST token to deposit (e.g. 'wstETH')
|
|
2888
|
+
* @param amount - Initial deposit amount
|
|
2889
|
+
* @param targetLeverage - Target leverage multiplier (e.g. 2.0 for 2x)
|
|
2890
|
+
* @param loanToken - Token to borrow (default: inferred from market)
|
|
2891
|
+
* @param maxIterations - Max loop iterations (default: 5, safety cap)
|
|
2748
2892
|
*/
|
|
2893
|
+
async leverageLoop(collateralToken, amount, targetLeverage, loanToken, maxIterations = 5) {
|
|
2894
|
+
if (targetLeverage < 1.1 || targetLeverage > 10) {
|
|
2895
|
+
throw new AgetherError(
|
|
2896
|
+
"Target leverage must be between 1.1x and 10x",
|
|
2897
|
+
"INVALID_LEVERAGE"
|
|
2898
|
+
);
|
|
2899
|
+
}
|
|
2900
|
+
const spreads = await this.getYieldSpread();
|
|
2901
|
+
const matchingMarket = spreads.find(
|
|
2902
|
+
(s) => s.collateralToken.toLowerCase() === collateralToken.toLowerCase() && (!loanToken || s.loanToken.toLowerCase() === loanToken.toLowerCase())
|
|
2903
|
+
);
|
|
2904
|
+
if (!matchingMarket) {
|
|
2905
|
+
throw new AgetherError(
|
|
2906
|
+
`No LST market found for ${collateralToken}`,
|
|
2907
|
+
"MARKET_NOT_FOUND"
|
|
2908
|
+
);
|
|
2909
|
+
}
|
|
2910
|
+
if (!matchingMarket) {
|
|
2911
|
+
throw new AgetherError(
|
|
2912
|
+
`No yield data available for ${collateralToken}. Ensure DeFi Llama API is reachable.`,
|
|
2913
|
+
"NO_YIELD_DATA"
|
|
2914
|
+
);
|
|
2915
|
+
}
|
|
2916
|
+
if (targetLeverage > matchingMarket.maxSafeLeverage) {
|
|
2917
|
+
throw new AgetherError(
|
|
2918
|
+
`Target leverage ${targetLeverage}x exceeds max safe leverage ${matchingMarket.maxSafeLeverage}x for ${collateralToken}. Max LLTV is ${(matchingMarket.lltv * 100).toFixed(0)}%.`,
|
|
2919
|
+
"LEVERAGE_TOO_HIGH"
|
|
2920
|
+
);
|
|
2921
|
+
}
|
|
2922
|
+
const initialAmount = parseFloat(amount);
|
|
2923
|
+
const iterations = [];
|
|
2924
|
+
let totalCollateral = initialAmount;
|
|
2925
|
+
let totalDebt = 0;
|
|
2926
|
+
let currentAmount = initialAmount;
|
|
2927
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
2928
|
+
const currentLeverage = totalDebt > 0 ? totalCollateral / (totalCollateral - totalDebt) : 1;
|
|
2929
|
+
if (currentLeverage >= targetLeverage * 0.98) break;
|
|
2930
|
+
const borrowAmount = currentAmount * matchingMarket.lltv * 0.8;
|
|
2931
|
+
const swapOutput = borrowAmount * 0.997;
|
|
2932
|
+
iterations.push({
|
|
2933
|
+
iteration: i + 1,
|
|
2934
|
+
deposit: currentAmount,
|
|
2935
|
+
borrow: borrowAmount,
|
|
2936
|
+
swapOutput
|
|
2937
|
+
});
|
|
2938
|
+
totalDebt += borrowAmount;
|
|
2939
|
+
totalCollateral += swapOutput;
|
|
2940
|
+
currentAmount = swapOutput;
|
|
2941
|
+
}
|
|
2942
|
+
const finalLeverage = totalDebt > 0 ? totalCollateral / (totalCollateral - totalDebt) : 1;
|
|
2943
|
+
return {
|
|
2944
|
+
strategy: "leverage_loop",
|
|
2945
|
+
collateralToken: matchingMarket.collateralToken,
|
|
2946
|
+
loanToken: matchingMarket.loanToken,
|
|
2947
|
+
initialDeposit: amount,
|
|
2948
|
+
targetLeverage,
|
|
2949
|
+
achievedLeverage: parseFloat(finalLeverage.toFixed(2)),
|
|
2950
|
+
totalCollateral: parseFloat(totalCollateral.toFixed(6)),
|
|
2951
|
+
totalDebt: parseFloat(totalDebt.toFixed(6)),
|
|
2952
|
+
iterations,
|
|
2953
|
+
estimatedNetApy: parseFloat(
|
|
2954
|
+
(matchingMarket.collateralYield * finalLeverage - matchingMarket.borrowRate * (finalLeverage - 1)).toFixed(2)
|
|
2955
|
+
),
|
|
2956
|
+
healthFactor: parseFloat(
|
|
2957
|
+
(totalCollateral * matchingMarket.lltv / totalDebt).toFixed(2)
|
|
2958
|
+
),
|
|
2959
|
+
warning: "This is a simulation. Actual execution requires DEX swap integration. Real results may differ due to slippage and price movements."
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
/**
|
|
2963
|
+
* Withdraw collateral from Morpho Blue.
|
|
2964
|
+
*
|
|
2965
|
+
* AgentAccount.execute: Morpho.withdrawCollateral(params, amount, account, receiver)
|
|
2966
|
+
*
|
|
2967
|
+
* @param receiver - defaults to EOA wallet
|
|
2968
|
+
*/
|
|
2749
2969
|
async withdrawCollateral(tokenSymbol, amount, marketParams, receiver) {
|
|
2750
2970
|
const acctAddr = await this.getAccountAddress();
|
|
2751
2971
|
const colInfo = await this._resolveToken(tokenSymbol);
|
|
@@ -3267,6 +3487,31 @@ var MorphoClient = class {
|
|
|
3267
3487
|
return result;
|
|
3268
3488
|
}
|
|
3269
3489
|
};
|
|
3490
|
+
// ════════════════════════════════════════════════════════
|
|
3491
|
+
// Yield Spread Analysis & Leverage
|
|
3492
|
+
// ════════════════════════════════════════════════════════
|
|
3493
|
+
/**
|
|
3494
|
+
* Mapping from Morpho collateral token symbols to DeFi Llama project IDs.
|
|
3495
|
+
* Used to look up native staking/restaking yields from the DeFi Llama Yields API.
|
|
3496
|
+
*/
|
|
3497
|
+
_MorphoClient.LST_PROJECT_MAP = {
|
|
3498
|
+
"wstETH": { project: "lido", symbol: "STETH" },
|
|
3499
|
+
"cbETH": { project: "coinbase-wrapped-staked-eth", symbol: "CBETH" },
|
|
3500
|
+
"rETH": { project: "rocket-pool", symbol: "RETH" },
|
|
3501
|
+
"weETH": { project: "ether.fi-stake", symbol: "WEETH" },
|
|
3502
|
+
"ezETH": { project: "renzo", symbol: "EZETH" },
|
|
3503
|
+
"rsETH": { project: "kelp", symbol: "RSETH" },
|
|
3504
|
+
"swETH": { project: "swell-liquid-staking", symbol: "SWETH" },
|
|
3505
|
+
"mETH": { project: "meth-protocol", symbol: "METH" },
|
|
3506
|
+
"sfrxETH": { project: "frax-ether", symbol: "SFRXETH" },
|
|
3507
|
+
"oETH": { project: "origin-ether", symbol: "OETH" },
|
|
3508
|
+
"ETHx": { project: "stader", symbol: "ETHX" },
|
|
3509
|
+
"wBETH": { project: "binance-staked-eth", symbol: "WBETH" }
|
|
3510
|
+
};
|
|
3511
|
+
/** Cache for LST yields fetched from DeFi Llama. TTL: 30 minutes. */
|
|
3512
|
+
_MorphoClient._lstYieldCache = null;
|
|
3513
|
+
_MorphoClient.LST_CACHE_TTL = 30 * 60 * 1e3;
|
|
3514
|
+
var MorphoClient = _MorphoClient;
|
|
3270
3515
|
|
|
3271
3516
|
// src/clients/ScoringClient.ts
|
|
3272
3517
|
var import_axios3 = __toESM(require("axios"));
|
package/dist/index.mjs
CHANGED
|
@@ -1239,7 +1239,7 @@ var MODE_SINGLE2 = "0x0000000000000000000000000000000000000000000000000000000000
|
|
|
1239
1239
|
var MODE_BATCH = "0x0100000000000000000000000000000000000000000000000000000000000000";
|
|
1240
1240
|
var morphoIface = new ethers2.Interface(MORPHO_BLUE_ABI);
|
|
1241
1241
|
var erc20Iface2 = new ethers2.Interface(ERC20_ABI);
|
|
1242
|
-
var
|
|
1242
|
+
var _MorphoClient = class _MorphoClient {
|
|
1243
1243
|
constructor(config) {
|
|
1244
1244
|
/** Market params cache: keyed by market uniqueKey (bytes32 hash) */
|
|
1245
1245
|
this._marketCache = /* @__PURE__ */ new Map();
|
|
@@ -2615,9 +2615,17 @@ var MorphoClient = class {
|
|
|
2615
2615
|
const estimated = totalBorrowShares > 0n ? repayShares * totalBorrowAssets / totalBorrowShares + 10n : 0n;
|
|
2616
2616
|
approveAmount = estimated > 0n ? estimated : ethers2.parseUnits("1", loanDecimals);
|
|
2617
2617
|
} else {
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2618
|
+
const marketId = ethers2.keccak256(ethers2.AbiCoder.defaultAbiCoder().encode(
|
|
2619
|
+
["address", "address", "address", "address", "uint256"],
|
|
2620
|
+
[params.loanToken, params.collateralToken, params.oracle, params.irm, params.lltv]
|
|
2621
|
+
));
|
|
2622
|
+
const pos = await this.morphoBlue.position(marketId, acctAddr);
|
|
2623
|
+
repayShares = BigInt(pos.borrowShares);
|
|
2624
|
+
repayAssets = 0n;
|
|
2625
|
+
const onChainMkt = await this.morphoBlue.market(marketId);
|
|
2626
|
+
const totalBorrowAssets = BigInt(onChainMkt.totalBorrowAssets);
|
|
2627
|
+
const totalBorrowShares = BigInt(onChainMkt.totalBorrowShares);
|
|
2628
|
+
approveAmount = totalBorrowShares > 0n ? repayShares * totalBorrowAssets / totalBorrowShares + 10n : ethers2.parseUnits("1", loanDecimals);
|
|
2621
2629
|
}
|
|
2622
2630
|
} else {
|
|
2623
2631
|
repayAssets = ethers2.parseUnits(amount, loanDecimals);
|
|
@@ -2626,20 +2634,32 @@ var MorphoClient = class {
|
|
|
2626
2634
|
}
|
|
2627
2635
|
const loanContract = new Contract2(loanTokenAddr, ERC20_ABI, this._signer);
|
|
2628
2636
|
const acctBalance = await loanContract.balanceOf(acctAddr);
|
|
2629
|
-
|
|
2630
|
-
|
|
2637
|
+
const checkAmount = repayShares > 0n && repayAssets === 0n ? approveAmount * 1005n / 1000n : approveAmount;
|
|
2638
|
+
if (acctBalance < checkAmount) {
|
|
2639
|
+
const shortfall = checkAmount - acctBalance;
|
|
2631
2640
|
const eoaBalance = await loanContract.balanceOf(await this.getSignerAddress());
|
|
2632
2641
|
if (eoaBalance < shortfall) {
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2642
|
+
if (repayShares > 0n && acctBalance + eoaBalance > 0n) {
|
|
2643
|
+
if (eoaBalance > 0n) {
|
|
2644
|
+
const transferTx = await loanContract.transfer(acctAddr, eoaBalance);
|
|
2645
|
+
await transferTx.wait();
|
|
2646
|
+
this._refreshSigner();
|
|
2647
|
+
}
|
|
2648
|
+
const newBalance = await loanContract.balanceOf(acctAddr);
|
|
2649
|
+
approveAmount = newBalance;
|
|
2650
|
+
} else {
|
|
2651
|
+
const loanInfo = this._tokenCache.get(loanTokenAddr.toLowerCase());
|
|
2652
|
+
const loanSymbol = loanInfo?.symbol ?? "loan token";
|
|
2653
|
+
throw new AgetherError(
|
|
2654
|
+
`Insufficient ${loanSymbol} for repay. Need ~${ethers2.formatUnits(checkAmount, loanDecimals)}, AgentAccount has ${ethers2.formatUnits(acctBalance, loanDecimals)}, EOA has ${ethers2.formatUnits(eoaBalance, loanDecimals)}.`,
|
|
2655
|
+
"INSUFFICIENT_BALANCE"
|
|
2656
|
+
);
|
|
2657
|
+
}
|
|
2658
|
+
} else {
|
|
2659
|
+
const transferTx = await loanContract.transfer(acctAddr, shortfall);
|
|
2660
|
+
await transferTx.wait();
|
|
2661
|
+
this._refreshSigner();
|
|
2639
2662
|
}
|
|
2640
|
-
const transferTx = await loanContract.transfer(acctAddr, shortfall);
|
|
2641
|
-
await transferTx.wait();
|
|
2642
|
-
this._refreshSigner();
|
|
2643
2663
|
}
|
|
2644
2664
|
const targets = [loanTokenAddr, morphoAddr];
|
|
2645
2665
|
const values = [0n, 0n];
|
|
@@ -2656,20 +2676,220 @@ var MorphoClient = class {
|
|
|
2656
2676
|
const receipt = await this.batch(targets, values, datas);
|
|
2657
2677
|
let remainingDebt = "0";
|
|
2658
2678
|
try {
|
|
2659
|
-
const
|
|
2660
|
-
|
|
2679
|
+
const markets = await this.getMarkets(true);
|
|
2680
|
+
const mkt = markets.find(
|
|
2681
|
+
(m) => m.collateralAsset?.address.toLowerCase() === params.collateralToken.toLowerCase() && m.loanAsset?.address.toLowerCase() === params.loanToken.toLowerCase()
|
|
2682
|
+
);
|
|
2683
|
+
if (mkt) {
|
|
2684
|
+
const pos = await this.morphoBlue.position(mkt.uniqueKey, acctAddr);
|
|
2685
|
+
const shares = BigInt(pos.borrowShares);
|
|
2686
|
+
if (shares > 0n) {
|
|
2687
|
+
const onChainMkt = await this.morphoBlue.market(mkt.uniqueKey);
|
|
2688
|
+
const totalAssets = BigInt(onChainMkt.totalBorrowAssets);
|
|
2689
|
+
const totalShares = BigInt(onChainMkt.totalBorrowShares);
|
|
2690
|
+
const debtWei = totalShares > 0n ? shares * totalAssets / totalShares : 0n;
|
|
2691
|
+
remainingDebt = ethers2.formatUnits(debtWei, loanDecimals);
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2661
2694
|
} catch (e) {
|
|
2662
2695
|
console.warn("[agether] failed to read remaining debt after repay:", e instanceof Error ? e.message : e);
|
|
2663
2696
|
}
|
|
2664
2697
|
return { tx: receipt.hash, amount, remainingDebt };
|
|
2665
2698
|
}
|
|
2699
|
+
// 30 min
|
|
2666
2700
|
/**
|
|
2667
|
-
*
|
|
2701
|
+
* Fetch native staking/restaking yields for all known LST/LRT tokens
|
|
2702
|
+
* from the DeFi Llama Yields API. Results are cached for 30 minutes.
|
|
2668
2703
|
*
|
|
2669
|
-
*
|
|
2704
|
+
* @returns Map of token symbol → APY (e.g. { wstETH: 4.56, cbETH: 3.61 })
|
|
2705
|
+
*/
|
|
2706
|
+
async _getLstYields() {
|
|
2707
|
+
if (_MorphoClient._lstYieldCache && Date.now() - _MorphoClient._lstYieldCache.ts < _MorphoClient.LST_CACHE_TTL) {
|
|
2708
|
+
return _MorphoClient._lstYieldCache.data;
|
|
2709
|
+
}
|
|
2710
|
+
const yields = {};
|
|
2711
|
+
try {
|
|
2712
|
+
const resp = await fetch("https://yields.llama.fi/pools", {
|
|
2713
|
+
headers: { "User-Agent": "agether-sdk/1.0" },
|
|
2714
|
+
signal: AbortSignal.timeout(1e4)
|
|
2715
|
+
});
|
|
2716
|
+
if (!resp.ok) {
|
|
2717
|
+
console.warn("[agether] DeFi Llama API returned", resp.status);
|
|
2718
|
+
return yields;
|
|
2719
|
+
}
|
|
2720
|
+
const data = await resp.json();
|
|
2721
|
+
const pools = data?.data ?? [];
|
|
2722
|
+
for (const [morphoSymbol, mapping] of Object.entries(_MorphoClient.LST_PROJECT_MAP)) {
|
|
2723
|
+
const matchingPools = pools.filter(
|
|
2724
|
+
(p) => p.project === mapping.project && p.symbol?.toUpperCase() === mapping.symbol && ["Ethereum", "Base"].includes(p.chain)
|
|
2725
|
+
);
|
|
2726
|
+
if (matchingPools.length > 0) {
|
|
2727
|
+
const best = matchingPools.reduce(
|
|
2728
|
+
(a, b) => (b.tvlUsd ?? 0) > (a.tvlUsd ?? 0) ? b : a
|
|
2729
|
+
);
|
|
2730
|
+
if (best.apy !== void 0 && best.apy > 0) {
|
|
2731
|
+
yields[morphoSymbol] = parseFloat(best.apy.toFixed(4));
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
const knownProjects = new Set(Object.values(_MorphoClient.LST_PROJECT_MAP).map((m) => m.project));
|
|
2736
|
+
const stakingProjects = pools.filter(
|
|
2737
|
+
(p) => p.symbol?.match(/^(ST|WST|CB|R|WE|EZ|RS|SW|M|OS|SFR|O|W?B)ETH$/i) && !knownProjects.has(p.project) && p.tvlUsd > 1e7 && p.apy > 0.5 && ["Ethereum", "Base"].includes(p.chain)
|
|
2738
|
+
);
|
|
2739
|
+
for (const p of stakingProjects) {
|
|
2740
|
+
const sym = p.symbol;
|
|
2741
|
+
if (!yields[sym]) {
|
|
2742
|
+
yields[sym] = parseFloat(p.apy.toFixed(4));
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
} catch (e) {
|
|
2746
|
+
console.warn("[agether] Failed to fetch LST yields:", e instanceof Error ? e.message : e);
|
|
2747
|
+
}
|
|
2748
|
+
_MorphoClient._lstYieldCache = { data: yields, ts: Date.now() };
|
|
2749
|
+
return yields;
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Analyze yield spread between LST/LRT collateral yield and Morpho borrow rates.
|
|
2753
|
+
* Positive spread = profitable carry trade (collateral earns more than debt costs).
|
|
2754
|
+
*
|
|
2755
|
+
* @returns Array of markets with yield analysis, sorted by net spread descending.
|
|
2756
|
+
*/
|
|
2757
|
+
async getYieldSpread() {
|
|
2758
|
+
const lstYields = await this._getLstYields();
|
|
2759
|
+
if (Object.keys(lstYields).length === 0) {
|
|
2760
|
+
console.warn("[agether] No LST yield data available \u2014 DeFi Llama may be unreachable");
|
|
2761
|
+
return [];
|
|
2762
|
+
}
|
|
2763
|
+
const lstTokens = Object.keys(lstYields);
|
|
2764
|
+
const allMarkets = [];
|
|
2765
|
+
for (const token of lstTokens) {
|
|
2766
|
+
try {
|
|
2767
|
+
const markets2 = await this.getMarketRates(token);
|
|
2768
|
+
allMarkets.push(...markets2);
|
|
2769
|
+
} catch {
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2773
|
+
const markets = allMarkets.filter((m) => {
|
|
2774
|
+
if (seen.has(m.marketId)) return false;
|
|
2775
|
+
seen.add(m.marketId);
|
|
2776
|
+
return true;
|
|
2777
|
+
});
|
|
2778
|
+
const results = [];
|
|
2779
|
+
for (const mkt of markets) {
|
|
2780
|
+
const collateralYield = lstYields[mkt.collateralToken];
|
|
2781
|
+
if (collateralYield === void 0) continue;
|
|
2782
|
+
const borrowRate = mkt.borrowApy;
|
|
2783
|
+
const netSpread = collateralYield - borrowRate;
|
|
2784
|
+
const lltv = parseFloat(mkt.lltv) / 100;
|
|
2785
|
+
const safeLtv = lltv * 0.8;
|
|
2786
|
+
const maxLeverage = 1 / (1 - safeLtv);
|
|
2787
|
+
const leveragedNetApy = collateralYield * maxLeverage - borrowRate * (maxLeverage - 1);
|
|
2788
|
+
results.push({
|
|
2789
|
+
collateralToken: mkt.collateralToken,
|
|
2790
|
+
loanToken: mkt.loanToken,
|
|
2791
|
+
collateralYield,
|
|
2792
|
+
borrowRate,
|
|
2793
|
+
netSpread,
|
|
2794
|
+
profitable: netSpread > 0,
|
|
2795
|
+
lltv,
|
|
2796
|
+
maxSafeLeverage: parseFloat(maxLeverage.toFixed(2)),
|
|
2797
|
+
leveragedNetApy: parseFloat(leveragedNetApy.toFixed(2)),
|
|
2798
|
+
liquidity: mkt.totalSupplyUsd - mkt.totalBorrowUsd,
|
|
2799
|
+
marketId: mkt.marketId
|
|
2800
|
+
});
|
|
2801
|
+
}
|
|
2802
|
+
return results.sort((a, b) => b.netSpread - a.netSpread);
|
|
2803
|
+
}
|
|
2804
|
+
/**
|
|
2805
|
+
* Execute a leverage loop: deposit collateral → borrow → swap to collateral → deposit again.
|
|
2806
|
+
* Repeats N times to achieve target leverage.
|
|
2807
|
+
*
|
|
2808
|
+
* Example: 1 wstETH at 3x leverage = deposit 1 → borrow → swap to ~0.77 wstETH → deposit →
|
|
2809
|
+
* borrow → swap to ~0.6 wstETH → deposit. Total exposure: ~2.37 wstETH, debt: ~1.37 ETH worth.
|
|
2670
2810
|
*
|
|
2671
|
-
* @param
|
|
2811
|
+
* @param collateralToken - LST token to deposit (e.g. 'wstETH')
|
|
2812
|
+
* @param amount - Initial deposit amount
|
|
2813
|
+
* @param targetLeverage - Target leverage multiplier (e.g. 2.0 for 2x)
|
|
2814
|
+
* @param loanToken - Token to borrow (default: inferred from market)
|
|
2815
|
+
* @param maxIterations - Max loop iterations (default: 5, safety cap)
|
|
2672
2816
|
*/
|
|
2817
|
+
async leverageLoop(collateralToken, amount, targetLeverage, loanToken, maxIterations = 5) {
|
|
2818
|
+
if (targetLeverage < 1.1 || targetLeverage > 10) {
|
|
2819
|
+
throw new AgetherError(
|
|
2820
|
+
"Target leverage must be between 1.1x and 10x",
|
|
2821
|
+
"INVALID_LEVERAGE"
|
|
2822
|
+
);
|
|
2823
|
+
}
|
|
2824
|
+
const spreads = await this.getYieldSpread();
|
|
2825
|
+
const matchingMarket = spreads.find(
|
|
2826
|
+
(s) => s.collateralToken.toLowerCase() === collateralToken.toLowerCase() && (!loanToken || s.loanToken.toLowerCase() === loanToken.toLowerCase())
|
|
2827
|
+
);
|
|
2828
|
+
if (!matchingMarket) {
|
|
2829
|
+
throw new AgetherError(
|
|
2830
|
+
`No LST market found for ${collateralToken}`,
|
|
2831
|
+
"MARKET_NOT_FOUND"
|
|
2832
|
+
);
|
|
2833
|
+
}
|
|
2834
|
+
if (!matchingMarket) {
|
|
2835
|
+
throw new AgetherError(
|
|
2836
|
+
`No yield data available for ${collateralToken}. Ensure DeFi Llama API is reachable.`,
|
|
2837
|
+
"NO_YIELD_DATA"
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2840
|
+
if (targetLeverage > matchingMarket.maxSafeLeverage) {
|
|
2841
|
+
throw new AgetherError(
|
|
2842
|
+
`Target leverage ${targetLeverage}x exceeds max safe leverage ${matchingMarket.maxSafeLeverage}x for ${collateralToken}. Max LLTV is ${(matchingMarket.lltv * 100).toFixed(0)}%.`,
|
|
2843
|
+
"LEVERAGE_TOO_HIGH"
|
|
2844
|
+
);
|
|
2845
|
+
}
|
|
2846
|
+
const initialAmount = parseFloat(amount);
|
|
2847
|
+
const iterations = [];
|
|
2848
|
+
let totalCollateral = initialAmount;
|
|
2849
|
+
let totalDebt = 0;
|
|
2850
|
+
let currentAmount = initialAmount;
|
|
2851
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
2852
|
+
const currentLeverage = totalDebt > 0 ? totalCollateral / (totalCollateral - totalDebt) : 1;
|
|
2853
|
+
if (currentLeverage >= targetLeverage * 0.98) break;
|
|
2854
|
+
const borrowAmount = currentAmount * matchingMarket.lltv * 0.8;
|
|
2855
|
+
const swapOutput = borrowAmount * 0.997;
|
|
2856
|
+
iterations.push({
|
|
2857
|
+
iteration: i + 1,
|
|
2858
|
+
deposit: currentAmount,
|
|
2859
|
+
borrow: borrowAmount,
|
|
2860
|
+
swapOutput
|
|
2861
|
+
});
|
|
2862
|
+
totalDebt += borrowAmount;
|
|
2863
|
+
totalCollateral += swapOutput;
|
|
2864
|
+
currentAmount = swapOutput;
|
|
2865
|
+
}
|
|
2866
|
+
const finalLeverage = totalDebt > 0 ? totalCollateral / (totalCollateral - totalDebt) : 1;
|
|
2867
|
+
return {
|
|
2868
|
+
strategy: "leverage_loop",
|
|
2869
|
+
collateralToken: matchingMarket.collateralToken,
|
|
2870
|
+
loanToken: matchingMarket.loanToken,
|
|
2871
|
+
initialDeposit: amount,
|
|
2872
|
+
targetLeverage,
|
|
2873
|
+
achievedLeverage: parseFloat(finalLeverage.toFixed(2)),
|
|
2874
|
+
totalCollateral: parseFloat(totalCollateral.toFixed(6)),
|
|
2875
|
+
totalDebt: parseFloat(totalDebt.toFixed(6)),
|
|
2876
|
+
iterations,
|
|
2877
|
+
estimatedNetApy: parseFloat(
|
|
2878
|
+
(matchingMarket.collateralYield * finalLeverage - matchingMarket.borrowRate * (finalLeverage - 1)).toFixed(2)
|
|
2879
|
+
),
|
|
2880
|
+
healthFactor: parseFloat(
|
|
2881
|
+
(totalCollateral * matchingMarket.lltv / totalDebt).toFixed(2)
|
|
2882
|
+
),
|
|
2883
|
+
warning: "This is a simulation. Actual execution requires DEX swap integration. Real results may differ due to slippage and price movements."
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
/**
|
|
2887
|
+
* Withdraw collateral from Morpho Blue.
|
|
2888
|
+
*
|
|
2889
|
+
* AgentAccount.execute: Morpho.withdrawCollateral(params, amount, account, receiver)
|
|
2890
|
+
*
|
|
2891
|
+
* @param receiver - defaults to EOA wallet
|
|
2892
|
+
*/
|
|
2673
2893
|
async withdrawCollateral(tokenSymbol, amount, marketParams, receiver) {
|
|
2674
2894
|
const acctAddr = await this.getAccountAddress();
|
|
2675
2895
|
const colInfo = await this._resolveToken(tokenSymbol);
|
|
@@ -3191,6 +3411,31 @@ var MorphoClient = class {
|
|
|
3191
3411
|
return result;
|
|
3192
3412
|
}
|
|
3193
3413
|
};
|
|
3414
|
+
// ════════════════════════════════════════════════════════
|
|
3415
|
+
// Yield Spread Analysis & Leverage
|
|
3416
|
+
// ════════════════════════════════════════════════════════
|
|
3417
|
+
/**
|
|
3418
|
+
* Mapping from Morpho collateral token symbols to DeFi Llama project IDs.
|
|
3419
|
+
* Used to look up native staking/restaking yields from the DeFi Llama Yields API.
|
|
3420
|
+
*/
|
|
3421
|
+
_MorphoClient.LST_PROJECT_MAP = {
|
|
3422
|
+
"wstETH": { project: "lido", symbol: "STETH" },
|
|
3423
|
+
"cbETH": { project: "coinbase-wrapped-staked-eth", symbol: "CBETH" },
|
|
3424
|
+
"rETH": { project: "rocket-pool", symbol: "RETH" },
|
|
3425
|
+
"weETH": { project: "ether.fi-stake", symbol: "WEETH" },
|
|
3426
|
+
"ezETH": { project: "renzo", symbol: "EZETH" },
|
|
3427
|
+
"rsETH": { project: "kelp", symbol: "RSETH" },
|
|
3428
|
+
"swETH": { project: "swell-liquid-staking", symbol: "SWETH" },
|
|
3429
|
+
"mETH": { project: "meth-protocol", symbol: "METH" },
|
|
3430
|
+
"sfrxETH": { project: "frax-ether", symbol: "SFRXETH" },
|
|
3431
|
+
"oETH": { project: "origin-ether", symbol: "OETH" },
|
|
3432
|
+
"ETHx": { project: "stader", symbol: "ETHX" },
|
|
3433
|
+
"wBETH": { project: "binance-staked-eth", symbol: "WBETH" }
|
|
3434
|
+
};
|
|
3435
|
+
/** Cache for LST yields fetched from DeFi Llama. TTL: 30 minutes. */
|
|
3436
|
+
_MorphoClient._lstYieldCache = null;
|
|
3437
|
+
_MorphoClient.LST_CACHE_TTL = 30 * 60 * 1e3;
|
|
3438
|
+
var MorphoClient = _MorphoClient;
|
|
3194
3439
|
|
|
3195
3440
|
// src/clients/ScoringClient.ts
|
|
3196
3441
|
import axios3 from "axios";
|