@agether/sdk 1.6.1 → 1.6.3
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.d.ts +25 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +351 -3
- package/dist/clients/AgentIdentityClient.d.ts +188 -0
- package/dist/clients/AgentIdentityClient.d.ts.map +1 -0
- package/dist/clients/AgentIdentityClient.js +333 -0
- package/dist/clients/AgetherClient.d.ts +63 -0
- package/dist/clients/AgetherClient.d.ts.map +1 -0
- package/dist/clients/AgetherClient.js +171 -0
- package/dist/clients/MorphoClient.d.ts +287 -0
- package/dist/clients/MorphoClient.d.ts.map +1 -0
- package/dist/clients/MorphoClient.js +951 -0
- package/dist/clients/ScoringClient.d.ts +89 -0
- package/dist/clients/ScoringClient.d.ts.map +1 -0
- package/dist/clients/ScoringClient.js +93 -0
- package/dist/clients/X402Client.d.ts +130 -0
- package/dist/clients/X402Client.d.ts.map +1 -0
- package/dist/clients/X402Client.js +301 -0
- package/dist/index.d.mts +142 -1
- package/dist/index.d.ts +142 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +351 -3
- package/dist/index.mjs +351 -3
- package/dist/types/index.d.ts +121 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +43 -0
- package/dist/utils/abis.d.ts +18 -0
- package/dist/utils/abis.d.ts.map +1 -0
- package/dist/utils/abis.js +93 -0
- package/dist/utils/config.d.ts +34 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +115 -0
- package/dist/utils/format.d.ts +44 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +75 -0
- package/package.json +1 -1
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MorphoClient — Direct Morpho Blue lending via AgentAccount.executeBatch
|
|
3
|
+
*
|
|
4
|
+
* Architecture (no intermediary contract):
|
|
5
|
+
* EOA → AgentAccount.executeBatch → Morpho Blue (direct)
|
|
6
|
+
*
|
|
7
|
+
* Batched operations:
|
|
8
|
+
* - depositAndBorrow: [ERC20.approve, Morpho.supplyCollateral, Morpho.borrow]
|
|
9
|
+
* - repay: [ERC20.approve, Morpho.repay]
|
|
10
|
+
* - supplyCollateral: [ERC20.approve, Morpho.supplyCollateral]
|
|
11
|
+
*
|
|
12
|
+
* Market discovery via Morpho GraphQL API (https://api.morpho.org/graphql)
|
|
13
|
+
*/
|
|
14
|
+
import { ethers, Contract } from 'ethers';
|
|
15
|
+
import axios from 'axios';
|
|
16
|
+
import { AgetherError, ChainId, } from '../types';
|
|
17
|
+
import { ACCOUNT_FACTORY_ABI, AGENT_ACCOUNT_ABI, AGENT_REPUTATION_ABI, IDENTITY_REGISTRY_ABI, MORPHO_BLUE_ABI, ERC20_ABI, } from '../utils/abis';
|
|
18
|
+
import { getDefaultConfig } from '../utils/config';
|
|
19
|
+
// ── Well-known collateral tokens on Base ──
|
|
20
|
+
const BASE_COLLATERALS = {
|
|
21
|
+
WETH: { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH', decimals: 18 },
|
|
22
|
+
wstETH: { address: '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452', symbol: 'wstETH', decimals: 18 },
|
|
23
|
+
cbETH: { address: '0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22', symbol: 'cbETH', decimals: 18 },
|
|
24
|
+
};
|
|
25
|
+
const MORPHO_API_URL = 'https://api.morpho.org/graphql';
|
|
26
|
+
// ── Morpho ABI interfaces for encoding ──
|
|
27
|
+
const morphoIface = new ethers.Interface(MORPHO_BLUE_ABI);
|
|
28
|
+
const erc20Iface = new ethers.Interface(ERC20_ABI);
|
|
29
|
+
// ── Client ──
|
|
30
|
+
export class MorphoClient {
|
|
31
|
+
constructor(config) {
|
|
32
|
+
this._marketCache = new Map();
|
|
33
|
+
this._discoveredAt = 0;
|
|
34
|
+
const chainId = config.chainId ?? ChainId.Base;
|
|
35
|
+
const defaultCfg = getDefaultConfig(chainId);
|
|
36
|
+
this.config = defaultCfg;
|
|
37
|
+
this.agentId = config.agentId;
|
|
38
|
+
this.provider = new ethers.JsonRpcProvider(config.rpcUrl || defaultCfg.rpcUrl);
|
|
39
|
+
this.wallet = new ethers.Wallet(config.privateKey, this.provider);
|
|
40
|
+
const addrs = { ...defaultCfg.contracts, ...config.contracts };
|
|
41
|
+
this.accountFactory = new Contract(addrs.accountFactory, ACCOUNT_FACTORY_ABI, this.wallet);
|
|
42
|
+
this.morphoBlue = new Contract(addrs.morphoBlue, MORPHO_BLUE_ABI, this.provider);
|
|
43
|
+
this.agentReputation = new Contract(addrs.agentReputation, AGENT_REPUTATION_ABI, this.wallet);
|
|
44
|
+
this.identityRegistry = new Contract(addrs.identityRegistry, IDENTITY_REGISTRY_ABI, this.wallet);
|
|
45
|
+
}
|
|
46
|
+
// ════════════════════════════════════════════════════════
|
|
47
|
+
// Account Management
|
|
48
|
+
// ════════════════════════════════════════════════════════
|
|
49
|
+
/** Resolve the AgentAccount address (cached). */
|
|
50
|
+
async getAccountAddress() {
|
|
51
|
+
if (this._accountAddress)
|
|
52
|
+
return this._accountAddress;
|
|
53
|
+
if (!this.agentId)
|
|
54
|
+
throw new AgetherError('agentId not set', 'NO_AGENT_ID');
|
|
55
|
+
const addr = await this.accountFactory.getAccount(BigInt(this.agentId));
|
|
56
|
+
if (addr === ethers.ZeroAddress) {
|
|
57
|
+
throw new AgetherError('No AgentAccount found. Call register() first.', 'NO_ACCOUNT');
|
|
58
|
+
}
|
|
59
|
+
this._accountAddress = addr;
|
|
60
|
+
return addr;
|
|
61
|
+
}
|
|
62
|
+
getAgentId() {
|
|
63
|
+
if (!this.agentId)
|
|
64
|
+
throw new AgetherError('agentId not set', 'NO_AGENT_ID');
|
|
65
|
+
return this.agentId;
|
|
66
|
+
}
|
|
67
|
+
getWalletAddress() {
|
|
68
|
+
return this.wallet.address;
|
|
69
|
+
}
|
|
70
|
+
/** Mint a new ERC-8004 identity and return the agentId. */
|
|
71
|
+
async _mintNewIdentity() {
|
|
72
|
+
const regTx = await this.identityRegistry.register();
|
|
73
|
+
const regReceipt = await regTx.wait();
|
|
74
|
+
let agentId = 0n;
|
|
75
|
+
for (const log of regReceipt.logs) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = this.identityRegistry.interface.parseLog({ topics: log.topics, data: log.data });
|
|
78
|
+
if (parsed?.name === 'Transfer') {
|
|
79
|
+
agentId = parsed.args[2];
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (agentId === 0n)
|
|
88
|
+
throw new AgetherError('Failed to parse agentId from registration', 'PARSE_ERROR');
|
|
89
|
+
return agentId;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Register: create ERC-8004 identity + AgentAccount in one flow.
|
|
93
|
+
* If already registered, returns existing state.
|
|
94
|
+
*/
|
|
95
|
+
async register(_name) {
|
|
96
|
+
const eoaAddr = this.wallet.address;
|
|
97
|
+
// Check if we already have an agentId
|
|
98
|
+
if (this.agentId) {
|
|
99
|
+
const exists = await this.accountFactory.accountExists(BigInt(this.agentId));
|
|
100
|
+
if (exists) {
|
|
101
|
+
const acct = await this.accountFactory.getAccount(BigInt(this.agentId));
|
|
102
|
+
this._accountAddress = acct;
|
|
103
|
+
return { agentId: this.agentId, address: eoaAddr, agentAccount: acct, alreadyRegistered: true };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Check if wallet already has an identity AND we know the agentId
|
|
107
|
+
let agentId;
|
|
108
|
+
if (this.agentId) {
|
|
109
|
+
// We have an agentId in config — reuse it
|
|
110
|
+
const balance = await this.identityRegistry.balanceOf(eoaAddr);
|
|
111
|
+
if (balance > 0n) {
|
|
112
|
+
agentId = BigInt(this.agentId);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// agentId in config but no on-chain identity — register fresh
|
|
116
|
+
agentId = await this._mintNewIdentity();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// No agentId — always register a new identity (wallets can have multiple ERC-8004 tokens)
|
|
121
|
+
agentId = await this._mintNewIdentity();
|
|
122
|
+
}
|
|
123
|
+
this.agentId = agentId.toString();
|
|
124
|
+
// Create AgentAccount if needed
|
|
125
|
+
const acctExists = await this.accountFactory.accountExists(agentId);
|
|
126
|
+
let txHash;
|
|
127
|
+
if (!acctExists) {
|
|
128
|
+
const tx = await this.accountFactory.createAccount(agentId);
|
|
129
|
+
const receipt = await tx.wait();
|
|
130
|
+
txHash = receipt.hash;
|
|
131
|
+
}
|
|
132
|
+
const acctAddr = await this.accountFactory.getAccount(agentId);
|
|
133
|
+
this._accountAddress = acctAddr;
|
|
134
|
+
return {
|
|
135
|
+
agentId: this.agentId,
|
|
136
|
+
address: eoaAddr,
|
|
137
|
+
agentAccount: acctAddr,
|
|
138
|
+
alreadyRegistered: acctExists,
|
|
139
|
+
tx: txHash,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** Get ETH / USDC / collateral balances for EOA and AgentAccount. */
|
|
143
|
+
async getBalances() {
|
|
144
|
+
const eoaAddr = this.wallet.address;
|
|
145
|
+
const usdc = new Contract(this.config.contracts.usdc, ERC20_ABI, this.provider);
|
|
146
|
+
const ethBal = await this.provider.getBalance(eoaAddr);
|
|
147
|
+
const usdcBal = await usdc.balanceOf(eoaAddr);
|
|
148
|
+
// Fetch collateral token balances for EOA
|
|
149
|
+
const eoaCollateral = {};
|
|
150
|
+
for (const [symbol, info] of Object.entries(BASE_COLLATERALS)) {
|
|
151
|
+
try {
|
|
152
|
+
const token = new Contract(info.address, ERC20_ABI, this.provider);
|
|
153
|
+
const bal = await token.balanceOf(eoaAddr);
|
|
154
|
+
eoaCollateral[symbol] = ethers.formatUnits(bal, info.decimals);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
eoaCollateral[symbol] = '0';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const result = {
|
|
161
|
+
agentId: this.agentId || '?',
|
|
162
|
+
address: eoaAddr,
|
|
163
|
+
eth: ethers.formatEther(ethBal),
|
|
164
|
+
usdc: ethers.formatUnits(usdcBal, 6),
|
|
165
|
+
collateral: eoaCollateral,
|
|
166
|
+
};
|
|
167
|
+
try {
|
|
168
|
+
const acctAddr = await this.getAccountAddress();
|
|
169
|
+
const acctEth = await this.provider.getBalance(acctAddr);
|
|
170
|
+
const acctUsdc = await usdc.balanceOf(acctAddr);
|
|
171
|
+
// Fetch collateral token balances for AgentAccount
|
|
172
|
+
const acctCollateral = {};
|
|
173
|
+
for (const [symbol, info] of Object.entries(BASE_COLLATERALS)) {
|
|
174
|
+
try {
|
|
175
|
+
const token = new Contract(info.address, ERC20_ABI, this.provider);
|
|
176
|
+
const bal = await token.balanceOf(acctAddr);
|
|
177
|
+
acctCollateral[symbol] = ethers.formatUnits(bal, info.decimals);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
acctCollateral[symbol] = '0';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
result.agentAccount = {
|
|
184
|
+
address: acctAddr,
|
|
185
|
+
eth: ethers.formatEther(acctEth),
|
|
186
|
+
usdc: ethers.formatUnits(acctUsdc, 6),
|
|
187
|
+
collateral: acctCollateral,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
catch { /* no account yet */ }
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
/** Transfer USDC from EOA to AgentAccount. */
|
|
194
|
+
async fundAccount(usdcAmount) {
|
|
195
|
+
const acctAddr = await this.getAccountAddress();
|
|
196
|
+
const usdc = new Contract(this.config.contracts.usdc, ERC20_ABI, this.wallet);
|
|
197
|
+
const amount = ethers.parseUnits(usdcAmount, 6);
|
|
198
|
+
const tx = await usdc.transfer(acctAddr, amount);
|
|
199
|
+
const receipt = await tx.wait();
|
|
200
|
+
return { tx: receipt.hash, amount: usdcAmount, agentAccount: acctAddr };
|
|
201
|
+
}
|
|
202
|
+
// ════════════════════════════════════════════════════════
|
|
203
|
+
// Market Discovery (Morpho GraphQL API)
|
|
204
|
+
// ════════════════════════════════════════════════════════
|
|
205
|
+
/**
|
|
206
|
+
* Fetch USDC borrow markets on Base from Morpho API.
|
|
207
|
+
* Caches results for 5 minutes.
|
|
208
|
+
*/
|
|
209
|
+
async getMarkets(forceRefresh = false) {
|
|
210
|
+
if (!forceRefresh && this._discoveredMarkets && Date.now() - this._discoveredAt < 300000) {
|
|
211
|
+
return this._discoveredMarkets;
|
|
212
|
+
}
|
|
213
|
+
const chainId = this.config.chainId;
|
|
214
|
+
const usdcAddr = this.config.contracts.usdc.toLowerCase();
|
|
215
|
+
const query = `{
|
|
216
|
+
markets(
|
|
217
|
+
first: 50
|
|
218
|
+
orderBy: SupplyAssetsUsd
|
|
219
|
+
orderDirection: Desc
|
|
220
|
+
where: { chainId_in: [${chainId}], loanAssetAddress_in: ["${usdcAddr}"] }
|
|
221
|
+
) {
|
|
222
|
+
items {
|
|
223
|
+
uniqueKey
|
|
224
|
+
lltv
|
|
225
|
+
oracleAddress
|
|
226
|
+
irmAddress
|
|
227
|
+
loanAsset { address symbol decimals }
|
|
228
|
+
collateralAsset { address symbol decimals }
|
|
229
|
+
state {
|
|
230
|
+
borrowAssets
|
|
231
|
+
supplyAssets
|
|
232
|
+
utilization
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}`;
|
|
237
|
+
try {
|
|
238
|
+
const resp = await axios.post(MORPHO_API_URL, { query }, { timeout: 10000 });
|
|
239
|
+
const items = resp.data?.data?.markets?.items ?? [];
|
|
240
|
+
this._discoveredMarkets = items.map((m) => ({
|
|
241
|
+
uniqueKey: m.uniqueKey,
|
|
242
|
+
loanAsset: m.loanAsset,
|
|
243
|
+
collateralAsset: m.collateralAsset ?? { address: ethers.ZeroAddress, symbol: 'N/A', decimals: 0 },
|
|
244
|
+
oracle: m.oracleAddress,
|
|
245
|
+
irm: m.irmAddress,
|
|
246
|
+
lltv: BigInt(m.lltv),
|
|
247
|
+
totalSupplyAssets: BigInt(m.state?.supplyAssets ?? '0'),
|
|
248
|
+
totalBorrowAssets: BigInt(m.state?.borrowAssets ?? '0'),
|
|
249
|
+
utilization: m.state?.utilization ? Number(m.state.utilization) : 0,
|
|
250
|
+
}));
|
|
251
|
+
this._discoveredAt = Date.now();
|
|
252
|
+
// Warm the params cache
|
|
253
|
+
for (const mi of this._discoveredMarkets) {
|
|
254
|
+
if (mi.collateralAsset.address !== ethers.ZeroAddress) {
|
|
255
|
+
this._marketCache.set(mi.collateralAsset.address.toLowerCase(), {
|
|
256
|
+
loanToken: mi.loanAsset.address,
|
|
257
|
+
collateralToken: mi.collateralAsset.address,
|
|
258
|
+
oracle: mi.oracle,
|
|
259
|
+
irm: mi.irm,
|
|
260
|
+
lltv: mi.lltv,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return this._discoveredMarkets;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Fallback: return cached or empty
|
|
268
|
+
return this._discoveredMarkets ?? [];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Get MarketParams for a collateral token.
|
|
273
|
+
* Tries cache → API → on-chain idToMarketParams.
|
|
274
|
+
*/
|
|
275
|
+
async findMarketForCollateral(collateralSymbolOrAddress) {
|
|
276
|
+
// Resolve symbol → address
|
|
277
|
+
const colInfo = BASE_COLLATERALS[collateralSymbolOrAddress];
|
|
278
|
+
const colAddr = (colInfo?.address ?? collateralSymbolOrAddress).toLowerCase();
|
|
279
|
+
// Check cache
|
|
280
|
+
const cached = this._marketCache.get(colAddr);
|
|
281
|
+
if (cached)
|
|
282
|
+
return cached;
|
|
283
|
+
// Try API discovery
|
|
284
|
+
await this.getMarkets();
|
|
285
|
+
const fromApi = this._marketCache.get(colAddr);
|
|
286
|
+
if (fromApi)
|
|
287
|
+
return fromApi;
|
|
288
|
+
throw new AgetherError(`No Morpho market found for collateral ${collateralSymbolOrAddress}`, 'MARKET_NOT_FOUND');
|
|
289
|
+
}
|
|
290
|
+
/** Read MarketParams on-chain by market ID (bytes32). */
|
|
291
|
+
async getMarketParams(marketId) {
|
|
292
|
+
const result = await this.morphoBlue.idToMarketParams(marketId);
|
|
293
|
+
return {
|
|
294
|
+
loanToken: result.loanToken,
|
|
295
|
+
collateralToken: result.collateralToken,
|
|
296
|
+
oracle: result.oracle,
|
|
297
|
+
irm: result.irm,
|
|
298
|
+
lltv: result.lltv,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
// ════════════════════════════════════════════════════════
|
|
302
|
+
// Position Reads
|
|
303
|
+
// ════════════════════════════════════════════════════════
|
|
304
|
+
/** Read on-chain position for a specific market. */
|
|
305
|
+
async getPosition(marketId) {
|
|
306
|
+
const acctAddr = await this.getAccountAddress();
|
|
307
|
+
const pos = await this.morphoBlue.position(marketId, acctAddr);
|
|
308
|
+
return {
|
|
309
|
+
supplyShares: pos.supplyShares,
|
|
310
|
+
borrowShares: pos.borrowShares,
|
|
311
|
+
collateral: pos.collateral,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Full status: positions across all discovered markets.
|
|
316
|
+
*/
|
|
317
|
+
async getStatus() {
|
|
318
|
+
const acctAddr = await this.getAccountAddress();
|
|
319
|
+
const markets = await this.getMarkets();
|
|
320
|
+
const positions = [];
|
|
321
|
+
let totalDebt = 0n;
|
|
322
|
+
for (const m of markets) {
|
|
323
|
+
if (!m.collateralAsset || m.collateralAsset.address === ethers.ZeroAddress)
|
|
324
|
+
continue;
|
|
325
|
+
try {
|
|
326
|
+
const pos = await this.morphoBlue.position(m.uniqueKey, acctAddr);
|
|
327
|
+
if (pos.collateral === 0n && pos.borrowShares === 0n && pos.supplyShares === 0n)
|
|
328
|
+
continue;
|
|
329
|
+
// Estimate debt from shares
|
|
330
|
+
let debt = 0n;
|
|
331
|
+
if (pos.borrowShares > 0n) {
|
|
332
|
+
try {
|
|
333
|
+
const mkt = await this.morphoBlue.market(m.uniqueKey);
|
|
334
|
+
const totalBorrowShares = BigInt(mkt.totalBorrowShares);
|
|
335
|
+
const totalBorrowAssets = BigInt(mkt.totalBorrowAssets);
|
|
336
|
+
debt = totalBorrowShares > 0n
|
|
337
|
+
? (BigInt(pos.borrowShares) * totalBorrowAssets) / totalBorrowShares
|
|
338
|
+
: 0n;
|
|
339
|
+
totalDebt += debt;
|
|
340
|
+
}
|
|
341
|
+
catch { /* skip */ }
|
|
342
|
+
}
|
|
343
|
+
positions.push({
|
|
344
|
+
marketId: m.uniqueKey,
|
|
345
|
+
collateralToken: m.collateralAsset.symbol,
|
|
346
|
+
collateral: ethers.formatUnits(pos.collateral, m.collateralAsset.decimals),
|
|
347
|
+
borrowShares: pos.borrowShares.toString(),
|
|
348
|
+
supplyShares: pos.supplyShares.toString(),
|
|
349
|
+
debt: ethers.formatUnits(debt, 6),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
agentId: this.agentId || '?',
|
|
358
|
+
agentAccount: acctAddr,
|
|
359
|
+
totalDebt: ethers.formatUnits(totalDebt, 6),
|
|
360
|
+
positions,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
// ════════════════════════════════════════════════════════
|
|
364
|
+
// Balance & Borrowing Capacity
|
|
365
|
+
// ════════════════════════════════════════════════════════
|
|
366
|
+
/**
|
|
367
|
+
* Get the USDC balance of the AgentAccount.
|
|
368
|
+
* @returns USDC balance in raw units (6 decimals)
|
|
369
|
+
*/
|
|
370
|
+
async getUsdcBalance() {
|
|
371
|
+
const acctAddr = await this.getAccountAddress();
|
|
372
|
+
const usdc = new Contract(this.config.contracts.usdc, ERC20_ABI, this.provider);
|
|
373
|
+
return usdc.balanceOf(acctAddr);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Calculate the maximum additional USDC that can be borrowed
|
|
377
|
+
* given the agent's current collateral and debt across all markets.
|
|
378
|
+
*
|
|
379
|
+
* For each market with collateral deposited:
|
|
380
|
+
* maxBorrow = (collateralValue * LLTV) - currentDebt
|
|
381
|
+
*
|
|
382
|
+
* Uses the Morpho oracle to price collateral → loan token.
|
|
383
|
+
*
|
|
384
|
+
* @returns Maximum additional USDC borrowable (6 decimals)
|
|
385
|
+
*/
|
|
386
|
+
async getMaxBorrowable() {
|
|
387
|
+
const acctAddr = await this.getAccountAddress();
|
|
388
|
+
const markets = await this.getMarkets();
|
|
389
|
+
let totalAdditional = 0n;
|
|
390
|
+
const byMarket = [];
|
|
391
|
+
for (const m of markets) {
|
|
392
|
+
if (!m.collateralAsset || m.collateralAsset.address === ethers.ZeroAddress)
|
|
393
|
+
continue;
|
|
394
|
+
try {
|
|
395
|
+
const pos = await this.morphoBlue.position(m.uniqueKey, acctAddr);
|
|
396
|
+
if (pos.collateral === 0n)
|
|
397
|
+
continue;
|
|
398
|
+
// Get market state for debt calculation
|
|
399
|
+
const mktState = await this.morphoBlue.market(m.uniqueKey);
|
|
400
|
+
const totalBorrowShares = BigInt(mktState.totalBorrowShares);
|
|
401
|
+
const totalBorrowAssets = BigInt(mktState.totalBorrowAssets);
|
|
402
|
+
const currentDebt = totalBorrowShares > 0n
|
|
403
|
+
? (BigInt(pos.borrowShares) * totalBorrowAssets) / totalBorrowShares
|
|
404
|
+
: 0n;
|
|
405
|
+
// Get oracle price for collateral → USDC conversion
|
|
406
|
+
// Morpho oracle returns price scaled to 36 + loanDecimals - collateralDecimals
|
|
407
|
+
// price = collateral * oraclePrice / 10^36
|
|
408
|
+
let collateralValueInLoan;
|
|
409
|
+
try {
|
|
410
|
+
const oracleContract = new Contract(m.oracle, [
|
|
411
|
+
'function price() view returns (uint256)',
|
|
412
|
+
], this.provider);
|
|
413
|
+
const oraclePrice = await oracleContract.price();
|
|
414
|
+
const ORACLE_PRICE_SCALE = 10n ** 36n;
|
|
415
|
+
collateralValueInLoan = (BigInt(pos.collateral) * oraclePrice) / ORACLE_PRICE_SCALE;
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
// maxBorrow = collateralValue * LLTV / 1e18 - currentDebt
|
|
421
|
+
const maxBorrowTotal = (collateralValueInLoan * m.lltv) / (10n ** 18n);
|
|
422
|
+
const maxAdditional = maxBorrowTotal > currentDebt ? maxBorrowTotal - currentDebt : 0n;
|
|
423
|
+
totalAdditional += maxAdditional;
|
|
424
|
+
byMarket.push({
|
|
425
|
+
collateralToken: m.collateralAsset.symbol,
|
|
426
|
+
maxAdditional,
|
|
427
|
+
currentDebt,
|
|
428
|
+
collateralValue: collateralValueInLoan,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return { total: totalAdditional, byMarket };
|
|
436
|
+
}
|
|
437
|
+
// ════════════════════════════════════════════════════════
|
|
438
|
+
// Market Rates & Yield Estimation
|
|
439
|
+
// ════════════════════════════════════════════════════════
|
|
440
|
+
/**
|
|
441
|
+
* Fetch current supply/borrow APY for a collateral market from Morpho GraphQL API.
|
|
442
|
+
*
|
|
443
|
+
* Note: On Morpho Blue, collateral does NOT earn yield directly. Supply APY
|
|
444
|
+
* is what lenders earn; borrow APY is what borrowers pay.
|
|
445
|
+
*/
|
|
446
|
+
async getMarketRates(collateralSymbolOrAddress) {
|
|
447
|
+
const chainId = this.config.chainId;
|
|
448
|
+
const usdcAddr = this.config.contracts.usdc.toLowerCase();
|
|
449
|
+
// Optionally filter by collateral
|
|
450
|
+
let collateralFilter = '';
|
|
451
|
+
if (collateralSymbolOrAddress) {
|
|
452
|
+
const colInfo = BASE_COLLATERALS[collateralSymbolOrAddress];
|
|
453
|
+
const colAddr = (colInfo?.address ?? collateralSymbolOrAddress).toLowerCase();
|
|
454
|
+
collateralFilter = `, collateralAssetAddress_in: ["${colAddr}"]`;
|
|
455
|
+
}
|
|
456
|
+
const query = `{
|
|
457
|
+
markets(
|
|
458
|
+
first: 50
|
|
459
|
+
orderBy: SupplyAssetsUsd
|
|
460
|
+
orderDirection: Desc
|
|
461
|
+
where: { chainId_in: [${chainId}], loanAssetAddress_in: ["${usdcAddr}"]${collateralFilter} }
|
|
462
|
+
) {
|
|
463
|
+
items {
|
|
464
|
+
uniqueKey
|
|
465
|
+
lltv
|
|
466
|
+
loanAsset { address symbol decimals }
|
|
467
|
+
collateralAsset { address symbol decimals }
|
|
468
|
+
state {
|
|
469
|
+
borrowAssets
|
|
470
|
+
supplyAssets
|
|
471
|
+
utilization
|
|
472
|
+
supplyApy
|
|
473
|
+
borrowApy
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}`;
|
|
478
|
+
try {
|
|
479
|
+
const resp = await axios.post(MORPHO_API_URL, { query }, { timeout: 10000 });
|
|
480
|
+
const items = resp.data?.data?.markets?.items ?? [];
|
|
481
|
+
return items
|
|
482
|
+
.filter((m) => m.collateralAsset?.address && m.collateralAsset.address !== ethers.ZeroAddress)
|
|
483
|
+
.map((m) => ({
|
|
484
|
+
collateralToken: m.collateralAsset.symbol,
|
|
485
|
+
loanToken: m.loanAsset.symbol,
|
|
486
|
+
supplyApy: m.state?.supplyApy ? Number(m.state.supplyApy) : 0,
|
|
487
|
+
borrowApy: m.state?.borrowApy ? Number(m.state.borrowApy) : 0,
|
|
488
|
+
utilization: m.state?.utilization ? Number(m.state.utilization) : 0,
|
|
489
|
+
totalSupplyUsd: m.state?.supplyAssets ? Number(m.state.supplyAssets) / 1e6 : 0,
|
|
490
|
+
totalBorrowUsd: m.state?.borrowAssets ? Number(m.state.borrowAssets) / 1e6 : 0,
|
|
491
|
+
lltv: `${(Number(m.lltv) / 1e16).toFixed(0)}%`,
|
|
492
|
+
marketId: m.uniqueKey,
|
|
493
|
+
}));
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
return [];
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Estimate theoretical yield for a given collateral amount over a period.
|
|
501
|
+
*
|
|
502
|
+
* ⚠️ IMPORTANT: On Morpho Blue, collateral does NOT earn yield directly.
|
|
503
|
+
* This estimates what the collateral WOULD earn if it were instead supplied
|
|
504
|
+
* as a lender (not used as collateral). This is a theoretical upper bound
|
|
505
|
+
* useful for setting spending caps.
|
|
506
|
+
*
|
|
507
|
+
* @param collateralSymbol - e.g. 'WETH'
|
|
508
|
+
* @param amount - collateral amount in human-readable (e.g. '1.5')
|
|
509
|
+
* @param periodDays - estimation period in days (default: 1)
|
|
510
|
+
* @param ethPriceUsd - ETH price in USD for value conversion (if not provided, uses oracle)
|
|
511
|
+
* @returns Estimated yield in USD for the period
|
|
512
|
+
*/
|
|
513
|
+
async getYieldEstimate(collateralSymbol, amount, periodDays = 1, ethPriceUsd) {
|
|
514
|
+
const colInfo = BASE_COLLATERALS[collateralSymbol];
|
|
515
|
+
if (!colInfo)
|
|
516
|
+
throw new AgetherError(`Unknown collateral: ${collateralSymbol}`, 'UNKNOWN_COLLATERAL');
|
|
517
|
+
// Get market rates for this collateral
|
|
518
|
+
const rates = await this.getMarketRates(collateralSymbol);
|
|
519
|
+
if (rates.length === 0) {
|
|
520
|
+
throw new AgetherError(`No market found for ${collateralSymbol}`, 'MARKET_NOT_FOUND');
|
|
521
|
+
}
|
|
522
|
+
// Use the most liquid market (first one, sorted by supply)
|
|
523
|
+
const market = rates[0];
|
|
524
|
+
const supplyApy = market.supplyApy;
|
|
525
|
+
// Determine collateral value in USD
|
|
526
|
+
let collateralValueUsd;
|
|
527
|
+
if (ethPriceUsd) {
|
|
528
|
+
collateralValueUsd = parseFloat(amount) * ethPriceUsd;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
try {
|
|
532
|
+
const params = await this.findMarketForCollateral(collateralSymbol);
|
|
533
|
+
const oracleContract = new Contract(params.oracle, [
|
|
534
|
+
'function price() view returns (uint256)',
|
|
535
|
+
], this.provider);
|
|
536
|
+
const oraclePrice = await oracleContract.price();
|
|
537
|
+
const ORACLE_PRICE_SCALE = 10n ** 36n;
|
|
538
|
+
const amountWei = ethers.parseUnits(amount, colInfo.decimals);
|
|
539
|
+
const valueInUsdc = (amountWei * oraclePrice) / ORACLE_PRICE_SCALE;
|
|
540
|
+
collateralValueUsd = Number(valueInUsdc) / 1e6;
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
throw new AgetherError('Cannot determine collateral value. Provide ethPriceUsd.', 'PRICE_UNAVAILABLE');
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// Calculate yield: value * APY * (days / 365)
|
|
547
|
+
const estimatedYieldUsd = collateralValueUsd * supplyApy * (periodDays / 365);
|
|
548
|
+
return {
|
|
549
|
+
collateralToken: collateralSymbol,
|
|
550
|
+
amount,
|
|
551
|
+
periodDays,
|
|
552
|
+
theoreticalSupplyApy: supplyApy,
|
|
553
|
+
estimatedYieldUsd,
|
|
554
|
+
collateralValueUsd,
|
|
555
|
+
disclaimer: 'Collateral on Morpho Blue does NOT earn yield directly. This estimates what it WOULD earn if supplied as a lender instead. Use as a theoretical spending cap.',
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
// ════════════════════════════════════════════════════════
|
|
559
|
+
// Lending Operations (all via AgentAccount.executeBatch)
|
|
560
|
+
// ════════════════════════════════════════════════════════
|
|
561
|
+
/**
|
|
562
|
+
* Deposit collateral into Morpho Blue.
|
|
563
|
+
*
|
|
564
|
+
* Flow:
|
|
565
|
+
* 1. EOA transfers collateral to AgentAccount
|
|
566
|
+
* 2. AgentAccount.executeBatch:
|
|
567
|
+
* [collateral.approve(MorphoBlue), Morpho.supplyCollateral(params)]
|
|
568
|
+
*/
|
|
569
|
+
async supplyCollateral(tokenSymbol, amount, marketParams) {
|
|
570
|
+
const acctAddr = await this.getAccountAddress();
|
|
571
|
+
const colInfo = BASE_COLLATERALS[tokenSymbol];
|
|
572
|
+
if (!colInfo)
|
|
573
|
+
throw new AgetherError(`Unknown collateral: ${tokenSymbol}`, 'UNKNOWN_COLLATERAL');
|
|
574
|
+
const params = marketParams ?? await this.findMarketForCollateral(tokenSymbol);
|
|
575
|
+
const weiAmount = ethers.parseUnits(amount, colInfo.decimals);
|
|
576
|
+
const morphoAddr = this.config.contracts.morphoBlue;
|
|
577
|
+
// Step 1: Transfer collateral from EOA → AgentAccount
|
|
578
|
+
const colToken = new Contract(colInfo.address, ERC20_ABI, this.wallet);
|
|
579
|
+
const transferTx = await colToken.transfer(acctAddr, weiAmount);
|
|
580
|
+
await transferTx.wait();
|
|
581
|
+
// Step 2: AgentAccount batch: approve + supplyCollateral
|
|
582
|
+
const targets = [colInfo.address, morphoAddr];
|
|
583
|
+
const values = [0n, 0n];
|
|
584
|
+
const datas = [
|
|
585
|
+
erc20Iface.encodeFunctionData('approve', [morphoAddr, weiAmount]),
|
|
586
|
+
morphoIface.encodeFunctionData('supplyCollateral', [
|
|
587
|
+
this._toTuple(params), weiAmount, acctAddr, '0x',
|
|
588
|
+
]),
|
|
589
|
+
];
|
|
590
|
+
const receipt = await this.batch(targets, values, datas);
|
|
591
|
+
return {
|
|
592
|
+
tx: receipt.hash,
|
|
593
|
+
collateralToken: tokenSymbol,
|
|
594
|
+
amount,
|
|
595
|
+
agentAccount: acctAddr,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Borrow USDC against existing collateral.
|
|
600
|
+
*
|
|
601
|
+
* AgentAccount.execute: Morpho.borrow(params, amount, 0, account, account)
|
|
602
|
+
*
|
|
603
|
+
* @param usdcAmount - USDC amount (e.g. '100')
|
|
604
|
+
* @param tokenSymbol - collateral symbol to identify which market (default: first with collateral)
|
|
605
|
+
*/
|
|
606
|
+
async borrow(usdcAmount, tokenSymbol, marketParams) {
|
|
607
|
+
const acctAddr = await this.getAccountAddress();
|
|
608
|
+
const amount = ethers.parseUnits(usdcAmount, 6);
|
|
609
|
+
const morphoAddr = this.config.contracts.morphoBlue;
|
|
610
|
+
// Find market
|
|
611
|
+
let params;
|
|
612
|
+
let usedToken = tokenSymbol || 'WETH';
|
|
613
|
+
if (marketParams) {
|
|
614
|
+
params = marketParams;
|
|
615
|
+
}
|
|
616
|
+
else if (tokenSymbol) {
|
|
617
|
+
params = await this.findMarketForCollateral(tokenSymbol);
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
// Auto-detect: find first market with collateral
|
|
621
|
+
const { params: p, symbol } = await this._findActiveMarket();
|
|
622
|
+
params = p;
|
|
623
|
+
usedToken = symbol;
|
|
624
|
+
}
|
|
625
|
+
const data = morphoIface.encodeFunctionData('borrow', [
|
|
626
|
+
this._toTuple(params), amount, 0n, acctAddr, acctAddr,
|
|
627
|
+
]);
|
|
628
|
+
const receipt = await this.exec(morphoAddr, data);
|
|
629
|
+
return {
|
|
630
|
+
tx: receipt.hash,
|
|
631
|
+
amount: usdcAmount,
|
|
632
|
+
collateralToken: usedToken,
|
|
633
|
+
agentAccount: acctAddr,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Deposit collateral AND borrow USDC in one batched transaction.
|
|
638
|
+
*
|
|
639
|
+
* AgentAccount.executeBatch:
|
|
640
|
+
* [collateral.approve, Morpho.supplyCollateral, Morpho.borrow]
|
|
641
|
+
*
|
|
642
|
+
* The collateral must be transferred to AgentAccount first.
|
|
643
|
+
*/
|
|
644
|
+
async depositAndBorrow(tokenSymbol, collateralAmount, borrowUsdcAmount, marketParams) {
|
|
645
|
+
const acctAddr = await this.getAccountAddress();
|
|
646
|
+
const colInfo = BASE_COLLATERALS[tokenSymbol];
|
|
647
|
+
if (!colInfo)
|
|
648
|
+
throw new AgetherError(`Unknown collateral: ${tokenSymbol}`, 'UNKNOWN_COLLATERAL');
|
|
649
|
+
const params = marketParams ?? await this.findMarketForCollateral(tokenSymbol);
|
|
650
|
+
const colWei = ethers.parseUnits(collateralAmount, colInfo.decimals);
|
|
651
|
+
const borrowWei = ethers.parseUnits(borrowUsdcAmount, 6);
|
|
652
|
+
const morphoAddr = this.config.contracts.morphoBlue;
|
|
653
|
+
// Step 1: Transfer collateral from EOA → AgentAccount
|
|
654
|
+
const colToken = new Contract(colInfo.address, ERC20_ABI, this.wallet);
|
|
655
|
+
const transferTx = await colToken.transfer(acctAddr, colWei);
|
|
656
|
+
await transferTx.wait();
|
|
657
|
+
// Step 2: Batched — approve + supplyCollateral + borrow
|
|
658
|
+
const targets = [colInfo.address, morphoAddr, morphoAddr];
|
|
659
|
+
const values = [0n, 0n, 0n];
|
|
660
|
+
const datas = [
|
|
661
|
+
erc20Iface.encodeFunctionData('approve', [morphoAddr, colWei]),
|
|
662
|
+
morphoIface.encodeFunctionData('supplyCollateral', [
|
|
663
|
+
this._toTuple(params), colWei, acctAddr, '0x',
|
|
664
|
+
]),
|
|
665
|
+
morphoIface.encodeFunctionData('borrow', [
|
|
666
|
+
this._toTuple(params), borrowWei, 0n, acctAddr, acctAddr,
|
|
667
|
+
]),
|
|
668
|
+
];
|
|
669
|
+
const receipt = await this.batch(targets, values, datas);
|
|
670
|
+
return {
|
|
671
|
+
tx: receipt.hash,
|
|
672
|
+
collateralToken: tokenSymbol,
|
|
673
|
+
collateralAmount,
|
|
674
|
+
borrowAmount: borrowUsdcAmount,
|
|
675
|
+
agentAccount: acctAddr,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Repay borrowed USDC from AgentAccount.
|
|
680
|
+
*
|
|
681
|
+
* AgentAccount.executeBatch:
|
|
682
|
+
* [USDC.approve(MorphoBlue), Morpho.repay(params)]
|
|
683
|
+
*/
|
|
684
|
+
async repay(usdcAmount, tokenSymbol, marketParams) {
|
|
685
|
+
const acctAddr = await this.getAccountAddress();
|
|
686
|
+
const morphoAddr = this.config.contracts.morphoBlue;
|
|
687
|
+
const usdcAddr = this.config.contracts.usdc;
|
|
688
|
+
let params;
|
|
689
|
+
if (marketParams) {
|
|
690
|
+
params = marketParams;
|
|
691
|
+
}
|
|
692
|
+
else if (tokenSymbol) {
|
|
693
|
+
params = await this.findMarketForCollateral(tokenSymbol);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
const { params: p } = await this._findActiveMarket();
|
|
697
|
+
params = p;
|
|
698
|
+
}
|
|
699
|
+
// Determine whether to repay by shares (full repay) or by assets (partial).
|
|
700
|
+
// Using shares-based repay prevents dust borrow shares from remaining.
|
|
701
|
+
let repayAssets;
|
|
702
|
+
let repayShares;
|
|
703
|
+
let approveAmount;
|
|
704
|
+
if (usdcAmount === 'all') {
|
|
705
|
+
// Full repay: use shares to ensure no dust remains
|
|
706
|
+
const markets = await this.getMarkets();
|
|
707
|
+
const mkt = markets.find((m) => m.collateralAsset?.address.toLowerCase() === params.collateralToken.toLowerCase());
|
|
708
|
+
if (mkt) {
|
|
709
|
+
const pos = await this.morphoBlue.position(mkt.uniqueKey, acctAddr);
|
|
710
|
+
repayShares = BigInt(pos.borrowShares);
|
|
711
|
+
repayAssets = 0n;
|
|
712
|
+
// Read on-chain market state for accurate share→asset conversion
|
|
713
|
+
const onChainMkt = await this.morphoBlue.market(mkt.uniqueKey);
|
|
714
|
+
const totalBorrowAssets = BigInt(onChainMkt.totalBorrowAssets);
|
|
715
|
+
const totalBorrowShares = BigInt(onChainMkt.totalBorrowShares);
|
|
716
|
+
const estimated = totalBorrowShares > 0n
|
|
717
|
+
? (repayShares * totalBorrowAssets) / totalBorrowShares + 10n
|
|
718
|
+
: 0n;
|
|
719
|
+
approveAmount = estimated > 0n ? estimated : ethers.parseUnits('1', 6);
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
// Fallback: large asset repay
|
|
723
|
+
repayAssets = ethers.parseUnits('999999', 6);
|
|
724
|
+
repayShares = 0n;
|
|
725
|
+
approveAmount = repayAssets;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
repayAssets = ethers.parseUnits(usdcAmount, 6);
|
|
730
|
+
repayShares = 0n;
|
|
731
|
+
approveAmount = repayAssets;
|
|
732
|
+
// Check if this covers the full debt — if so, switch to shares-based
|
|
733
|
+
try {
|
|
734
|
+
const markets = await this.getMarkets();
|
|
735
|
+
const mkt = markets.find((m) => m.collateralAsset?.address.toLowerCase() === params.collateralToken.toLowerCase());
|
|
736
|
+
if (mkt) {
|
|
737
|
+
const pos = await this.morphoBlue.position(mkt.uniqueKey, acctAddr);
|
|
738
|
+
const onChainMkt = await this.morphoBlue.market(mkt.uniqueKey);
|
|
739
|
+
const totalBorrowAssets = BigInt(onChainMkt.totalBorrowAssets);
|
|
740
|
+
const totalBorrowShares = BigInt(onChainMkt.totalBorrowShares);
|
|
741
|
+
const currentDebt = totalBorrowShares > 0n
|
|
742
|
+
? (BigInt(pos.borrowShares) * totalBorrowAssets) / totalBorrowShares
|
|
743
|
+
: 0n;
|
|
744
|
+
// If repaying >= debt, use shares to clear dust
|
|
745
|
+
if (repayAssets >= currentDebt && BigInt(pos.borrowShares) > 0n) {
|
|
746
|
+
repayShares = BigInt(pos.borrowShares);
|
|
747
|
+
repayAssets = 0n;
|
|
748
|
+
approveAmount = currentDebt + 10n; // small buffer for rounding
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
catch { /* fall through to asset-based repay */ }
|
|
753
|
+
}
|
|
754
|
+
// Batch: approve + repay
|
|
755
|
+
const targets = [usdcAddr, morphoAddr];
|
|
756
|
+
const values = [0n, 0n];
|
|
757
|
+
const datas = [
|
|
758
|
+
erc20Iface.encodeFunctionData('approve', [morphoAddr, approveAmount]),
|
|
759
|
+
morphoIface.encodeFunctionData('repay', [
|
|
760
|
+
this._toTuple(params), repayAssets, repayShares, acctAddr, '0x',
|
|
761
|
+
]),
|
|
762
|
+
];
|
|
763
|
+
const receipt = await this.batch(targets, values, datas);
|
|
764
|
+
// Read remaining debt
|
|
765
|
+
let remainingDebt = '0';
|
|
766
|
+
try {
|
|
767
|
+
const status = await this.getStatus();
|
|
768
|
+
remainingDebt = status.totalDebt;
|
|
769
|
+
}
|
|
770
|
+
catch { /* ignore */ }
|
|
771
|
+
return { tx: receipt.hash, amount: usdcAmount, remainingDebt };
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Withdraw collateral from Morpho Blue.
|
|
775
|
+
*
|
|
776
|
+
* AgentAccount.execute: Morpho.withdrawCollateral(params, amount, account, receiver)
|
|
777
|
+
*
|
|
778
|
+
* @param receiver - defaults to EOA wallet
|
|
779
|
+
*/
|
|
780
|
+
async withdrawCollateral(tokenSymbol, amount, marketParams, receiver) {
|
|
781
|
+
const acctAddr = await this.getAccountAddress();
|
|
782
|
+
const colInfo = BASE_COLLATERALS[tokenSymbol];
|
|
783
|
+
if (!colInfo)
|
|
784
|
+
throw new AgetherError(`Unknown collateral: ${tokenSymbol}`, 'UNKNOWN_COLLATERAL');
|
|
785
|
+
const params = marketParams ?? await this.findMarketForCollateral(tokenSymbol);
|
|
786
|
+
const morphoAddr = this.config.contracts.morphoBlue;
|
|
787
|
+
const dest = receiver || this.wallet.address;
|
|
788
|
+
// Handle 'all' — withdraw full collateral
|
|
789
|
+
let weiAmount;
|
|
790
|
+
if (amount === 'all') {
|
|
791
|
+
const markets = await this.getMarkets();
|
|
792
|
+
const market = markets.find((m) => m.collateralAsset.address.toLowerCase() === colInfo.address.toLowerCase());
|
|
793
|
+
if (!market)
|
|
794
|
+
throw new AgetherError('Market not found', 'MARKET_NOT_FOUND');
|
|
795
|
+
const pos = await this.morphoBlue.position(market.uniqueKey, acctAddr);
|
|
796
|
+
weiAmount = pos.collateral;
|
|
797
|
+
if (weiAmount === 0n)
|
|
798
|
+
throw new AgetherError('No collateral to withdraw', 'NO_COLLATERAL');
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
weiAmount = ethers.parseUnits(amount, colInfo.decimals);
|
|
802
|
+
}
|
|
803
|
+
const data = morphoIface.encodeFunctionData('withdrawCollateral', [
|
|
804
|
+
this._toTuple(params), weiAmount, acctAddr, dest,
|
|
805
|
+
]);
|
|
806
|
+
const receipt = await this.exec(morphoAddr, data);
|
|
807
|
+
// Read remaining collateral
|
|
808
|
+
let remainingCollateral = '0';
|
|
809
|
+
try {
|
|
810
|
+
const markets = await this.getMarkets();
|
|
811
|
+
const market = markets.find((m) => m.collateralAsset.address.toLowerCase() === colInfo.address.toLowerCase());
|
|
812
|
+
if (market) {
|
|
813
|
+
const pos = await this.morphoBlue.position(market.uniqueKey, acctAddr);
|
|
814
|
+
remainingCollateral = ethers.formatUnits(pos.collateral, colInfo.decimals);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
catch { /* ignore */ }
|
|
818
|
+
return {
|
|
819
|
+
tx: receipt.hash,
|
|
820
|
+
token: tokenSymbol,
|
|
821
|
+
amount: amount === 'all' ? ethers.formatUnits(weiAmount, colInfo.decimals) : amount,
|
|
822
|
+
remainingCollateral,
|
|
823
|
+
destination: dest,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Sponsor: transfer collateral to another agent's AgentAccount.
|
|
828
|
+
* (The agent must then supplyCollateral themselves via their own account.)
|
|
829
|
+
*/
|
|
830
|
+
async sponsor(target, tokenSymbol, amount) {
|
|
831
|
+
const colInfo = BASE_COLLATERALS[tokenSymbol];
|
|
832
|
+
if (!colInfo)
|
|
833
|
+
throw new AgetherError(`Unknown collateral: ${tokenSymbol}`, 'UNKNOWN_COLLATERAL');
|
|
834
|
+
let targetAddr;
|
|
835
|
+
if (target.address) {
|
|
836
|
+
targetAddr = target.address;
|
|
837
|
+
}
|
|
838
|
+
else if (target.agentId) {
|
|
839
|
+
targetAddr = await this.accountFactory.getAccount(BigInt(target.agentId));
|
|
840
|
+
if (targetAddr === ethers.ZeroAddress)
|
|
841
|
+
throw new AgetherError('Target agent has no account', 'NO_ACCOUNT');
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
throw new AgetherError('Provide agentId or address', 'INVALID_TARGET');
|
|
845
|
+
}
|
|
846
|
+
const weiAmount = ethers.parseUnits(amount, colInfo.decimals);
|
|
847
|
+
const colToken = new Contract(colInfo.address, ERC20_ABI, this.wallet);
|
|
848
|
+
const tx = await colToken.transfer(targetAddr, weiAmount);
|
|
849
|
+
const receipt = await tx.wait();
|
|
850
|
+
return { tx: receipt.hash, targetAccount: targetAddr, targetAgentId: target.agentId };
|
|
851
|
+
}
|
|
852
|
+
// ════════════════════════════════════════════════════════
|
|
853
|
+
// Reputation (AgentReputation contract)
|
|
854
|
+
// ════════════════════════════════════════════════════════
|
|
855
|
+
async getCreditScore() {
|
|
856
|
+
if (!this.agentId)
|
|
857
|
+
throw new AgetherError('agentId not set', 'NO_AGENT_ID');
|
|
858
|
+
return this.agentReputation.getCreditScore(BigInt(this.agentId));
|
|
859
|
+
}
|
|
860
|
+
async getAttestation() {
|
|
861
|
+
if (!this.agentId)
|
|
862
|
+
throw new AgetherError('agentId not set', 'NO_AGENT_ID');
|
|
863
|
+
const att = await this.agentReputation.getAttestation(BigInt(this.agentId));
|
|
864
|
+
return { score: att.score, timestamp: att.timestamp, signer: att.signer };
|
|
865
|
+
}
|
|
866
|
+
async isEligible(minScore = 500n) {
|
|
867
|
+
if (!this.agentId)
|
|
868
|
+
throw new AgetherError('agentId not set', 'NO_AGENT_ID');
|
|
869
|
+
const [eligible, currentScore] = await this.agentReputation.isEligible(BigInt(this.agentId), minScore);
|
|
870
|
+
return { eligible, currentScore };
|
|
871
|
+
}
|
|
872
|
+
async isScoreFresh() {
|
|
873
|
+
if (!this.agentId)
|
|
874
|
+
throw new AgetherError('agentId not set', 'NO_AGENT_ID');
|
|
875
|
+
const [fresh, age] = await this.agentReputation.isScoreFresh(BigInt(this.agentId));
|
|
876
|
+
return { fresh, age };
|
|
877
|
+
}
|
|
878
|
+
// ════════════════════════════════════════════════════════
|
|
879
|
+
// Internal Helpers
|
|
880
|
+
// ════════════════════════════════════════════════════════
|
|
881
|
+
/**
|
|
882
|
+
* Execute a single call via AgentAccount.execute.
|
|
883
|
+
*/
|
|
884
|
+
async exec(target, data, value = 0n) {
|
|
885
|
+
const acctAddr = await this.getAccountAddress();
|
|
886
|
+
const account = new Contract(acctAddr, AGENT_ACCOUNT_ABI, this.wallet);
|
|
887
|
+
// Estimate gas with buffer
|
|
888
|
+
let gasLimit;
|
|
889
|
+
try {
|
|
890
|
+
const estimate = await account.execute.estimateGas(target, value, data);
|
|
891
|
+
gasLimit = (estimate * 130n) / 100n; // 30% buffer
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
gasLimit = 500000n;
|
|
895
|
+
}
|
|
896
|
+
const tx = await account.execute(target, value, data, { gasLimit });
|
|
897
|
+
return tx.wait();
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Execute multiple calls via AgentAccount.executeBatch.
|
|
901
|
+
*/
|
|
902
|
+
async batch(targets, values, datas) {
|
|
903
|
+
const acctAddr = await this.getAccountAddress();
|
|
904
|
+
const account = new Contract(acctAddr, AGENT_ACCOUNT_ABI, this.wallet);
|
|
905
|
+
// Estimate gas with buffer
|
|
906
|
+
let gasLimit;
|
|
907
|
+
try {
|
|
908
|
+
const estimate = await account.executeBatch.estimateGas(targets, values, datas);
|
|
909
|
+
gasLimit = (estimate * 130n) / 100n;
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
gasLimit = 800000n;
|
|
913
|
+
}
|
|
914
|
+
const tx = await account.executeBatch(targets, values, datas, { gasLimit });
|
|
915
|
+
return tx.wait();
|
|
916
|
+
}
|
|
917
|
+
/** Convert MorphoMarketParams to Solidity tuple. */
|
|
918
|
+
_toTuple(p) {
|
|
919
|
+
return [p.loanToken, p.collateralToken, p.oracle, p.irm, p.lltv];
|
|
920
|
+
}
|
|
921
|
+
/** Find the first market where the agent has collateral deposited. */
|
|
922
|
+
async _findActiveMarket() {
|
|
923
|
+
const acctAddr = await this.getAccountAddress();
|
|
924
|
+
const markets = await this.getMarkets();
|
|
925
|
+
for (const m of markets) {
|
|
926
|
+
if (!m.collateralAsset || m.collateralAsset.address === ethers.ZeroAddress)
|
|
927
|
+
continue;
|
|
928
|
+
try {
|
|
929
|
+
const pos = await this.morphoBlue.position(m.uniqueKey, acctAddr);
|
|
930
|
+
if (pos.collateral > 0n) {
|
|
931
|
+
return {
|
|
932
|
+
params: {
|
|
933
|
+
loanToken: m.loanAsset.address,
|
|
934
|
+
collateralToken: m.collateralAsset.address,
|
|
935
|
+
oracle: m.oracle,
|
|
936
|
+
irm: m.irm,
|
|
937
|
+
lltv: m.lltv,
|
|
938
|
+
},
|
|
939
|
+
symbol: m.collateralAsset.symbol,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
catch {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
// Default to WETH
|
|
948
|
+
const params = await this.findMarketForCollateral('WETH');
|
|
949
|
+
return { params, symbol: 'WETH' };
|
|
950
|
+
}
|
|
951
|
+
}
|