@agether/sdk 2.6.1 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -27
- package/dist/cli.js +54 -36
- package/dist/index.d.mts +43 -14
- package/dist/index.d.ts +43 -14
- package/dist/index.js +65 -32
- package/dist/index.mjs +62 -32
- package/package.json +2 -2
- package/dist/cli.d.ts +0 -27
- package/dist/cli.d.ts.map +0 -1
- package/dist/clients/AgentIdentityClient.d.ts +0 -188
- package/dist/clients/AgentIdentityClient.d.ts.map +0 -1
- package/dist/clients/AgentIdentityClient.js +0 -337
- package/dist/clients/AgetherClient.d.ts +0 -74
- package/dist/clients/AgetherClient.d.ts.map +0 -1
- package/dist/clients/AgetherClient.js +0 -172
- package/dist/clients/MorphoClient.d.ts +0 -482
- package/dist/clients/MorphoClient.d.ts.map +0 -1
- package/dist/clients/MorphoClient.js +0 -1717
- package/dist/clients/ScoringClient.d.ts +0 -89
- package/dist/clients/ScoringClient.d.ts.map +0 -1
- package/dist/clients/ScoringClient.js +0 -93
- package/dist/clients/X402Client.d.ts +0 -168
- package/dist/clients/X402Client.d.ts.map +0 -1
- package/dist/clients/X402Client.js +0 -378
- package/dist/index.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -132
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -46
- package/dist/utils/abis.d.ts +0 -29
- package/dist/utils/abis.d.ts.map +0 -1
- package/dist/utils/abis.js +0 -139
- package/dist/utils/config.d.ts +0 -36
- package/dist/utils/config.d.ts.map +0 -1
- package/dist/utils/config.js +0 -168
- package/dist/utils/format.d.ts +0 -44
- package/dist/utils/format.d.ts.map +0 -1
- package/dist/utils/format.js +0 -75
|
@@ -1,1717 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MorphoClient — Direct Morpho Blue lending via Safe account (ERC-4337 UserOps)
|
|
3
|
-
*
|
|
4
|
-
* Architecture (v2 — Safe + Safe7579):
|
|
5
|
-
* EOA signs UserOp → EntryPoint.handleOps() → Safe → Safe7579 → execute → Morpho Blue
|
|
6
|
-
*
|
|
7
|
-
* ERC-7579 execution modes (same encoding, submitted via UserOp):
|
|
8
|
-
* - Single: execute(MODE_SINGLE, abi.encode(target, value, callData))
|
|
9
|
-
* - Batch: execute(MODE_BATCH, abi.encode(Execution[]))
|
|
10
|
-
*
|
|
11
|
-
* The Safe account has a sentinel owner (no execTransaction).
|
|
12
|
-
* All state-changing operations go through ERC-4337 EntryPoint v0.7.
|
|
13
|
-
*
|
|
14
|
-
* Market discovery via Morpho GraphQL API (https://api.morpho.org/graphql)
|
|
15
|
-
*
|
|
16
|
-
* Supports two signing modes:
|
|
17
|
-
* - `privateKey`: SDK manages wallet lifecycle (existing behavior)
|
|
18
|
-
* - `signer`: external signer (Bankr, Privy, Turnkey, MetaMask, etc.)
|
|
19
|
-
*/
|
|
20
|
-
import { ethers, Contract } from 'ethers';
|
|
21
|
-
import axios from 'axios';
|
|
22
|
-
import { AgetherError, ChainId, } from '../types';
|
|
23
|
-
import { ACCOUNT_FACTORY_ABI, SAFE7579_ACCOUNT_ABI, AGENT_REPUTATION_ABI, IDENTITY_REGISTRY_ABI, MORPHO_BLUE_ABI, ERC20_ABI, ERC8004_VALIDATION_MODULE_ABI, ENTRYPOINT_V07_ABI, } from '../utils/abis';
|
|
24
|
-
import { getDefaultConfig } from '../utils/config';
|
|
25
|
-
// ── Well-known collateral tokens on Base ──
|
|
26
|
-
const BASE_COLLATERALS = {
|
|
27
|
-
WETH: { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH', decimals: 18 },
|
|
28
|
-
wstETH: { address: '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452', symbol: 'wstETH', decimals: 18 },
|
|
29
|
-
cbETH: { address: '0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22', symbol: 'cbETH', decimals: 18 },
|
|
30
|
-
};
|
|
31
|
-
const MORPHO_API_URL = 'https://api.morpho.org/graphql';
|
|
32
|
-
// ── ERC-7579 Execution Mode Constants ──
|
|
33
|
-
/** Single call: callType=0x00, execType=0x00, rest zero-padded to 32 bytes */
|
|
34
|
-
const MODE_SINGLE = '0x0000000000000000000000000000000000000000000000000000000000000000';
|
|
35
|
-
/** Batch call: callType=0x01, execType=0x00, rest zero-padded to 32 bytes */
|
|
36
|
-
const MODE_BATCH = '0x0100000000000000000000000000000000000000000000000000000000000000';
|
|
37
|
-
// ── Morpho ABI interfaces for encoding ──
|
|
38
|
-
const morphoIface = new ethers.Interface(MORPHO_BLUE_ABI);
|
|
39
|
-
const erc20Iface = new ethers.Interface(ERC20_ABI);
|
|
40
|
-
// ── Client ──
|
|
41
|
-
export class MorphoClient {
|
|
42
|
-
constructor(config) {
|
|
43
|
-
this._marketCache = new Map();
|
|
44
|
-
this._discoveredAt = 0;
|
|
45
|
-
const chainId = config.chainId ?? ChainId.Base;
|
|
46
|
-
const defaultCfg = getDefaultConfig(chainId);
|
|
47
|
-
this.config = defaultCfg;
|
|
48
|
-
this.agentId = config.agentId;
|
|
49
|
-
this._rpcUrl = config.rpcUrl || defaultCfg.rpcUrl;
|
|
50
|
-
if ('signer' in config && config.signer) {
|
|
51
|
-
// ── External signer path ──
|
|
52
|
-
this._useExternalSigner = true;
|
|
53
|
-
const signerProvider = config.signer.provider;
|
|
54
|
-
if (signerProvider) {
|
|
55
|
-
// Signer already has a provider — use it
|
|
56
|
-
this.provider = signerProvider;
|
|
57
|
-
this._signer = config.signer;
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
// Signer has no provider — create one from rpcUrl and connect
|
|
61
|
-
this.provider = new ethers.JsonRpcProvider(this._rpcUrl);
|
|
62
|
-
this._signer = config.signer.connect(this.provider);
|
|
63
|
-
}
|
|
64
|
-
// Try to cache address synchronously (works for ethers.Wallet and similar)
|
|
65
|
-
if ('address' in config.signer && typeof config.signer.address === 'string') {
|
|
66
|
-
this._eoaAddress = config.signer.address;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
// ── Private key path (existing behavior) ──
|
|
71
|
-
this._privateKey = config.privateKey;
|
|
72
|
-
this._useExternalSigner = false;
|
|
73
|
-
this.provider = new ethers.JsonRpcProvider(this._rpcUrl);
|
|
74
|
-
const wallet = new ethers.Wallet(this._privateKey, this.provider);
|
|
75
|
-
this._signer = wallet;
|
|
76
|
-
this._eoaAddress = wallet.address;
|
|
77
|
-
}
|
|
78
|
-
const addrs = { ...defaultCfg.contracts, ...config.contracts };
|
|
79
|
-
this.agether4337Factory = new Contract(addrs.agether4337Factory, ACCOUNT_FACTORY_ABI, this._signer);
|
|
80
|
-
this.morphoBlue = new Contract(addrs.morphoBlue, MORPHO_BLUE_ABI, this.provider);
|
|
81
|
-
this.agether8004Scorer = new Contract(addrs.agether8004Scorer, AGENT_REPUTATION_ABI, this._signer);
|
|
82
|
-
this.identityRegistry = new Contract(addrs.identityRegistry, IDENTITY_REGISTRY_ABI, this._signer);
|
|
83
|
-
this.entryPoint = new Contract(addrs.entryPoint, ENTRYPOINT_V07_ABI, this._signer);
|
|
84
|
-
this.validationModule = new Contract(addrs.erc8004ValidationModule, ERC8004_VALIDATION_MODULE_ABI, this.provider);
|
|
85
|
-
}
|
|
86
|
-
// ════════════════════════════════════════════════════════
|
|
87
|
-
// KYA Gate Check
|
|
88
|
-
// ════════════════════════════════════════════════════════
|
|
89
|
-
/**
|
|
90
|
-
* Check whether the KYA (Know Your Agent) code verification gate is active.
|
|
91
|
-
* Reads the ERC8004ValidationModule's validationRegistry — when set to
|
|
92
|
-
* a non-zero address, the module enforces KYA code approval.
|
|
93
|
-
*/
|
|
94
|
-
async isKyaRequired() {
|
|
95
|
-
try {
|
|
96
|
-
const registryAddr = await this.validationModule.validationRegistry();
|
|
97
|
-
return registryAddr !== ethers.ZeroAddress;
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
// ════════════════════════════════════════════════════════
|
|
104
|
-
// Account Management
|
|
105
|
-
// ════════════════════════════════════════════════════════
|
|
106
|
-
/** Resolve the AgentAccount address (cached, with retry for flaky RPCs). */
|
|
107
|
-
async getAccountAddress() {
|
|
108
|
-
if (this._accountAddress)
|
|
109
|
-
return this._accountAddress;
|
|
110
|
-
if (!this.agentId)
|
|
111
|
-
throw new AgetherError('agentId not set', 'NO_AGENT_ID');
|
|
112
|
-
const MAX_RETRIES = 3;
|
|
113
|
-
let lastErr;
|
|
114
|
-
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
115
|
-
try {
|
|
116
|
-
const addr = await this.agether4337Factory.getAccount(BigInt(this.agentId));
|
|
117
|
-
if (addr === ethers.ZeroAddress) {
|
|
118
|
-
throw new AgetherError('No AgentAccount found. Call register() first.', 'NO_ACCOUNT');
|
|
119
|
-
}
|
|
120
|
-
this._accountAddress = addr;
|
|
121
|
-
return addr;
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
// Don't retry application-level errors (NO_ACCOUNT)
|
|
125
|
-
if (err instanceof AgetherError)
|
|
126
|
-
throw err;
|
|
127
|
-
lastErr = err;
|
|
128
|
-
if (attempt < MAX_RETRIES) {
|
|
129
|
-
await new Promise((r) => setTimeout(r, 500 * attempt)); // backoff
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
throw lastErr;
|
|
134
|
-
}
|
|
135
|
-
getAgentId() {
|
|
136
|
-
if (!this.agentId)
|
|
137
|
-
throw new AgetherError('agentId not set', 'NO_AGENT_ID');
|
|
138
|
-
return this.agentId;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Get the EOA wallet address (synchronous, best-effort).
|
|
142
|
-
*
|
|
143
|
-
* For the `privateKey` path this always works. For the `signer` path
|
|
144
|
-
* it works if the signer exposes `.address` synchronously (e.g. ethers.Wallet).
|
|
145
|
-
* If the address has not been resolved yet, throws — call `getSignerAddress()` first.
|
|
146
|
-
*/
|
|
147
|
-
getWalletAddress() {
|
|
148
|
-
if (this._eoaAddress)
|
|
149
|
-
return this._eoaAddress;
|
|
150
|
-
// Try sync access for Wallet-like signers
|
|
151
|
-
const signer = this._signer;
|
|
152
|
-
if (signer.address && typeof signer.address === 'string') {
|
|
153
|
-
const addr = signer.address;
|
|
154
|
-
this._eoaAddress = addr;
|
|
155
|
-
return addr;
|
|
156
|
-
}
|
|
157
|
-
throw new AgetherError('EOA address not yet resolved. Call getSignerAddress() (async) first, or use a signer that exposes .address synchronously.', 'ADDRESS_NOT_RESOLVED');
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Resolve the EOA signer address (async, works with all signer types).
|
|
161
|
-
* Result is cached after the first call.
|
|
162
|
-
*/
|
|
163
|
-
async getSignerAddress() {
|
|
164
|
-
if (!this._eoaAddress) {
|
|
165
|
-
this._eoaAddress = await this._signer.getAddress();
|
|
166
|
-
}
|
|
167
|
-
return this._eoaAddress;
|
|
168
|
-
}
|
|
169
|
-
/** Mint a new ERC-8004 identity and return the agentId. */
|
|
170
|
-
async _mintNewIdentity() {
|
|
171
|
-
const regTx = await this.identityRegistry.register();
|
|
172
|
-
const regReceipt = await regTx.wait();
|
|
173
|
-
this._refreshSigner();
|
|
174
|
-
let agentId = 0n;
|
|
175
|
-
for (const log of regReceipt.logs) {
|
|
176
|
-
try {
|
|
177
|
-
const parsed = this.identityRegistry.interface.parseLog({ topics: log.topics, data: log.data });
|
|
178
|
-
if (parsed?.name === 'Transfer') {
|
|
179
|
-
agentId = parsed.args[2];
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
catch (e) {
|
|
184
|
-
console.warn('[agether] parseLog skip:', e instanceof Error ? e.message : e);
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (agentId === 0n)
|
|
189
|
-
throw new AgetherError('Failed to parse agentId from registration', 'PARSE_ERROR');
|
|
190
|
-
return agentId;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Register: create ERC-8004 identity + AgentAccount in one flow.
|
|
194
|
-
* If already registered, returns existing state.
|
|
195
|
-
*/
|
|
196
|
-
async register(_name) {
|
|
197
|
-
const eoaAddr = await this.getSignerAddress();
|
|
198
|
-
// Check if we already have an agentId
|
|
199
|
-
if (this.agentId) {
|
|
200
|
-
const exists = await this.agether4337Factory.accountExists(BigInt(this.agentId));
|
|
201
|
-
if (exists) {
|
|
202
|
-
const acct = await this.agether4337Factory.getAccount(BigInt(this.agentId));
|
|
203
|
-
this._accountAddress = acct;
|
|
204
|
-
const kyaRequired = await this.isKyaRequired();
|
|
205
|
-
return { agentId: this.agentId, address: eoaAddr, agentAccount: acct, alreadyRegistered: true, kyaRequired };
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
// Check if wallet already has an identity AND we know the agentId
|
|
209
|
-
let agentId;
|
|
210
|
-
if (this.agentId) {
|
|
211
|
-
// We have an agentId in config — reuse it
|
|
212
|
-
const balance = await this.identityRegistry.balanceOf(eoaAddr);
|
|
213
|
-
if (balance > 0n) {
|
|
214
|
-
agentId = BigInt(this.agentId);
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
// agentId in config but no onchain identity — register fresh
|
|
218
|
-
agentId = await this._mintNewIdentity();
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
// No agentId — always register a new identity (wallets can have multiple ERC-8004 tokens)
|
|
223
|
-
agentId = await this._mintNewIdentity();
|
|
224
|
-
}
|
|
225
|
-
this.agentId = agentId.toString();
|
|
226
|
-
// Create AgentAccount if needed
|
|
227
|
-
const acctExists = await this.agether4337Factory.accountExists(agentId);
|
|
228
|
-
let txHash;
|
|
229
|
-
if (!acctExists) {
|
|
230
|
-
const tx = await this.agether4337Factory.createAccount(agentId);
|
|
231
|
-
const receipt = await tx.wait();
|
|
232
|
-
this._refreshSigner();
|
|
233
|
-
txHash = receipt.hash;
|
|
234
|
-
}
|
|
235
|
-
const acctAddr = await this.agether4337Factory.getAccount(agentId);
|
|
236
|
-
this._accountAddress = acctAddr;
|
|
237
|
-
const kyaRequired = await this.isKyaRequired();
|
|
238
|
-
return {
|
|
239
|
-
agentId: this.agentId,
|
|
240
|
-
address: eoaAddr,
|
|
241
|
-
agentAccount: acctAddr,
|
|
242
|
-
alreadyRegistered: acctExists,
|
|
243
|
-
kyaRequired,
|
|
244
|
-
tx: txHash,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
/** Get ETH / USDC / collateral balances for EOA and AgentAccount. */
|
|
248
|
-
async getBalances() {
|
|
249
|
-
const eoaAddr = await this.getSignerAddress();
|
|
250
|
-
const usdc = new Contract(this.config.contracts.usdc, ERC20_ABI, this.provider);
|
|
251
|
-
const ethBal = await this.provider.getBalance(eoaAddr);
|
|
252
|
-
const usdcBal = await usdc.balanceOf(eoaAddr);
|
|
253
|
-
// Fetch collateral token balances for EOA
|
|
254
|
-
const eoaCollateral = {};
|
|
255
|
-
for (const [symbol, info] of Object.entries(BASE_COLLATERALS)) {
|
|
256
|
-
try {
|
|
257
|
-
const token = new Contract(info.address, ERC20_ABI, this.provider);
|
|
258
|
-
const bal = await token.balanceOf(eoaAddr);
|
|
259
|
-
eoaCollateral[symbol] = ethers.formatUnits(bal, info.decimals);
|
|
260
|
-
}
|
|
261
|
-
catch (e) {
|
|
262
|
-
console.warn(`[agether] EOA collateral fetch failed for ${symbol}:`, e instanceof Error ? e.message : e);
|
|
263
|
-
eoaCollateral[symbol] = '0';
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
const result = {
|
|
267
|
-
agentId: this.agentId || '?',
|
|
268
|
-
address: eoaAddr,
|
|
269
|
-
eth: ethers.formatEther(ethBal),
|
|
270
|
-
usdc: ethers.formatUnits(usdcBal, 6),
|
|
271
|
-
collateral: eoaCollateral,
|
|
272
|
-
};
|
|
273
|
-
try {
|
|
274
|
-
const acctAddr = await this.getAccountAddress();
|
|
275
|
-
const acctEth = await this.provider.getBalance(acctAddr);
|
|
276
|
-
const acctUsdc = await usdc.balanceOf(acctAddr);
|
|
277
|
-
// Fetch collateral token balances for AgentAccount
|
|
278
|
-
const acctCollateral = {};
|
|
279
|
-
for (const [symbol, info] of Object.entries(BASE_COLLATERALS)) {
|
|
280
|
-
try {
|
|
281
|
-
const token = new Contract(info.address, ERC20_ABI, this.provider);
|
|
282
|
-
const bal = await token.balanceOf(acctAddr);
|
|
283
|
-
acctCollateral[symbol] = ethers.formatUnits(bal, info.decimals);
|
|
284
|
-
}
|
|
285
|
-
catch (e) {
|
|
286
|
-
console.warn(`[agether] AgentAccount collateral fetch failed for ${symbol}:`, e instanceof Error ? e.message : e);
|
|
287
|
-
acctCollateral[symbol] = '0';
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
result.agentAccount = {
|
|
291
|
-
address: acctAddr,
|
|
292
|
-
eth: ethers.formatEther(acctEth),
|
|
293
|
-
usdc: ethers.formatUnits(acctUsdc, 6),
|
|
294
|
-
collateral: acctCollateral,
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
catch (err) {
|
|
298
|
-
// Only swallow NO_ACCOUNT (not registered); rethrow RPC / other errors
|
|
299
|
-
if (err instanceof AgetherError && err.code === 'NO_ACCOUNT') {
|
|
300
|
-
// genuinely no account — leave agentAccount undefined
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
// Unexpected error (likely RPC flake) — still surface it so callers know
|
|
304
|
-
console.warn('[agether] getBalances: failed to fetch AgentAccount data:', err.message ?? err);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return result;
|
|
308
|
-
}
|
|
309
|
-
/** Transfer USDC from EOA to AgentAccount. */
|
|
310
|
-
async fundAccount(usdcAmount) {
|
|
311
|
-
const acctAddr = await this.getAccountAddress();
|
|
312
|
-
const usdc = new Contract(this.config.contracts.usdc, ERC20_ABI, this._signer);
|
|
313
|
-
const amount = ethers.parseUnits(usdcAmount, 6);
|
|
314
|
-
const tx = await usdc.transfer(acctAddr, amount);
|
|
315
|
-
const receipt = await tx.wait();
|
|
316
|
-
this._refreshSigner();
|
|
317
|
-
return { tx: receipt.hash, amount: usdcAmount, agentAccount: acctAddr };
|
|
318
|
-
}
|
|
319
|
-
// ════════════════════════════════════════════════════════
|
|
320
|
-
// Market Discovery (Morpho GraphQL API)
|
|
321
|
-
// ════════════════════════════════════════════════════════
|
|
322
|
-
/**
|
|
323
|
-
* Fetch USDC borrow markets on Base from Morpho API.
|
|
324
|
-
* Caches results for 5 minutes.
|
|
325
|
-
*/
|
|
326
|
-
async getMarkets(forceRefresh = false) {
|
|
327
|
-
if (!forceRefresh && this._discoveredMarkets && Date.now() - this._discoveredAt < 300000) {
|
|
328
|
-
return this._discoveredMarkets;
|
|
329
|
-
}
|
|
330
|
-
const chainId = this.config.chainId;
|
|
331
|
-
const usdcAddr = this.config.contracts.usdc.toLowerCase();
|
|
332
|
-
const query = `{
|
|
333
|
-
markets(
|
|
334
|
-
first: 50
|
|
335
|
-
orderBy: SupplyAssetsUsd
|
|
336
|
-
orderDirection: Desc
|
|
337
|
-
where: { chainId_in: [${chainId}], loanAssetAddress_in: ["${usdcAddr}"] }
|
|
338
|
-
) {
|
|
339
|
-
items {
|
|
340
|
-
uniqueKey
|
|
341
|
-
lltv
|
|
342
|
-
oracleAddress
|
|
343
|
-
irmAddress
|
|
344
|
-
loanAsset { address symbol decimals }
|
|
345
|
-
collateralAsset { address symbol decimals }
|
|
346
|
-
state {
|
|
347
|
-
borrowAssets
|
|
348
|
-
supplyAssets
|
|
349
|
-
utilization
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}`;
|
|
354
|
-
try {
|
|
355
|
-
const resp = await axios.post(MORPHO_API_URL, { query }, { timeout: 10000 });
|
|
356
|
-
const items = resp.data?.data?.markets?.items ?? [];
|
|
357
|
-
this._discoveredMarkets = items.map((m) => ({
|
|
358
|
-
uniqueKey: m.uniqueKey,
|
|
359
|
-
loanAsset: m.loanAsset,
|
|
360
|
-
collateralAsset: m.collateralAsset ?? { address: ethers.ZeroAddress, symbol: 'N/A', decimals: 0 },
|
|
361
|
-
oracle: m.oracleAddress,
|
|
362
|
-
irm: m.irmAddress,
|
|
363
|
-
lltv: BigInt(m.lltv),
|
|
364
|
-
totalSupplyAssets: BigInt(m.state?.supplyAssets ?? '0'),
|
|
365
|
-
totalBorrowAssets: BigInt(m.state?.borrowAssets ?? '0'),
|
|
366
|
-
utilization: m.state?.utilization ? Number(m.state.utilization) : 0,
|
|
367
|
-
}));
|
|
368
|
-
this._discoveredAt = Date.now();
|
|
369
|
-
// Warm the params cache
|
|
370
|
-
for (const mi of this._discoveredMarkets) {
|
|
371
|
-
if (mi.collateralAsset.address !== ethers.ZeroAddress) {
|
|
372
|
-
this._marketCache.set(mi.collateralAsset.address.toLowerCase(), {
|
|
373
|
-
loanToken: mi.loanAsset.address,
|
|
374
|
-
collateralToken: mi.collateralAsset.address,
|
|
375
|
-
oracle: mi.oracle,
|
|
376
|
-
irm: mi.irm,
|
|
377
|
-
lltv: mi.lltv,
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
return this._discoveredMarkets;
|
|
382
|
-
}
|
|
383
|
-
catch (e) {
|
|
384
|
-
console.warn('[agether] getMarkets failed, using cache:', e instanceof Error ? e.message : e);
|
|
385
|
-
// Fallback: return cached or empty
|
|
386
|
-
return this._discoveredMarkets ?? [];
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Get MarketParams for a collateral token.
|
|
391
|
-
* Tries cache → API → onchain idToMarketParams.
|
|
392
|
-
*/
|
|
393
|
-
async findMarketForCollateral(collateralSymbolOrAddress) {
|
|
394
|
-
// Resolve symbol → address
|
|
395
|
-
const colInfo = BASE_COLLATERALS[collateralSymbolOrAddress];
|
|
396
|
-
const colAddr = (colInfo?.address ?? collateralSymbolOrAddress).toLowerCase();
|
|
397
|
-
// Check cache
|
|
398
|
-
const cached = this._marketCache.get(colAddr);
|
|
399
|
-
if (cached)
|
|
400
|
-
return cached;
|
|
401
|
-
// Try API discovery
|
|
402
|
-
await this.getMarkets();
|
|
403
|
-
const fromApi = this._marketCache.get(colAddr);
|
|
404
|
-
if (fromApi)
|
|
405
|
-
return fromApi;
|
|
406
|
-
throw new AgetherError(`No Morpho market found for collateral ${collateralSymbolOrAddress}`, 'MARKET_NOT_FOUND');
|
|
407
|
-
}
|
|
408
|
-
/** Read MarketParams onchain by market ID (bytes32). */
|
|
409
|
-
async getMarketParams(marketId) {
|
|
410
|
-
const result = await this.morphoBlue.idToMarketParams(marketId);
|
|
411
|
-
return {
|
|
412
|
-
loanToken: result.loanToken,
|
|
413
|
-
collateralToken: result.collateralToken,
|
|
414
|
-
oracle: result.oracle,
|
|
415
|
-
irm: result.irm,
|
|
416
|
-
lltv: result.lltv,
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
// ════════════════════════════════════════════════════════
|
|
420
|
-
// Position Reads
|
|
421
|
-
// ════════════════════════════════════════════════════════
|
|
422
|
-
/** Read onchain position for a specific market. */
|
|
423
|
-
async getPosition(marketId) {
|
|
424
|
-
const acctAddr = await this.getAccountAddress();
|
|
425
|
-
const pos = await this.morphoBlue.position(marketId, acctAddr);
|
|
426
|
-
return {
|
|
427
|
-
supplyShares: pos.supplyShares,
|
|
428
|
-
borrowShares: pos.borrowShares,
|
|
429
|
-
collateral: pos.collateral,
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
/**
|
|
433
|
-
* Full status: positions across all discovered markets.
|
|
434
|
-
*/
|
|
435
|
-
async getStatus() {
|
|
436
|
-
const acctAddr = await this.getAccountAddress();
|
|
437
|
-
const markets = await this.getMarkets();
|
|
438
|
-
const positions = [];
|
|
439
|
-
let totalDebt = 0n;
|
|
440
|
-
for (const m of markets) {
|
|
441
|
-
if (!m.collateralAsset || m.collateralAsset.address === ethers.ZeroAddress)
|
|
442
|
-
continue;
|
|
443
|
-
try {
|
|
444
|
-
const pos = await this.morphoBlue.position(m.uniqueKey, acctAddr);
|
|
445
|
-
if (pos.collateral === 0n && pos.borrowShares === 0n && pos.supplyShares === 0n)
|
|
446
|
-
continue;
|
|
447
|
-
// Estimate debt from shares (ceil division — shows actual amount owed, never hides dust)
|
|
448
|
-
let debt = 0n;
|
|
449
|
-
if (pos.borrowShares > 0n) {
|
|
450
|
-
try {
|
|
451
|
-
const mkt = await this.morphoBlue.market(m.uniqueKey);
|
|
452
|
-
const totalBorrowShares = BigInt(mkt.totalBorrowShares);
|
|
453
|
-
const totalBorrowAssets = BigInt(mkt.totalBorrowAssets);
|
|
454
|
-
debt = totalBorrowShares > 0n
|
|
455
|
-
? (BigInt(pos.borrowShares) * totalBorrowAssets + totalBorrowShares - 1n) / totalBorrowShares
|
|
456
|
-
: 0n;
|
|
457
|
-
totalDebt += debt;
|
|
458
|
-
}
|
|
459
|
-
catch (e) {
|
|
460
|
-
console.warn(`[agether] debt calc failed for market ${m.uniqueKey}:`, e instanceof Error ? e.message : e);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
positions.push({
|
|
464
|
-
marketId: m.uniqueKey,
|
|
465
|
-
collateralToken: m.collateralAsset.symbol,
|
|
466
|
-
collateral: ethers.formatUnits(pos.collateral, m.collateralAsset.decimals),
|
|
467
|
-
borrowShares: pos.borrowShares.toString(),
|
|
468
|
-
supplyShares: pos.supplyShares.toString(),
|
|
469
|
-
debt: ethers.formatUnits(debt, 6),
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
catch (e) {
|
|
473
|
-
console.warn(`[agether] position read failed for market:`, e instanceof Error ? e.message : e);
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return {
|
|
478
|
-
agentId: this.agentId || '?',
|
|
479
|
-
agentAccount: acctAddr,
|
|
480
|
-
totalDebt: ethers.formatUnits(totalDebt, 6),
|
|
481
|
-
positions,
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
// ════════════════════════════════════════════════════════
|
|
485
|
-
// Balance & Borrowing Capacity
|
|
486
|
-
// ════════════════════════════════════════════════════════
|
|
487
|
-
/**
|
|
488
|
-
* Get the USDC balance of the AgentAccount.
|
|
489
|
-
* @returns USDC balance in raw units (6 decimals)
|
|
490
|
-
*/
|
|
491
|
-
async getUsdcBalance() {
|
|
492
|
-
const acctAddr = await this.getAccountAddress();
|
|
493
|
-
const usdc = new Contract(this.config.contracts.usdc, ERC20_ABI, this.provider);
|
|
494
|
-
return usdc.balanceOf(acctAddr);
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* Calculate the maximum additional USDC that can be borrowed
|
|
498
|
-
* given the agent's current collateral and debt across all markets.
|
|
499
|
-
*
|
|
500
|
-
* For each market with collateral deposited:
|
|
501
|
-
* maxBorrow = (collateralValue * LLTV) - currentDebt
|
|
502
|
-
*
|
|
503
|
-
* Uses the Morpho oracle to price collateral → loan token.
|
|
504
|
-
*
|
|
505
|
-
* @returns Maximum additional USDC borrowable (6 decimals)
|
|
506
|
-
*/
|
|
507
|
-
async getMaxBorrowable() {
|
|
508
|
-
const acctAddr = await this.getAccountAddress();
|
|
509
|
-
const markets = await this.getMarkets();
|
|
510
|
-
let totalAdditional = 0n;
|
|
511
|
-
const byMarket = [];
|
|
512
|
-
for (const m of markets) {
|
|
513
|
-
if (!m.collateralAsset || m.collateralAsset.address === ethers.ZeroAddress)
|
|
514
|
-
continue;
|
|
515
|
-
try {
|
|
516
|
-
const pos = await this.morphoBlue.position(m.uniqueKey, acctAddr);
|
|
517
|
-
if (pos.collateral === 0n)
|
|
518
|
-
continue;
|
|
519
|
-
// Get market state for debt calculation (ceil division for accurate debt)
|
|
520
|
-
const mktState = await this.morphoBlue.market(m.uniqueKey);
|
|
521
|
-
const totalBorrowShares = BigInt(mktState.totalBorrowShares);
|
|
522
|
-
const totalBorrowAssets = BigInt(mktState.totalBorrowAssets);
|
|
523
|
-
const currentDebt = totalBorrowShares > 0n
|
|
524
|
-
? (BigInt(pos.borrowShares) * totalBorrowAssets + totalBorrowShares - 1n) / totalBorrowShares
|
|
525
|
-
: 0n;
|
|
526
|
-
// Get oracle price for collateral → USDC conversion
|
|
527
|
-
// Morpho oracle returns price scaled to 36 + loanDecimals - collateralDecimals
|
|
528
|
-
// price = collateral * oraclePrice / 10^36
|
|
529
|
-
let collateralValueInLoan;
|
|
530
|
-
try {
|
|
531
|
-
const oracleContract = new Contract(m.oracle, [
|
|
532
|
-
'function price() view returns (uint256)',
|
|
533
|
-
], this.provider);
|
|
534
|
-
const oraclePrice = await oracleContract.price();
|
|
535
|
-
const ORACLE_PRICE_SCALE = 10n ** 36n;
|
|
536
|
-
collateralValueInLoan = (BigInt(pos.collateral) * oraclePrice) / ORACLE_PRICE_SCALE;
|
|
537
|
-
}
|
|
538
|
-
catch (e) {
|
|
539
|
-
console.warn(`[agether] oracle price fetch failed:`, e instanceof Error ? e.message : e);
|
|
540
|
-
continue;
|
|
541
|
-
}
|
|
542
|
-
// maxBorrow = collateralValue * LLTV / 1e18 - currentDebt
|
|
543
|
-
const maxBorrowTotal = (collateralValueInLoan * m.lltv) / (10n ** 18n);
|
|
544
|
-
const maxAdditional = maxBorrowTotal > currentDebt ? maxBorrowTotal - currentDebt : 0n;
|
|
545
|
-
totalAdditional += maxAdditional;
|
|
546
|
-
byMarket.push({
|
|
547
|
-
collateralToken: m.collateralAsset.symbol,
|
|
548
|
-
maxAdditional,
|
|
549
|
-
currentDebt,
|
|
550
|
-
collateralValue: collateralValueInLoan,
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
catch (e) {
|
|
554
|
-
console.warn(`[agether] maxBorrow calc failed:`, e instanceof Error ? e.message : e);
|
|
555
|
-
continue;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
return { total: totalAdditional, byMarket };
|
|
559
|
-
}
|
|
560
|
-
// ════════════════════════════════════════════════════════
|
|
561
|
-
// Market Rates & Yield Estimation
|
|
562
|
-
// ════════════════════════════════════════════════════════
|
|
563
|
-
/**
|
|
564
|
-
* Fetch current supply/borrow APY for a collateral market from Morpho GraphQL API.
|
|
565
|
-
*
|
|
566
|
-
* Note: On Morpho Blue, collateral does NOT earn yield directly. Supply APY
|
|
567
|
-
* is what lenders earn; borrow APY is what borrowers pay.
|
|
568
|
-
*/
|
|
569
|
-
async getMarketRates(collateralSymbolOrAddress) {
|
|
570
|
-
const chainId = this.config.chainId;
|
|
571
|
-
const usdcAddr = this.config.contracts.usdc.toLowerCase();
|
|
572
|
-
// Optionally filter by collateral
|
|
573
|
-
let collateralFilter = '';
|
|
574
|
-
if (collateralSymbolOrAddress) {
|
|
575
|
-
const colInfo = BASE_COLLATERALS[collateralSymbolOrAddress];
|
|
576
|
-
const colAddr = (colInfo?.address ?? collateralSymbolOrAddress).toLowerCase();
|
|
577
|
-
collateralFilter = `, collateralAssetAddress_in: ["${colAddr}"]`;
|
|
578
|
-
}
|
|
579
|
-
const query = `{
|
|
580
|
-
markets(
|
|
581
|
-
first: 50
|
|
582
|
-
orderBy: SupplyAssetsUsd
|
|
583
|
-
orderDirection: Desc
|
|
584
|
-
where: { chainId_in: [${chainId}], loanAssetAddress_in: ["${usdcAddr}"]${collateralFilter} }
|
|
585
|
-
) {
|
|
586
|
-
items {
|
|
587
|
-
uniqueKey
|
|
588
|
-
lltv
|
|
589
|
-
loanAsset { address symbol decimals }
|
|
590
|
-
collateralAsset { address symbol decimals }
|
|
591
|
-
state {
|
|
592
|
-
borrowAssets
|
|
593
|
-
supplyAssets
|
|
594
|
-
utilization
|
|
595
|
-
supplyApy
|
|
596
|
-
borrowApy
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}`;
|
|
601
|
-
try {
|
|
602
|
-
const resp = await axios.post(MORPHO_API_URL, { query }, { timeout: 10000 });
|
|
603
|
-
const items = resp.data?.data?.markets?.items ?? [];
|
|
604
|
-
return items
|
|
605
|
-
.filter((m) => m.collateralAsset?.address && m.collateralAsset.address !== ethers.ZeroAddress)
|
|
606
|
-
.map((m) => ({
|
|
607
|
-
collateralToken: m.collateralAsset.symbol,
|
|
608
|
-
loanToken: m.loanAsset.symbol,
|
|
609
|
-
supplyApy: m.state?.supplyApy ? Number(m.state.supplyApy) : 0,
|
|
610
|
-
borrowApy: m.state?.borrowApy ? Number(m.state.borrowApy) : 0,
|
|
611
|
-
utilization: m.state?.utilization ? Number(m.state.utilization) : 0,
|
|
612
|
-
totalSupplyUsd: m.state?.supplyAssets ? Number(m.state.supplyAssets) / 1e6 : 0,
|
|
613
|
-
totalBorrowUsd: m.state?.borrowAssets ? Number(m.state.borrowAssets) / 1e6 : 0,
|
|
614
|
-
lltv: `${(Number(m.lltv) / 1e16).toFixed(0)}%`,
|
|
615
|
-
marketId: m.uniqueKey,
|
|
616
|
-
}));
|
|
617
|
-
}
|
|
618
|
-
catch (e) {
|
|
619
|
-
console.warn('[agether] getMarketRates failed:', e instanceof Error ? e.message : e);
|
|
620
|
-
return [];
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
/**
|
|
624
|
-
* Estimate theoretical yield for a given collateral amount over a period.
|
|
625
|
-
*
|
|
626
|
-
* ⚠️ IMPORTANT: On Morpho Blue, collateral does NOT earn yield directly.
|
|
627
|
-
* This estimates what the collateral WOULD earn if it were instead supplied
|
|
628
|
-
* as a lender (not used as collateral). This is a theoretical upper bound
|
|
629
|
-
* useful for setting spending caps.
|
|
630
|
-
*
|
|
631
|
-
* @param collateralSymbol - e.g. 'WETH'
|
|
632
|
-
* @param amount - collateral amount in human-readable (e.g. '1.5')
|
|
633
|
-
* @param periodDays - estimation period in days (default: 1)
|
|
634
|
-
* @param ethPriceUsd - ETH price in USD for value conversion (if not provided, uses oracle)
|
|
635
|
-
* @returns Estimated yield in USD for the period
|
|
636
|
-
*/
|
|
637
|
-
async getYieldEstimate(collateralSymbol, amount, periodDays = 1, ethPriceUsd) {
|
|
638
|
-
const colInfo = BASE_COLLATERALS[collateralSymbol];
|
|
639
|
-
if (!colInfo)
|
|
640
|
-
throw new AgetherError(`Unknown collateral: ${collateralSymbol}`, 'UNKNOWN_COLLATERAL');
|
|
641
|
-
// Get market rates for this collateral
|
|
642
|
-
const rates = await this.getMarketRates(collateralSymbol);
|
|
643
|
-
if (rates.length === 0) {
|
|
644
|
-
throw new AgetherError(`No market found for ${collateralSymbol}`, 'MARKET_NOT_FOUND');
|
|
645
|
-
}
|
|
646
|
-
// Use the most liquid market (first one, sorted by supply)
|
|
647
|
-
const market = rates[0];
|
|
648
|
-
const supplyApy = market.supplyApy;
|
|
649
|
-
// Determine collateral value in USD
|
|
650
|
-
let collateralValueUsd;
|
|
651
|
-
if (ethPriceUsd) {
|
|
652
|
-
collateralValueUsd = parseFloat(amount) * ethPriceUsd;
|
|
653
|
-
}
|
|
654
|
-
else {
|
|
655
|
-
try {
|
|
656
|
-
const params = await this.findMarketForCollateral(collateralSymbol);
|
|
657
|
-
const oracleContract = new Contract(params.oracle, [
|
|
658
|
-
'function price() view returns (uint256)',
|
|
659
|
-
], this.provider);
|
|
660
|
-
const oraclePrice = await oracleContract.price();
|
|
661
|
-
const ORACLE_PRICE_SCALE = 10n ** 36n;
|
|
662
|
-
const amountWei = ethers.parseUnits(amount, colInfo.decimals);
|
|
663
|
-
const valueInUsdc = (amountWei * oraclePrice) / ORACLE_PRICE_SCALE;
|
|
664
|
-
collateralValueUsd = Number(valueInUsdc) / 1e6;
|
|
665
|
-
}
|
|
666
|
-
catch (e) {
|
|
667
|
-
console.warn('[agether] oracle price fetch for yield estimation failed:', e instanceof Error ? e.message : e);
|
|
668
|
-
throw new AgetherError('Cannot determine collateral value. Provide ethPriceUsd.', 'PRICE_UNAVAILABLE');
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
// Calculate yield: value * APY * (days / 365)
|
|
672
|
-
const estimatedYieldUsd = collateralValueUsd * supplyApy * (periodDays / 365);
|
|
673
|
-
return {
|
|
674
|
-
collateralToken: collateralSymbol,
|
|
675
|
-
amount,
|
|
676
|
-
periodDays,
|
|
677
|
-
theoreticalSupplyApy: supplyApy,
|
|
678
|
-
estimatedYieldUsd,
|
|
679
|
-
collateralValueUsd,
|
|
680
|
-
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.',
|
|
681
|
-
};
|
|
682
|
-
}
|
|
683
|
-
// ════════════════════════════════════════════════════════
|
|
684
|
-
// Supply-Side (Lending) — earn yield by supplying USDC
|
|
685
|
-
// ════════════════════════════════════════════════════════
|
|
686
|
-
/**
|
|
687
|
-
* Supply USDC to a Morpho Blue market as a lender (earn yield).
|
|
688
|
-
*
|
|
689
|
-
* Unlike `supplyCollateral` (borrower-side), this is the **lender-side**:
|
|
690
|
-
* you deposit the loanToken (USDC) into the market's supply pool and earn
|
|
691
|
-
* interest paid by borrowers.
|
|
692
|
-
*
|
|
693
|
-
* @param usdcAmount - Amount of USDC to supply (e.g. '500')
|
|
694
|
-
* @param collateralSymbol - Market collateral token to identify which market (e.g. 'WETH')
|
|
695
|
-
* Optional — defaults to highest-APY market
|
|
696
|
-
*/
|
|
697
|
-
async supplyAsset(usdcAmount, collateralSymbol) {
|
|
698
|
-
const acctAddr = await this.getAccountAddress();
|
|
699
|
-
const amount = ethers.parseUnits(usdcAmount, 6);
|
|
700
|
-
const morphoAddr = this.config.contracts.morphoBlue;
|
|
701
|
-
const usdcAddr = this.config.contracts.usdc;
|
|
702
|
-
// Find market
|
|
703
|
-
let params;
|
|
704
|
-
let usedCollateral;
|
|
705
|
-
if (collateralSymbol) {
|
|
706
|
-
params = await this.findMarketForCollateral(collateralSymbol);
|
|
707
|
-
usedCollateral = collateralSymbol;
|
|
708
|
-
}
|
|
709
|
-
else {
|
|
710
|
-
// Auto-pick highest APY market
|
|
711
|
-
const rates = await this.getMarketRates();
|
|
712
|
-
if (rates.length === 0)
|
|
713
|
-
throw new AgetherError('No markets available', 'NO_MARKETS');
|
|
714
|
-
const best = rates.reduce((a, b) => a.supplyApy > b.supplyApy ? a : b);
|
|
715
|
-
params = await this.findMarketForCollateral(best.collateralToken);
|
|
716
|
-
usedCollateral = best.collateralToken;
|
|
717
|
-
}
|
|
718
|
-
// Compute marketId for result
|
|
719
|
-
const marketId = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(['address', 'address', 'address', 'address', 'uint256'], [params.loanToken, params.collateralToken, params.oracle, params.irm, params.lltv]));
|
|
720
|
-
// Ensure AgentAccount has enough USDC (transfer shortfall from EOA)
|
|
721
|
-
const usdcContract = new Contract(usdcAddr, ERC20_ABI, this._signer);
|
|
722
|
-
const acctBalance = await usdcContract.balanceOf(acctAddr);
|
|
723
|
-
if (acctBalance < amount) {
|
|
724
|
-
const shortfall = amount - acctBalance;
|
|
725
|
-
const eoaBalance = await usdcContract.balanceOf(await this.getSignerAddress());
|
|
726
|
-
if (eoaBalance < shortfall) {
|
|
727
|
-
throw new AgetherError(`Insufficient USDC. Need ${usdcAmount}, AgentAccount has ${ethers.formatUnits(acctBalance, 6)}, EOA has ${ethers.formatUnits(eoaBalance, 6)}.`, 'INSUFFICIENT_BALANCE');
|
|
728
|
-
}
|
|
729
|
-
const transferTx = await usdcContract.transfer(acctAddr, shortfall);
|
|
730
|
-
await transferTx.wait();
|
|
731
|
-
this._refreshSigner();
|
|
732
|
-
}
|
|
733
|
-
// Batch: approve USDC → supply(marketParams, assets, 0, onBehalf, "0x")
|
|
734
|
-
const targets = [usdcAddr, morphoAddr];
|
|
735
|
-
const values = [0n, 0n];
|
|
736
|
-
const datas = [
|
|
737
|
-
erc20Iface.encodeFunctionData('approve', [morphoAddr, amount]),
|
|
738
|
-
morphoIface.encodeFunctionData('supply', [
|
|
739
|
-
this._toTuple(params), amount, 0n, acctAddr, '0x',
|
|
740
|
-
]),
|
|
741
|
-
];
|
|
742
|
-
const receipt = await this.batch(targets, values, datas);
|
|
743
|
-
return {
|
|
744
|
-
tx: receipt.hash,
|
|
745
|
-
amount: usdcAmount,
|
|
746
|
-
marketId,
|
|
747
|
-
collateralToken: usedCollateral,
|
|
748
|
-
agentAccount: acctAddr,
|
|
749
|
-
};
|
|
750
|
-
}
|
|
751
|
-
/**
|
|
752
|
-
* Withdraw supplied USDC (+ earned interest) from a Morpho Blue market.
|
|
753
|
-
*
|
|
754
|
-
* @param usdcAmount - Amount to withdraw (e.g. '100' or 'all' for full position)
|
|
755
|
-
* @param collateralSymbol - Market collateral to identify which market
|
|
756
|
-
* @param receiver - Destination address (defaults to EOA)
|
|
757
|
-
*/
|
|
758
|
-
async withdrawSupply(usdcAmount, collateralSymbol, receiver) {
|
|
759
|
-
const acctAddr = await this.getAccountAddress();
|
|
760
|
-
const morphoAddr = this.config.contracts.morphoBlue;
|
|
761
|
-
const dest = receiver || await this.getSignerAddress();
|
|
762
|
-
// Find market
|
|
763
|
-
let params;
|
|
764
|
-
if (collateralSymbol) {
|
|
765
|
-
params = await this.findMarketForCollateral(collateralSymbol);
|
|
766
|
-
}
|
|
767
|
-
else {
|
|
768
|
-
// Find first market with supply position
|
|
769
|
-
const { params: p } = await this._findActiveSupplyMarket();
|
|
770
|
-
params = p;
|
|
771
|
-
}
|
|
772
|
-
const marketId = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(['address', 'address', 'address', 'address', 'uint256'], [params.loanToken, params.collateralToken, params.oracle, params.irm, params.lltv]));
|
|
773
|
-
let withdrawAssets;
|
|
774
|
-
let withdrawShares;
|
|
775
|
-
if (usdcAmount === 'all') {
|
|
776
|
-
// Shares-based withdraw to avoid dust
|
|
777
|
-
const pos = await this.morphoBlue.position(marketId, acctAddr);
|
|
778
|
-
withdrawShares = BigInt(pos.supplyShares);
|
|
779
|
-
withdrawAssets = 0n;
|
|
780
|
-
if (withdrawShares === 0n)
|
|
781
|
-
throw new AgetherError('No supply position to withdraw', 'NO_SUPPLY');
|
|
782
|
-
}
|
|
783
|
-
else {
|
|
784
|
-
withdrawAssets = ethers.parseUnits(usdcAmount, 6);
|
|
785
|
-
withdrawShares = 0n;
|
|
786
|
-
}
|
|
787
|
-
// withdraw(marketParams, assets, shares, onBehalf, receiver)
|
|
788
|
-
const data = morphoIface.encodeFunctionData('withdraw', [
|
|
789
|
-
this._toTuple(params), withdrawAssets, withdrawShares, acctAddr, dest,
|
|
790
|
-
]);
|
|
791
|
-
const receipt = await this.exec(morphoAddr, data);
|
|
792
|
-
// Read remaining supply
|
|
793
|
-
let remainingSupply = '0';
|
|
794
|
-
try {
|
|
795
|
-
const pos = await this.morphoBlue.position(marketId, acctAddr);
|
|
796
|
-
const mkt = await this.morphoBlue.market(marketId);
|
|
797
|
-
const totalSupplyAssets = BigInt(mkt.totalSupplyAssets);
|
|
798
|
-
const totalSupplyShares = BigInt(mkt.totalSupplyShares);
|
|
799
|
-
const currentAssets = totalSupplyShares > 0n
|
|
800
|
-
? (BigInt(pos.supplyShares) * totalSupplyAssets) / totalSupplyShares
|
|
801
|
-
: 0n;
|
|
802
|
-
remainingSupply = ethers.formatUnits(currentAssets, 6);
|
|
803
|
-
}
|
|
804
|
-
catch (e) {
|
|
805
|
-
console.warn('[agether] failed to read remaining supply:', e instanceof Error ? e.message : e);
|
|
806
|
-
}
|
|
807
|
-
return {
|
|
808
|
-
tx: receipt.hash,
|
|
809
|
-
amount: usdcAmount,
|
|
810
|
-
remainingSupply,
|
|
811
|
-
destination: dest,
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
|
-
/**
|
|
815
|
-
* Get supply (lending) position with yield tracking.
|
|
816
|
-
*
|
|
817
|
-
* Yield is computed WITHOUT an indexer/DB:
|
|
818
|
-
* 1. Read current supplyShares → convert to USDC value
|
|
819
|
-
* 2. Read historical Morpho Supply/Withdraw events via eth_getLogs
|
|
820
|
-
* 3. netDeposited = Σ(Supply.assets) − Σ(Withdraw.assets)
|
|
821
|
-
* 4. earnedYield = currentValue − netDeposited
|
|
822
|
-
*
|
|
823
|
-
* @param collateralSymbol - Market collateral token (optional, returns all if omitted)
|
|
824
|
-
*/
|
|
825
|
-
async getSupplyPositions(collateralSymbol) {
|
|
826
|
-
const acctAddr = await this.getAccountAddress();
|
|
827
|
-
const morphoAddr = this.config.contracts.morphoBlue;
|
|
828
|
-
const markets = await this.getMarkets();
|
|
829
|
-
const rates = await this.getMarketRates(collateralSymbol);
|
|
830
|
-
const results = [];
|
|
831
|
-
for (const m of markets) {
|
|
832
|
-
if (!m.collateralAsset || m.collateralAsset.address === ethers.ZeroAddress)
|
|
833
|
-
continue;
|
|
834
|
-
if (collateralSymbol) {
|
|
835
|
-
const colInfo = BASE_COLLATERALS[collateralSymbol];
|
|
836
|
-
const filterAddr = (colInfo?.address ?? collateralSymbol).toLowerCase();
|
|
837
|
-
if (m.collateralAsset.address.toLowerCase() !== filterAddr)
|
|
838
|
-
continue;
|
|
839
|
-
}
|
|
840
|
-
try {
|
|
841
|
-
const pos = await this.morphoBlue.position(m.uniqueKey, acctAddr);
|
|
842
|
-
const supplyShares = BigInt(pos.supplyShares);
|
|
843
|
-
if (supplyShares === 0n)
|
|
844
|
-
continue;
|
|
845
|
-
// Convert shares → current asset value
|
|
846
|
-
const mkt = await this.morphoBlue.market(m.uniqueKey);
|
|
847
|
-
const totalSupplyAssets = BigInt(mkt.totalSupplyAssets);
|
|
848
|
-
const totalSupplyShares = BigInt(mkt.totalSupplyShares);
|
|
849
|
-
const currentAssets = totalSupplyShares > 0n
|
|
850
|
-
? (supplyShares * totalSupplyAssets) / totalSupplyShares
|
|
851
|
-
: 0n;
|
|
852
|
-
// Compute net deposited from events (no DB needed)
|
|
853
|
-
const netDeposited = await this._computeNetDeposited(m.uniqueKey, acctAddr, morphoAddr);
|
|
854
|
-
const earnedYield = currentAssets > netDeposited ? currentAssets - netDeposited : 0n;
|
|
855
|
-
// Find APY from rates
|
|
856
|
-
const rateInfo = rates.find((r) => r.collateralToken === m.collateralAsset.symbol);
|
|
857
|
-
const supplyApy = rateInfo?.supplyApy ?? 0;
|
|
858
|
-
results.push({
|
|
859
|
-
marketId: m.uniqueKey,
|
|
860
|
-
loanToken: m.loanAsset.symbol,
|
|
861
|
-
collateralToken: m.collateralAsset.symbol,
|
|
862
|
-
supplyShares: supplyShares.toString(),
|
|
863
|
-
suppliedAssets: ethers.formatUnits(currentAssets, 6),
|
|
864
|
-
netDeposited: ethers.formatUnits(netDeposited, 6),
|
|
865
|
-
earnedYield: ethers.formatUnits(earnedYield, 6),
|
|
866
|
-
supplyApy,
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
catch (e) {
|
|
870
|
-
console.warn(`[agether] getSupplyPositions failed for market:`, e instanceof Error ? e.message : e);
|
|
871
|
-
continue;
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
return results;
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* Pay a recipient using ONLY earned yield from a supply position.
|
|
878
|
-
*
|
|
879
|
-
* Computes available yield, verifies the requested amount doesn't exceed it,
|
|
880
|
-
* then withdraws from the supply position and sends directly to the recipient.
|
|
881
|
-
*
|
|
882
|
-
* @param recipient - Address to receive the USDC
|
|
883
|
-
* @param usdcAmount - Amount to pay from yield (e.g. '5.50')
|
|
884
|
-
* @param collateralSymbol - Market collateral to identify which supply position
|
|
885
|
-
*/
|
|
886
|
-
async payFromYield(recipient, usdcAmount, collateralSymbol) {
|
|
887
|
-
const acctAddr = await this.getAccountAddress();
|
|
888
|
-
const morphoAddr = this.config.contracts.morphoBlue;
|
|
889
|
-
const amount = ethers.parseUnits(usdcAmount, 6);
|
|
890
|
-
// Get supply position and compute yield
|
|
891
|
-
const positions = await this.getSupplyPositions(collateralSymbol);
|
|
892
|
-
if (positions.length === 0) {
|
|
893
|
-
throw new AgetherError('No supply position found', 'NO_SUPPLY');
|
|
894
|
-
}
|
|
895
|
-
// Use the position with the most yield
|
|
896
|
-
const pos = positions.reduce((a, b) => parseFloat(a.earnedYield) > parseFloat(b.earnedYield) ? a : b);
|
|
897
|
-
const availableYield = ethers.parseUnits(pos.earnedYield, 6);
|
|
898
|
-
if (amount > availableYield) {
|
|
899
|
-
throw new AgetherError(`Requested ${usdcAmount} USDC exceeds available yield of ${pos.earnedYield} USDC. ` +
|
|
900
|
-
`Use withdrawSupply to withdraw principal.`, 'EXCEEDS_YIELD');
|
|
901
|
-
}
|
|
902
|
-
// Find market params
|
|
903
|
-
const params = await this.findMarketForCollateral(pos.collateralToken);
|
|
904
|
-
// Withdraw directly to recipient: withdraw(params, amount, 0, onBehalf, receiver)
|
|
905
|
-
const data = morphoIface.encodeFunctionData('withdraw', [
|
|
906
|
-
this._toTuple(params), amount, 0n, acctAddr, recipient,
|
|
907
|
-
]);
|
|
908
|
-
const receipt = await this.exec(morphoAddr, data);
|
|
909
|
-
// Read remaining position
|
|
910
|
-
let remainingYield = '0';
|
|
911
|
-
let remainingSupply = '0';
|
|
912
|
-
try {
|
|
913
|
-
const updatedPositions = await this.getSupplyPositions(pos.collateralToken);
|
|
914
|
-
if (updatedPositions.length > 0) {
|
|
915
|
-
remainingYield = updatedPositions[0].earnedYield;
|
|
916
|
-
remainingSupply = updatedPositions[0].suppliedAssets;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
catch (e) {
|
|
920
|
-
console.warn('[agether] failed to read remaining yield:', e instanceof Error ? e.message : e);
|
|
921
|
-
}
|
|
922
|
-
return {
|
|
923
|
-
tx: receipt.hash,
|
|
924
|
-
yieldWithdrawn: usdcAmount,
|
|
925
|
-
recipient,
|
|
926
|
-
remainingYield,
|
|
927
|
-
remainingSupply,
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
// ════════════════════════════════════════════════════════
|
|
931
|
-
// Collateral & Borrowing Operations (all via AgentAccount)
|
|
932
|
-
// ════════════════════════════════════════════════════════
|
|
933
|
-
/**
|
|
934
|
-
* Deposit collateral into Morpho Blue.
|
|
935
|
-
*
|
|
936
|
-
* Flow:
|
|
937
|
-
* 1. EOA transfers collateral to AgentAccount
|
|
938
|
-
* 2. AgentAccount.executeBatch:
|
|
939
|
-
* [collateral.approve(MorphoBlue), Morpho.supplyCollateral(params)]
|
|
940
|
-
*/
|
|
941
|
-
async supplyCollateral(tokenSymbol, amount, marketParams) {
|
|
942
|
-
const acctAddr = await this.getAccountAddress();
|
|
943
|
-
const colInfo = BASE_COLLATERALS[tokenSymbol];
|
|
944
|
-
if (!colInfo)
|
|
945
|
-
throw new AgetherError(`Unknown collateral: ${tokenSymbol}`, 'UNKNOWN_COLLATERAL');
|
|
946
|
-
const params = marketParams ?? await this.findMarketForCollateral(tokenSymbol);
|
|
947
|
-
const weiAmount = ethers.parseUnits(amount, colInfo.decimals);
|
|
948
|
-
const morphoAddr = this.config.contracts.morphoBlue;
|
|
949
|
-
// Step 1: Transfer collateral from EOA → AgentAccount (only the shortfall)
|
|
950
|
-
const colToken = new Contract(colInfo.address, ERC20_ABI, this._signer);
|
|
951
|
-
const acctBalance = await colToken.balanceOf(acctAddr);
|
|
952
|
-
if (acctBalance < weiAmount) {
|
|
953
|
-
const shortfall = weiAmount - acctBalance;
|
|
954
|
-
const eoaBalance = await colToken.balanceOf(await this.getSignerAddress());
|
|
955
|
-
if (eoaBalance < shortfall) {
|
|
956
|
-
throw new AgetherError(`Insufficient ${tokenSymbol}. Need ${amount}, AgentAccount has ${ethers.formatUnits(acctBalance, colInfo.decimals)}, EOA has ${ethers.formatUnits(eoaBalance, colInfo.decimals)}.`, 'INSUFFICIENT_BALANCE');
|
|
957
|
-
}
|
|
958
|
-
const transferTx = await colToken.transfer(acctAddr, shortfall);
|
|
959
|
-
await transferTx.wait();
|
|
960
|
-
this._refreshSigner();
|
|
961
|
-
}
|
|
962
|
-
// Step 2: AgentAccount batch: approve + supplyCollateral
|
|
963
|
-
const targets = [colInfo.address, morphoAddr];
|
|
964
|
-
const values = [0n, 0n];
|
|
965
|
-
const datas = [
|
|
966
|
-
erc20Iface.encodeFunctionData('approve', [morphoAddr, weiAmount]),
|
|
967
|
-
morphoIface.encodeFunctionData('supplyCollateral', [
|
|
968
|
-
this._toTuple(params), weiAmount, acctAddr, '0x',
|
|
969
|
-
]),
|
|
970
|
-
];
|
|
971
|
-
const receipt = await this.batch(targets, values, datas);
|
|
972
|
-
return {
|
|
973
|
-
tx: receipt.hash,
|
|
974
|
-
collateralToken: tokenSymbol,
|
|
975
|
-
amount,
|
|
976
|
-
agentAccount: acctAddr,
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
/**
|
|
980
|
-
* Borrow USDC against existing collateral.
|
|
981
|
-
*
|
|
982
|
-
* AgentAccount.execute: Morpho.borrow(params, amount, 0, account, account)
|
|
983
|
-
*
|
|
984
|
-
* @param usdcAmount - USDC amount (e.g. '100')
|
|
985
|
-
* @param tokenSymbol - collateral symbol to identify which market (default: first with collateral)
|
|
986
|
-
*/
|
|
987
|
-
async borrow(usdcAmount, tokenSymbol, marketParams) {
|
|
988
|
-
const acctAddr = await this.getAccountAddress();
|
|
989
|
-
const amount = ethers.parseUnits(usdcAmount, 6);
|
|
990
|
-
const morphoAddr = this.config.contracts.morphoBlue;
|
|
991
|
-
// Find market
|
|
992
|
-
let params;
|
|
993
|
-
let usedToken = tokenSymbol || 'WETH';
|
|
994
|
-
if (marketParams) {
|
|
995
|
-
params = marketParams;
|
|
996
|
-
}
|
|
997
|
-
else if (tokenSymbol) {
|
|
998
|
-
params = await this.findMarketForCollateral(tokenSymbol);
|
|
999
|
-
}
|
|
1000
|
-
else {
|
|
1001
|
-
// Auto-detect: find first market with collateral
|
|
1002
|
-
const { params: p, symbol } = await this._findActiveMarket();
|
|
1003
|
-
params = p;
|
|
1004
|
-
usedToken = symbol;
|
|
1005
|
-
}
|
|
1006
|
-
// ── Pre-check: verify collateral is sufficient for the borrow amount ──
|
|
1007
|
-
// Avoids wasting gas on a UserOp that will revert with "insufficient collateral".
|
|
1008
|
-
try {
|
|
1009
|
-
const marketId = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(['address', 'address', 'address', 'address', 'uint256'], [params.loanToken, params.collateralToken, params.oracle, params.irm, params.lltv]));
|
|
1010
|
-
const pos = await this.morphoBlue.position(marketId, acctAddr);
|
|
1011
|
-
if (pos.collateral === 0n) {
|
|
1012
|
-
throw new AgetherError(`No collateral deposited for ${usedToken}. Deposit collateral first.`, 'NO_COLLATERAL');
|
|
1013
|
-
}
|
|
1014
|
-
// Compute max borrowable: collateral × oraclePrice / 1e36 × LLTV / 1e18 − debt
|
|
1015
|
-
const oracleContract = new Contract(params.oracle, ['function price() view returns (uint256)'], this.provider);
|
|
1016
|
-
const oraclePrice = await oracleContract.price();
|
|
1017
|
-
const collateralValueInLoan = (BigInt(pos.collateral) * oraclePrice) / (10n ** 36n);
|
|
1018
|
-
const maxBorrowTotal = (collateralValueInLoan * params.lltv) / (10n ** 18n);
|
|
1019
|
-
// Compute current debt from shares
|
|
1020
|
-
const mktState = await this.morphoBlue.market(marketId);
|
|
1021
|
-
const totalBorrowShares = BigInt(mktState.totalBorrowShares);
|
|
1022
|
-
const totalBorrowAssets = BigInt(mktState.totalBorrowAssets);
|
|
1023
|
-
const currentDebt = totalBorrowShares > 0n
|
|
1024
|
-
? (BigInt(pos.borrowShares) * totalBorrowAssets + totalBorrowShares - 1n) / totalBorrowShares
|
|
1025
|
-
: 0n;
|
|
1026
|
-
const maxAdditional = maxBorrowTotal > currentDebt ? maxBorrowTotal - currentDebt : 0n;
|
|
1027
|
-
if (amount > maxAdditional) {
|
|
1028
|
-
const maxUsd = ethers.formatUnits(maxAdditional, 6);
|
|
1029
|
-
const colFormatted = ethers.formatUnits(pos.collateral, 18);
|
|
1030
|
-
throw new AgetherError(`Borrow of $${usdcAmount} USDC exceeds max borrowable $${maxUsd} USDC ` +
|
|
1031
|
-
`(collateral: ${colFormatted} ${usedToken}, LLTV: ${Number(params.lltv) / 1e18 * 100}%). ` +
|
|
1032
|
-
`Deposit more collateral or reduce borrow amount.`, 'EXCEEDS_MAX_LTV');
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
catch (e) {
|
|
1036
|
-
if (e instanceof AgetherError)
|
|
1037
|
-
throw e;
|
|
1038
|
-
console.warn('[agether] borrow pre-check failed (proceeding anyway):', e instanceof Error ? e.message : e);
|
|
1039
|
-
}
|
|
1040
|
-
const data = morphoIface.encodeFunctionData('borrow', [
|
|
1041
|
-
this._toTuple(params), amount, 0n, acctAddr, acctAddr,
|
|
1042
|
-
]);
|
|
1043
|
-
const receipt = await this.exec(morphoAddr, data);
|
|
1044
|
-
return {
|
|
1045
|
-
tx: receipt.hash,
|
|
1046
|
-
amount: usdcAmount,
|
|
1047
|
-
collateralToken: usedToken,
|
|
1048
|
-
agentAccount: acctAddr,
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
|
-
/**
|
|
1052
|
-
* Deposit collateral AND borrow USDC in one batched transaction.
|
|
1053
|
-
*
|
|
1054
|
-
* AgentAccount.executeBatch:
|
|
1055
|
-
* [collateral.approve, Morpho.supplyCollateral, Morpho.borrow]
|
|
1056
|
-
*
|
|
1057
|
-
* The collateral must be transferred to AgentAccount first.
|
|
1058
|
-
*/
|
|
1059
|
-
async depositAndBorrow(tokenSymbol, collateralAmount, borrowUsdcAmount, marketParams) {
|
|
1060
|
-
const acctAddr = await this.getAccountAddress();
|
|
1061
|
-
const colInfo = BASE_COLLATERALS[tokenSymbol];
|
|
1062
|
-
if (!colInfo)
|
|
1063
|
-
throw new AgetherError(`Unknown collateral: ${tokenSymbol}`, 'UNKNOWN_COLLATERAL');
|
|
1064
|
-
const params = marketParams ?? await this.findMarketForCollateral(tokenSymbol);
|
|
1065
|
-
const colWei = ethers.parseUnits(collateralAmount, colInfo.decimals);
|
|
1066
|
-
const borrowWei = ethers.parseUnits(borrowUsdcAmount, 6);
|
|
1067
|
-
const morphoAddr = this.config.contracts.morphoBlue;
|
|
1068
|
-
// ── Pre-check: will the new total collateral support the borrow? ──
|
|
1069
|
-
try {
|
|
1070
|
-
const marketId = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(['address', 'address', 'address', 'address', 'uint256'], [params.loanToken, params.collateralToken, params.oracle, params.irm, params.lltv]));
|
|
1071
|
-
const pos = await this.morphoBlue.position(marketId, acctAddr);
|
|
1072
|
-
const totalCollateral = BigInt(pos.collateral) + colWei; // existing + new deposit
|
|
1073
|
-
const oracleContract = new Contract(params.oracle, ['function price() view returns (uint256)'], this.provider);
|
|
1074
|
-
const oraclePrice = await oracleContract.price();
|
|
1075
|
-
const collateralValueInLoan = (totalCollateral * oraclePrice) / (10n ** 36n);
|
|
1076
|
-
const maxBorrowTotal = (collateralValueInLoan * params.lltv) / (10n ** 18n);
|
|
1077
|
-
const mktState = await this.morphoBlue.market(marketId);
|
|
1078
|
-
const totalBorrowShares = BigInt(mktState.totalBorrowShares);
|
|
1079
|
-
const totalBorrowAssets = BigInt(mktState.totalBorrowAssets);
|
|
1080
|
-
const currentDebt = totalBorrowShares > 0n
|
|
1081
|
-
? (BigInt(pos.borrowShares) * totalBorrowAssets + totalBorrowShares - 1n) / totalBorrowShares
|
|
1082
|
-
: 0n;
|
|
1083
|
-
const maxAdditional = maxBorrowTotal > currentDebt ? maxBorrowTotal - currentDebt : 0n;
|
|
1084
|
-
if (borrowWei > maxAdditional) {
|
|
1085
|
-
const maxUsd = ethers.formatUnits(maxAdditional, 6);
|
|
1086
|
-
throw new AgetherError(`Borrow of $${borrowUsdcAmount} USDC exceeds max borrowable $${maxUsd} USDC ` +
|
|
1087
|
-
`(total collateral: ${ethers.formatUnits(totalCollateral, colInfo.decimals)} ${tokenSymbol}, ` +
|
|
1088
|
-
`LLTV: ${Number(params.lltv) / 1e18 * 100}%). Reduce borrow or increase collateral.`, 'EXCEEDS_MAX_LTV');
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
catch (e) {
|
|
1092
|
-
if (e instanceof AgetherError)
|
|
1093
|
-
throw e;
|
|
1094
|
-
console.warn('[agether] depositAndBorrow pre-check failed (proceeding anyway):', e instanceof Error ? e.message : e);
|
|
1095
|
-
}
|
|
1096
|
-
// Step 1: Transfer collateral from EOA → AgentAccount (only the shortfall)
|
|
1097
|
-
const colToken = new Contract(colInfo.address, ERC20_ABI, this._signer);
|
|
1098
|
-
const acctBalance = await colToken.balanceOf(acctAddr);
|
|
1099
|
-
if (acctBalance < colWei) {
|
|
1100
|
-
const shortfall = colWei - acctBalance;
|
|
1101
|
-
const eoaBalance = await colToken.balanceOf(await this.getSignerAddress());
|
|
1102
|
-
if (eoaBalance < shortfall) {
|
|
1103
|
-
throw new AgetherError(`Insufficient ${tokenSymbol}. Need ${collateralAmount}, AgentAccount has ${ethers.formatUnits(acctBalance, colInfo.decimals)}, EOA has ${ethers.formatUnits(eoaBalance, colInfo.decimals)}.`, 'INSUFFICIENT_BALANCE');
|
|
1104
|
-
}
|
|
1105
|
-
const transferTx = await colToken.transfer(acctAddr, shortfall);
|
|
1106
|
-
await transferTx.wait();
|
|
1107
|
-
this._refreshSigner();
|
|
1108
|
-
}
|
|
1109
|
-
// Step 2: Batched — approve + supplyCollateral + borrow
|
|
1110
|
-
const targets = [colInfo.address, morphoAddr, morphoAddr];
|
|
1111
|
-
const values = [0n, 0n, 0n];
|
|
1112
|
-
const datas = [
|
|
1113
|
-
erc20Iface.encodeFunctionData('approve', [morphoAddr, colWei]),
|
|
1114
|
-
morphoIface.encodeFunctionData('supplyCollateral', [
|
|
1115
|
-
this._toTuple(params), colWei, acctAddr, '0x',
|
|
1116
|
-
]),
|
|
1117
|
-
morphoIface.encodeFunctionData('borrow', [
|
|
1118
|
-
this._toTuple(params), borrowWei, 0n, acctAddr, acctAddr,
|
|
1119
|
-
]),
|
|
1120
|
-
];
|
|
1121
|
-
const receipt = await this.batch(targets, values, datas);
|
|
1122
|
-
return {
|
|
1123
|
-
tx: receipt.hash,
|
|
1124
|
-
collateralToken: tokenSymbol,
|
|
1125
|
-
collateralAmount,
|
|
1126
|
-
borrowAmount: borrowUsdcAmount,
|
|
1127
|
-
agentAccount: acctAddr,
|
|
1128
|
-
};
|
|
1129
|
-
}
|
|
1130
|
-
/**
|
|
1131
|
-
* Repay borrowed USDC from AgentAccount.
|
|
1132
|
-
*
|
|
1133
|
-
* AgentAccount.executeBatch:
|
|
1134
|
-
* [USDC.approve(MorphoBlue), Morpho.repay(params)]
|
|
1135
|
-
*/
|
|
1136
|
-
async repay(usdcAmount, tokenSymbol, marketParams) {
|
|
1137
|
-
const acctAddr = await this.getAccountAddress();
|
|
1138
|
-
const morphoAddr = this.config.contracts.morphoBlue;
|
|
1139
|
-
const usdcAddr = this.config.contracts.usdc;
|
|
1140
|
-
let params;
|
|
1141
|
-
if (marketParams) {
|
|
1142
|
-
params = marketParams;
|
|
1143
|
-
}
|
|
1144
|
-
else if (tokenSymbol) {
|
|
1145
|
-
params = await this.findMarketForCollateral(tokenSymbol);
|
|
1146
|
-
}
|
|
1147
|
-
else {
|
|
1148
|
-
const { params: p } = await this._findActiveMarket();
|
|
1149
|
-
params = p;
|
|
1150
|
-
}
|
|
1151
|
-
// Determine whether to repay by shares (full repay) or by assets (partial).
|
|
1152
|
-
// Using shares-based repay prevents dust borrow shares from remaining.
|
|
1153
|
-
let repayAssets;
|
|
1154
|
-
let repayShares;
|
|
1155
|
-
let approveAmount;
|
|
1156
|
-
if (usdcAmount === 'all') {
|
|
1157
|
-
// Full repay: use shares to ensure no dust remains
|
|
1158
|
-
const markets = await this.getMarkets();
|
|
1159
|
-
const mkt = markets.find((m) => m.collateralAsset?.address.toLowerCase() === params.collateralToken.toLowerCase());
|
|
1160
|
-
if (mkt) {
|
|
1161
|
-
const pos = await this.morphoBlue.position(mkt.uniqueKey, acctAddr);
|
|
1162
|
-
repayShares = BigInt(pos.borrowShares);
|
|
1163
|
-
repayAssets = 0n;
|
|
1164
|
-
// Read onchain market state for accurate share→asset conversion
|
|
1165
|
-
const onChainMkt = await this.morphoBlue.market(mkt.uniqueKey);
|
|
1166
|
-
const totalBorrowAssets = BigInt(onChainMkt.totalBorrowAssets);
|
|
1167
|
-
const totalBorrowShares = BigInt(onChainMkt.totalBorrowShares);
|
|
1168
|
-
const estimated = totalBorrowShares > 0n
|
|
1169
|
-
? (repayShares * totalBorrowAssets) / totalBorrowShares + 10n
|
|
1170
|
-
: 0n;
|
|
1171
|
-
approveAmount = estimated > 0n ? estimated : ethers.parseUnits('1', 6);
|
|
1172
|
-
}
|
|
1173
|
-
else {
|
|
1174
|
-
// Fallback: large asset repay
|
|
1175
|
-
repayAssets = ethers.parseUnits('999999', 6);
|
|
1176
|
-
repayShares = 0n;
|
|
1177
|
-
approveAmount = repayAssets;
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
else {
|
|
1181
|
-
repayAssets = ethers.parseUnits(usdcAmount, 6);
|
|
1182
|
-
repayShares = 0n;
|
|
1183
|
-
approveAmount = repayAssets;
|
|
1184
|
-
// Specific amount: always use asset-based repay (no shares conversion).
|
|
1185
|
-
// This avoids rounding issues where shares→assets needs slightly more
|
|
1186
|
-
// USDC than the AgentAccount holds. Use 'all' for full debt clearance.
|
|
1187
|
-
}
|
|
1188
|
-
// Ensure AgentAccount has enough USDC to cover the repay.
|
|
1189
|
-
// If not, transfer the shortfall from EOA → AgentAccount first.
|
|
1190
|
-
const usdcContract = new Contract(usdcAddr, ERC20_ABI, this._signer);
|
|
1191
|
-
const acctBalance = await usdcContract.balanceOf(acctAddr);
|
|
1192
|
-
if (acctBalance < approveAmount) {
|
|
1193
|
-
const shortfall = approveAmount - acctBalance;
|
|
1194
|
-
const eoaBalance = await usdcContract.balanceOf(await this.getSignerAddress());
|
|
1195
|
-
if (eoaBalance < shortfall) {
|
|
1196
|
-
throw new AgetherError(`Insufficient USDC for repay. Need ${ethers.formatUnits(approveAmount, 6)} USDC, ` +
|
|
1197
|
-
`AgentAccount has ${ethers.formatUnits(acctBalance, 6)}, EOA has ${ethers.formatUnits(eoaBalance, 6)}.`, 'INSUFFICIENT_BALANCE');
|
|
1198
|
-
}
|
|
1199
|
-
const transferTx = await usdcContract.transfer(acctAddr, shortfall);
|
|
1200
|
-
await transferTx.wait();
|
|
1201
|
-
this._refreshSigner();
|
|
1202
|
-
}
|
|
1203
|
-
// Batch: approve + repay
|
|
1204
|
-
const targets = [usdcAddr, morphoAddr];
|
|
1205
|
-
const values = [0n, 0n];
|
|
1206
|
-
const datas = [
|
|
1207
|
-
erc20Iface.encodeFunctionData('approve', [morphoAddr, approveAmount]),
|
|
1208
|
-
morphoIface.encodeFunctionData('repay', [
|
|
1209
|
-
this._toTuple(params), repayAssets, repayShares, acctAddr, '0x',
|
|
1210
|
-
]),
|
|
1211
|
-
];
|
|
1212
|
-
const receipt = await this.batch(targets, values, datas);
|
|
1213
|
-
// Read remaining debt
|
|
1214
|
-
let remainingDebt = '0';
|
|
1215
|
-
try {
|
|
1216
|
-
const status = await this.getStatus();
|
|
1217
|
-
remainingDebt = status.totalDebt;
|
|
1218
|
-
}
|
|
1219
|
-
catch (e) {
|
|
1220
|
-
console.warn('[agether] failed to read remaining debt after repay:', e instanceof Error ? e.message : e);
|
|
1221
|
-
}
|
|
1222
|
-
return { tx: receipt.hash, amount: usdcAmount, remainingDebt };
|
|
1223
|
-
}
|
|
1224
|
-
/**
|
|
1225
|
-
* Withdraw collateral from Morpho Blue.
|
|
1226
|
-
*
|
|
1227
|
-
* AgentAccount.execute: Morpho.withdrawCollateral(params, amount, account, receiver)
|
|
1228
|
-
*
|
|
1229
|
-
* @param receiver - defaults to EOA wallet
|
|
1230
|
-
*/
|
|
1231
|
-
async withdrawCollateral(tokenSymbol, amount, marketParams, receiver) {
|
|
1232
|
-
const acctAddr = await this.getAccountAddress();
|
|
1233
|
-
const colInfo = BASE_COLLATERALS[tokenSymbol];
|
|
1234
|
-
if (!colInfo)
|
|
1235
|
-
throw new AgetherError(`Unknown collateral: ${tokenSymbol}`, 'UNKNOWN_COLLATERAL');
|
|
1236
|
-
const params = marketParams ?? await this.findMarketForCollateral(tokenSymbol);
|
|
1237
|
-
const morphoAddr = this.config.contracts.morphoBlue;
|
|
1238
|
-
const usdcAddr = this.config.contracts.usdc;
|
|
1239
|
-
const dest = receiver || await this.getSignerAddress();
|
|
1240
|
-
// Handle 'all' — withdraw full collateral
|
|
1241
|
-
let weiAmount;
|
|
1242
|
-
const markets = await this.getMarkets();
|
|
1243
|
-
const market = markets.find((m) => m.collateralAsset?.address.toLowerCase() === colInfo.address.toLowerCase());
|
|
1244
|
-
if (amount === 'all') {
|
|
1245
|
-
if (!market)
|
|
1246
|
-
throw new AgetherError('Market not found', 'MARKET_NOT_FOUND');
|
|
1247
|
-
const pos = await this.morphoBlue.position(market.uniqueKey, acctAddr);
|
|
1248
|
-
weiAmount = pos.collateral;
|
|
1249
|
-
if (weiAmount === 0n)
|
|
1250
|
-
throw new AgetherError('No collateral to withdraw', 'NO_COLLATERAL');
|
|
1251
|
-
}
|
|
1252
|
-
else {
|
|
1253
|
-
weiAmount = ethers.parseUnits(amount, colInfo.decimals);
|
|
1254
|
-
}
|
|
1255
|
-
// ── Check for dust borrow shares ──────────────────────
|
|
1256
|
-
// If any borrow shares remain (even dust), Morpho will reject a full
|
|
1257
|
-
// collateral withdrawal because LTV would become infinite.
|
|
1258
|
-
// Auto-repay dust debt before withdrawing.
|
|
1259
|
-
let hasDustDebt = false;
|
|
1260
|
-
let dustBorrowShares = 0n;
|
|
1261
|
-
let dustApproveAmount = 0n;
|
|
1262
|
-
if (market) {
|
|
1263
|
-
try {
|
|
1264
|
-
const pos = await this.morphoBlue.position(market.uniqueKey, acctAddr);
|
|
1265
|
-
dustBorrowShares = BigInt(pos.borrowShares);
|
|
1266
|
-
if (dustBorrowShares > 0n) {
|
|
1267
|
-
hasDustDebt = true;
|
|
1268
|
-
// Convert shares → assets to know how much USDC to approve
|
|
1269
|
-
const onChainMkt = await this.morphoBlue.market(market.uniqueKey);
|
|
1270
|
-
const totalBorrowAssets = BigInt(onChainMkt.totalBorrowAssets);
|
|
1271
|
-
const totalBorrowShares = BigInt(onChainMkt.totalBorrowShares);
|
|
1272
|
-
const estimated = totalBorrowShares > 0n
|
|
1273
|
-
? (dustBorrowShares * totalBorrowAssets) / totalBorrowShares + 10n
|
|
1274
|
-
: 0n;
|
|
1275
|
-
dustApproveAmount = estimated > 0n ? estimated : ethers.parseUnits('1', 6);
|
|
1276
|
-
console.log(`[agether] dust borrow shares detected: ${dustBorrowShares} shares ≈ ${ethers.formatUnits(dustApproveAmount, 6)} USDC — auto-repaying before withdraw`);
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
catch (e) {
|
|
1280
|
-
console.warn('[agether] failed to check borrow shares before withdraw:', e instanceof Error ? e.message : e);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
const withdrawData = morphoIface.encodeFunctionData('withdrawCollateral', [
|
|
1284
|
-
this._toTuple(params), weiAmount, acctAddr, dest,
|
|
1285
|
-
]);
|
|
1286
|
-
let receipt;
|
|
1287
|
-
if (hasDustDebt) {
|
|
1288
|
-
// Ensure AgentAccount has enough USDC to cover the dust repay
|
|
1289
|
-
const usdcContract = new Contract(usdcAddr, ERC20_ABI, this._signer);
|
|
1290
|
-
const acctBalance = await usdcContract.balanceOf(acctAddr);
|
|
1291
|
-
if (acctBalance < dustApproveAmount) {
|
|
1292
|
-
const shortfall = dustApproveAmount - acctBalance;
|
|
1293
|
-
const eoaBalance = await usdcContract.balanceOf(await this.getSignerAddress());
|
|
1294
|
-
if (eoaBalance >= shortfall) {
|
|
1295
|
-
console.log(`[agether] transferring ${ethers.formatUnits(shortfall, 6)} USDC from EOA → AgentAccount for dust repay`);
|
|
1296
|
-
const transferTx = await usdcContract.transfer(acctAddr, shortfall);
|
|
1297
|
-
await transferTx.wait();
|
|
1298
|
-
this._refreshSigner();
|
|
1299
|
-
}
|
|
1300
|
-
// If EOA doesn't have enough either, still try — the tx will revert with a clear error
|
|
1301
|
-
}
|
|
1302
|
-
// Batch: approve USDC → repay dust shares → withdraw collateral
|
|
1303
|
-
const targets = [usdcAddr, morphoAddr, morphoAddr];
|
|
1304
|
-
const values = [0n, 0n, 0n];
|
|
1305
|
-
const datas = [
|
|
1306
|
-
erc20Iface.encodeFunctionData('approve', [morphoAddr, dustApproveAmount]),
|
|
1307
|
-
morphoIface.encodeFunctionData('repay', [
|
|
1308
|
-
this._toTuple(params), 0n, dustBorrowShares, acctAddr, '0x',
|
|
1309
|
-
]),
|
|
1310
|
-
withdrawData,
|
|
1311
|
-
];
|
|
1312
|
-
receipt = await this.batch(targets, values, datas);
|
|
1313
|
-
}
|
|
1314
|
-
else {
|
|
1315
|
-
receipt = await this.exec(morphoAddr, withdrawData);
|
|
1316
|
-
}
|
|
1317
|
-
// Read remaining collateral
|
|
1318
|
-
let remainingCollateral = '0';
|
|
1319
|
-
try {
|
|
1320
|
-
if (market) {
|
|
1321
|
-
const pos = await this.morphoBlue.position(market.uniqueKey, acctAddr);
|
|
1322
|
-
remainingCollateral = ethers.formatUnits(pos.collateral, colInfo.decimals);
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
catch (e) {
|
|
1326
|
-
console.warn('[agether] failed to read remaining collateral after withdraw:', e instanceof Error ? e.message : e);
|
|
1327
|
-
}
|
|
1328
|
-
return {
|
|
1329
|
-
tx: receipt.hash,
|
|
1330
|
-
token: tokenSymbol,
|
|
1331
|
-
amount: amount === 'all' ? ethers.formatUnits(weiAmount, colInfo.decimals) : amount,
|
|
1332
|
-
remainingCollateral,
|
|
1333
|
-
destination: dest,
|
|
1334
|
-
};
|
|
1335
|
-
}
|
|
1336
|
-
/**
|
|
1337
|
-
* Sponsor: transfer collateral to another agent's AgentAccount.
|
|
1338
|
-
* (The agent must then supplyCollateral themselves via their own account.)
|
|
1339
|
-
*/
|
|
1340
|
-
async sponsor(target, tokenSymbol, amount) {
|
|
1341
|
-
const colInfo = BASE_COLLATERALS[tokenSymbol];
|
|
1342
|
-
if (!colInfo)
|
|
1343
|
-
throw new AgetherError(`Unknown collateral: ${tokenSymbol}`, 'UNKNOWN_COLLATERAL');
|
|
1344
|
-
let targetAddr;
|
|
1345
|
-
if (target.address) {
|
|
1346
|
-
targetAddr = target.address;
|
|
1347
|
-
}
|
|
1348
|
-
else if (target.agentId) {
|
|
1349
|
-
targetAddr = await this.agether4337Factory.getAccount(BigInt(target.agentId));
|
|
1350
|
-
if (targetAddr === ethers.ZeroAddress)
|
|
1351
|
-
throw new AgetherError('Target agent has no account', 'NO_ACCOUNT');
|
|
1352
|
-
}
|
|
1353
|
-
else {
|
|
1354
|
-
throw new AgetherError('Provide agentId or address', 'INVALID_TARGET');
|
|
1355
|
-
}
|
|
1356
|
-
const weiAmount = ethers.parseUnits(amount, colInfo.decimals);
|
|
1357
|
-
const colToken = new Contract(colInfo.address, ERC20_ABI, this._signer);
|
|
1358
|
-
const tx = await colToken.transfer(targetAddr, weiAmount);
|
|
1359
|
-
const receipt = await tx.wait();
|
|
1360
|
-
this._refreshSigner();
|
|
1361
|
-
return { tx: receipt.hash, targetAccount: targetAddr, targetAgentId: target.agentId };
|
|
1362
|
-
}
|
|
1363
|
-
// ════════════════════════════════════════════════════════
|
|
1364
|
-
// Reputation (Agether8004Scorer contract)
|
|
1365
|
-
// ════════════════════════════════════════════════════════
|
|
1366
|
-
async getCreditScore() {
|
|
1367
|
-
if (!this.agentId)
|
|
1368
|
-
throw new AgetherError('agentId not set', 'NO_AGENT_ID');
|
|
1369
|
-
return this.agether8004Scorer.getCreditScore(BigInt(this.agentId));
|
|
1370
|
-
}
|
|
1371
|
-
async getAttestation() {
|
|
1372
|
-
if (!this.agentId)
|
|
1373
|
-
throw new AgetherError('agentId not set', 'NO_AGENT_ID');
|
|
1374
|
-
const att = await this.agether8004Scorer.getAttestation(BigInt(this.agentId));
|
|
1375
|
-
return { score: att.score, timestamp: att.timestamp, signer: att.signer };
|
|
1376
|
-
}
|
|
1377
|
-
async isEligible(minScore = 500n) {
|
|
1378
|
-
if (!this.agentId)
|
|
1379
|
-
throw new AgetherError('agentId not set', 'NO_AGENT_ID');
|
|
1380
|
-
const [eligible, currentScore] = await this.agether8004Scorer.isEligible(BigInt(this.agentId), minScore);
|
|
1381
|
-
return { eligible, currentScore };
|
|
1382
|
-
}
|
|
1383
|
-
async isScoreFresh() {
|
|
1384
|
-
if (!this.agentId)
|
|
1385
|
-
throw new AgetherError('agentId not set', 'NO_AGENT_ID');
|
|
1386
|
-
const [fresh, age] = await this.agether8004Scorer.isScoreFresh(BigInt(this.agentId));
|
|
1387
|
-
return { fresh, age };
|
|
1388
|
-
}
|
|
1389
|
-
// ════════════════════════════════════════════════════════
|
|
1390
|
-
// Internal Helpers
|
|
1391
|
-
// ════════════════════════════════════════════════════════
|
|
1392
|
-
/**
|
|
1393
|
-
* Refresh the signer and re-bind contract instances.
|
|
1394
|
-
*
|
|
1395
|
-
* For the **privateKey** path: recreates provider + wallet so the next tx
|
|
1396
|
-
* fetches a fresh nonce from chain. Anvil (and some RPC providers) return a
|
|
1397
|
-
* stale `eth_getTransactionCount` right after a block is mined, causing
|
|
1398
|
-
* "nonce too low" on the follow-up tx.
|
|
1399
|
-
*
|
|
1400
|
-
* For the **external signer** path: the signer is immutable and owned by the
|
|
1401
|
-
* caller (e.g. custody provider). We only re-bind contract instances to
|
|
1402
|
-
* ensure they reference the current signer. Nonce management is the caller's
|
|
1403
|
-
* responsibility.
|
|
1404
|
-
*/
|
|
1405
|
-
_refreshSigner() {
|
|
1406
|
-
if (this._useExternalSigner) {
|
|
1407
|
-
// External signer: re-bind contracts only (signer is immutable)
|
|
1408
|
-
const addrs = this.config.contracts;
|
|
1409
|
-
this.agether4337Factory = new Contract(addrs.agether4337Factory, ACCOUNT_FACTORY_ABI, this._signer);
|
|
1410
|
-
this.entryPoint = new Contract(addrs.entryPoint, ENTRYPOINT_V07_ABI, this._signer);
|
|
1411
|
-
this.validationModule = new Contract(addrs.erc8004ValidationModule, ERC8004_VALIDATION_MODULE_ABI, this.provider);
|
|
1412
|
-
this.agether8004Scorer = new Contract(addrs.agether8004Scorer, AGENT_REPUTATION_ABI, this._signer);
|
|
1413
|
-
this.identityRegistry = new Contract(addrs.identityRegistry, IDENTITY_REGISTRY_ABI, this._signer);
|
|
1414
|
-
}
|
|
1415
|
-
else {
|
|
1416
|
-
// Private key path: recreate provider + wallet for fresh nonce
|
|
1417
|
-
this.provider = new ethers.JsonRpcProvider(this._rpcUrl);
|
|
1418
|
-
const wallet = new ethers.Wallet(this._privateKey, this.provider);
|
|
1419
|
-
this._signer = wallet;
|
|
1420
|
-
this._eoaAddress = wallet.address;
|
|
1421
|
-
// Re-bind contract instances that use the wallet as signer
|
|
1422
|
-
const addrs = this.config.contracts;
|
|
1423
|
-
this.agether4337Factory = new Contract(addrs.agether4337Factory, ACCOUNT_FACTORY_ABI, this._signer);
|
|
1424
|
-
this.entryPoint = new Contract(addrs.entryPoint, ENTRYPOINT_V07_ABI, this._signer);
|
|
1425
|
-
this.validationModule = new Contract(addrs.erc8004ValidationModule, ERC8004_VALIDATION_MODULE_ABI, this.provider);
|
|
1426
|
-
this.agether8004Scorer = new Contract(addrs.agether8004Scorer, AGENT_REPUTATION_ABI, this._signer);
|
|
1427
|
-
this.identityRegistry = new Contract(addrs.identityRegistry, IDENTITY_REGISTRY_ABI, this._signer);
|
|
1428
|
-
// morphoBlue stays on provider (read-only) — no need to refresh
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
// ────────────────────────────────────────────────────────────
|
|
1432
|
-
// ERC-4337 UserOp helpers (Safe + Safe7579 + EntryPoint v0.7)
|
|
1433
|
-
// ────────────────────────────────────────────────────────────
|
|
1434
|
-
/**
|
|
1435
|
-
* Pack two uint128 values into a single bytes32:
|
|
1436
|
-
* bytes32 = (hi << 128) | lo
|
|
1437
|
-
*/
|
|
1438
|
-
_packUint128(hi, lo) {
|
|
1439
|
-
return ethers.zeroPadValue(ethers.toBeHex((hi << 128n) | lo), 32);
|
|
1440
|
-
}
|
|
1441
|
-
/**
|
|
1442
|
-
* Build, sign and submit a PackedUserOperation through EntryPoint.handleOps.
|
|
1443
|
-
*
|
|
1444
|
-
* @param callData – the ABI-encoded calldata for the Safe7579 account
|
|
1445
|
-
* (e.g. `execute(mode, executionCalldata)`)
|
|
1446
|
-
* @returns the transaction receipt of the handleOps call
|
|
1447
|
-
*/
|
|
1448
|
-
async _submitUserOp(callData) {
|
|
1449
|
-
const sender = await this.getAccountAddress();
|
|
1450
|
-
// ── Nonce key = validator module address ──
|
|
1451
|
-
// Safe7579 extracts the validator from the nonce: `validator := shr(96, nonce)`.
|
|
1452
|
-
// The EntryPoint's getNonce(sender, key) returns `(key << 64) | seq`.
|
|
1453
|
-
// Safe7579's own getNonce computes the key as:
|
|
1454
|
-
// uint192(bytes24(bytes20(address(validator)))) = address << 32
|
|
1455
|
-
// so that shr(96, (addr<<32)<<64 | seq) = shr(96, addr<<96 | seq) = addr.
|
|
1456
|
-
const validatorAddr = this.config.contracts.erc8004ValidationModule;
|
|
1457
|
-
const nonceKey = BigInt(validatorAddr) << 32n;
|
|
1458
|
-
const nonce = await this.entryPoint.getNonce(sender, nonceKey);
|
|
1459
|
-
// Gas prices from the network
|
|
1460
|
-
const feeData = await this.provider.getFeeData();
|
|
1461
|
-
const maxFeePerGas = feeData.maxFeePerGas ?? ethers.parseUnits('0.5', 'gwei');
|
|
1462
|
-
const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ?? ethers.parseUnits('0.1', 'gwei');
|
|
1463
|
-
// Default gas limits — generous for DeFi interactions
|
|
1464
|
-
const verificationGasLimit = 500000n;
|
|
1465
|
-
const callGasLimit = 800000n;
|
|
1466
|
-
const preVerificationGas = 100000n;
|
|
1467
|
-
const accountGasLimits = this._packUint128(verificationGasLimit, callGasLimit);
|
|
1468
|
-
const gasFees = this._packUint128(maxPriorityFeePerGas, maxFeePerGas);
|
|
1469
|
-
// ── Auto-fund the Safe account for gas ──
|
|
1470
|
-
// The account must pay `missingAccountFunds` to EntryPoint during validateUserOp.
|
|
1471
|
-
// Required prefund ≈ (verificationGasLimit + callGasLimit + preVerificationGas) × maxFeePerGas.
|
|
1472
|
-
// If the account has insufficient ETH, top it up from the EOA.
|
|
1473
|
-
const requiredPrefund = (verificationGasLimit + callGasLimit + preVerificationGas) * maxFeePerGas;
|
|
1474
|
-
const accountBalance = await this.provider.getBalance(sender);
|
|
1475
|
-
if (accountBalance < requiredPrefund) {
|
|
1476
|
-
const topUp = requiredPrefund - accountBalance;
|
|
1477
|
-
// Add 20 % buffer to avoid marginal shortfalls
|
|
1478
|
-
const topUpWithBuffer = (topUp * 120n) / 100n;
|
|
1479
|
-
const fundTx = await this._signer.sendTransaction({
|
|
1480
|
-
to: sender,
|
|
1481
|
-
value: topUpWithBuffer,
|
|
1482
|
-
});
|
|
1483
|
-
await fundTx.wait();
|
|
1484
|
-
this._refreshSigner();
|
|
1485
|
-
}
|
|
1486
|
-
// Build the PackedUserOperation (no paymaster, no initCode for existing accounts)
|
|
1487
|
-
const userOp = {
|
|
1488
|
-
sender,
|
|
1489
|
-
nonce,
|
|
1490
|
-
initCode: '0x',
|
|
1491
|
-
callData,
|
|
1492
|
-
accountGasLimits,
|
|
1493
|
-
preVerificationGas,
|
|
1494
|
-
gasFees,
|
|
1495
|
-
paymasterAndData: '0x',
|
|
1496
|
-
signature: '0x', // placeholder — replaced after signing
|
|
1497
|
-
};
|
|
1498
|
-
// Get the hash and sign it
|
|
1499
|
-
const userOpHash = await this.entryPoint.getUserOpHash(userOp);
|
|
1500
|
-
const signature = await this._signer.signMessage(ethers.getBytes(userOpHash));
|
|
1501
|
-
userOp.signature = signature;
|
|
1502
|
-
// Submit via handleOps — the EOA pays for gas as the bundler
|
|
1503
|
-
const tx = await this.entryPoint.handleOps([userOp], await this.getSignerAddress());
|
|
1504
|
-
const receipt = await tx.wait();
|
|
1505
|
-
this._refreshSigner();
|
|
1506
|
-
// ── Verify inner UserOp execution succeeded ──
|
|
1507
|
-
// EntryPoint's outer tx always succeeds (status=1) even if the inner UserOp
|
|
1508
|
-
// execution reverts. We MUST check the UserOperationEvent.success field.
|
|
1509
|
-
const epIface = new ethers.Interface(ENTRYPOINT_V07_ABI);
|
|
1510
|
-
for (const log of receipt.logs) {
|
|
1511
|
-
try {
|
|
1512
|
-
const parsed = epIface.parseLog({ topics: log.topics, data: log.data });
|
|
1513
|
-
if (parsed?.name === 'UserOperationEvent' && !parsed.args.success) {
|
|
1514
|
-
// Inner execution failed — try to extract the revert reason
|
|
1515
|
-
let revertMsg = 'UserOp inner execution reverted';
|
|
1516
|
-
for (const rLog of receipt.logs) {
|
|
1517
|
-
try {
|
|
1518
|
-
const rParsed = epIface.parseLog({ topics: rLog.topics, data: rLog.data });
|
|
1519
|
-
if (rParsed?.name === 'UserOperationRevertReason') {
|
|
1520
|
-
const reason = rParsed.args.revertReason;
|
|
1521
|
-
try {
|
|
1522
|
-
// Try to decode as Error(string) — selector 0x08c379a0
|
|
1523
|
-
if (reason.length >= 10 && reason.slice(0, 10) === '0x08c379a0') {
|
|
1524
|
-
const decoded = ethers.AbiCoder.defaultAbiCoder().decode(['string'], '0x' + reason.slice(10));
|
|
1525
|
-
revertMsg = `UserOp reverted: ${decoded[0]}`;
|
|
1526
|
-
}
|
|
1527
|
-
else {
|
|
1528
|
-
revertMsg = `UserOp reverted with data: ${reason}`;
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
catch {
|
|
1532
|
-
revertMsg = `UserOp reverted with data: ${reason}`;
|
|
1533
|
-
}
|
|
1534
|
-
break;
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
catch {
|
|
1538
|
-
continue;
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
throw new AgetherError(revertMsg, 'USEROP_EXECUTION_FAILED');
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
catch (e) {
|
|
1545
|
-
if (e instanceof AgetherError)
|
|
1546
|
-
throw e;
|
|
1547
|
-
continue; // skip logs that don't match EntryPoint events
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
return receipt;
|
|
1551
|
-
}
|
|
1552
|
-
/**
|
|
1553
|
-
* Execute a single call via Safe7579 account (ERC-7579 single mode)
|
|
1554
|
-
* through an ERC-4337 UserOperation.
|
|
1555
|
-
*/
|
|
1556
|
-
async exec(target, data, value = 0n) {
|
|
1557
|
-
// ERC-7579 single execution uses PACKED encoding (not abi.encode!):
|
|
1558
|
-
// bytes[0:20] = target address (20 bytes, no left-padding)
|
|
1559
|
-
// bytes[20:52] = value (uint256, 32 bytes)
|
|
1560
|
-
// bytes[52:] = callData (raw, no length prefix)
|
|
1561
|
-
// This matches Safe7579's ExecutionLib.decodeSingle().
|
|
1562
|
-
const valueHex = ethers.zeroPadValue(ethers.toBeHex(value), 32);
|
|
1563
|
-
const executionCalldata = ethers.concat([target, valueHex, data]);
|
|
1564
|
-
// Build the Safe7579 account calldata: execute(bytes32 mode, bytes executionCalldata)
|
|
1565
|
-
const safe7579Iface = new ethers.Interface(SAFE7579_ACCOUNT_ABI);
|
|
1566
|
-
const callData = safe7579Iface.encodeFunctionData('execute', [MODE_SINGLE, executionCalldata]);
|
|
1567
|
-
return this._submitUserOp(callData);
|
|
1568
|
-
}
|
|
1569
|
-
/**
|
|
1570
|
-
* Execute multiple calls via Safe7579 account (ERC-7579 batch mode)
|
|
1571
|
-
* through an ERC-4337 UserOperation.
|
|
1572
|
-
*/
|
|
1573
|
-
async batch(targets, values, datas) {
|
|
1574
|
-
// ERC-7579: encode batch execution = abi.encode(Execution[])
|
|
1575
|
-
// where Execution = (address target, uint256 value, bytes callData)
|
|
1576
|
-
const executions = targets.map((t, i) => [t, values[i], datas[i]]);
|
|
1577
|
-
const executionCalldata = ethers.AbiCoder.defaultAbiCoder().encode(['(address,uint256,bytes)[]'], [executions]);
|
|
1578
|
-
// Build the Safe7579 account calldata: execute(bytes32 mode, bytes executionCalldata)
|
|
1579
|
-
const safe7579Iface = new ethers.Interface(SAFE7579_ACCOUNT_ABI);
|
|
1580
|
-
const callData = safe7579Iface.encodeFunctionData('execute', [MODE_BATCH, executionCalldata]);
|
|
1581
|
-
return this._submitUserOp(callData);
|
|
1582
|
-
}
|
|
1583
|
-
/** Convert MorphoMarketParams to Solidity tuple. */
|
|
1584
|
-
_toTuple(p) {
|
|
1585
|
-
return [p.loanToken, p.collateralToken, p.oracle, p.irm, p.lltv];
|
|
1586
|
-
}
|
|
1587
|
-
/** Find the first market where the agent has collateral deposited. */
|
|
1588
|
-
async _findActiveMarket() {
|
|
1589
|
-
const acctAddr = await this.getAccountAddress();
|
|
1590
|
-
const markets = await this.getMarkets();
|
|
1591
|
-
for (const m of markets) {
|
|
1592
|
-
if (!m.collateralAsset || m.collateralAsset.address === ethers.ZeroAddress)
|
|
1593
|
-
continue;
|
|
1594
|
-
try {
|
|
1595
|
-
const pos = await this.morphoBlue.position(m.uniqueKey, acctAddr);
|
|
1596
|
-
if (pos.collateral > 0n) {
|
|
1597
|
-
return {
|
|
1598
|
-
params: {
|
|
1599
|
-
loanToken: m.loanAsset.address,
|
|
1600
|
-
collateralToken: m.collateralAsset.address,
|
|
1601
|
-
oracle: m.oracle,
|
|
1602
|
-
irm: m.irm,
|
|
1603
|
-
lltv: m.lltv,
|
|
1604
|
-
},
|
|
1605
|
-
symbol: m.collateralAsset.symbol,
|
|
1606
|
-
};
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
catch (e) {
|
|
1610
|
-
console.warn('[agether] _findActiveMarket position check failed:', e instanceof Error ? e.message : e);
|
|
1611
|
-
continue;
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
// Default to WETH
|
|
1615
|
-
const params = await this.findMarketForCollateral('WETH');
|
|
1616
|
-
return { params, symbol: 'WETH' };
|
|
1617
|
-
}
|
|
1618
|
-
/** Find the first market where the agent has a supply (lending) position. */
|
|
1619
|
-
async _findActiveSupplyMarket() {
|
|
1620
|
-
const acctAddr = await this.getAccountAddress();
|
|
1621
|
-
const markets = await this.getMarkets();
|
|
1622
|
-
for (const m of markets) {
|
|
1623
|
-
if (!m.collateralAsset || m.collateralAsset.address === ethers.ZeroAddress)
|
|
1624
|
-
continue;
|
|
1625
|
-
try {
|
|
1626
|
-
const pos = await this.morphoBlue.position(m.uniqueKey, acctAddr);
|
|
1627
|
-
if (BigInt(pos.supplyShares) > 0n) {
|
|
1628
|
-
return {
|
|
1629
|
-
params: {
|
|
1630
|
-
loanToken: m.loanAsset.address,
|
|
1631
|
-
collateralToken: m.collateralAsset.address,
|
|
1632
|
-
oracle: m.oracle,
|
|
1633
|
-
irm: m.irm,
|
|
1634
|
-
lltv: m.lltv,
|
|
1635
|
-
},
|
|
1636
|
-
symbol: m.collateralAsset.symbol,
|
|
1637
|
-
};
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
catch (e) {
|
|
1641
|
-
console.warn('[agether] _findActiveSupplyMarket position check failed:', e instanceof Error ? e.message : e);
|
|
1642
|
-
continue;
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
throw new AgetherError('No active supply position found', 'NO_SUPPLY');
|
|
1646
|
-
}
|
|
1647
|
-
/**
|
|
1648
|
-
* Compute net deposited amount from Morpho Blue events (no DB/indexer).
|
|
1649
|
-
*
|
|
1650
|
-
* Reads Supply and Withdraw events for the lending side (supply/withdraw, NOT
|
|
1651
|
-
* supplyCollateral/withdrawCollateral) filtered by our account address.
|
|
1652
|
-
*
|
|
1653
|
-
* netDeposited = Σ Supply.assets − Σ Withdraw.assets
|
|
1654
|
-
*
|
|
1655
|
-
* Uses eth_getLogs with topic filtering — free, fast, no infrastructure needed.
|
|
1656
|
-
*/
|
|
1657
|
-
async _computeNetDeposited(marketId, accountAddr, morphoAddr) {
|
|
1658
|
-
const morphoContract = new Contract(morphoAddr, MORPHO_BLUE_ABI, this.provider);
|
|
1659
|
-
// Morpho event signatures:
|
|
1660
|
-
// Supply(bytes32 indexed id, address indexed caller, address indexed onBehalf, uint256 assets, uint256 shares)
|
|
1661
|
-
// Withdraw(bytes32 indexed id, address caller, address indexed onBehalf, address indexed receiver, uint256 assets, uint256 shares)
|
|
1662
|
-
const paddedAccount = ethers.zeroPadValue(accountAddr, 32);
|
|
1663
|
-
// Supply events: topic0=Supply, topic1=marketId, topic3=onBehalf (our account)
|
|
1664
|
-
// Note: topic2=caller (indexed), topic3=onBehalf (indexed)
|
|
1665
|
-
const supplyFilter = {
|
|
1666
|
-
address: morphoAddr,
|
|
1667
|
-
topics: [
|
|
1668
|
-
morphoContract.interface.getEvent('Supply').topicHash,
|
|
1669
|
-
marketId, // topic1: indexed id
|
|
1670
|
-
null, // topic2: indexed caller (any)
|
|
1671
|
-
paddedAccount, // topic3: indexed onBehalf
|
|
1672
|
-
],
|
|
1673
|
-
fromBlock: 0,
|
|
1674
|
-
toBlock: 'latest',
|
|
1675
|
-
};
|
|
1676
|
-
// Withdraw events: topic0=Withdraw, topic1=marketId, topic2=onBehalf (our account)
|
|
1677
|
-
// In Withdraw event: id indexed, caller NOT indexed, onBehalf indexed, receiver indexed
|
|
1678
|
-
// So indexed topics: [Withdraw.sig, id, onBehalf, receiver]
|
|
1679
|
-
const withdrawFilter = {
|
|
1680
|
-
address: morphoAddr,
|
|
1681
|
-
topics: [
|
|
1682
|
-
morphoContract.interface.getEvent('Withdraw').topicHash,
|
|
1683
|
-
marketId, // topic1: indexed id
|
|
1684
|
-
paddedAccount, // topic2: indexed onBehalf
|
|
1685
|
-
null, // topic3: indexed receiver (any)
|
|
1686
|
-
],
|
|
1687
|
-
fromBlock: 0,
|
|
1688
|
-
toBlock: 'latest',
|
|
1689
|
-
};
|
|
1690
|
-
let totalSupplied = 0n;
|
|
1691
|
-
let totalWithdrawn = 0n;
|
|
1692
|
-
try {
|
|
1693
|
-
const supplyLogs = await this.provider.getLogs(supplyFilter);
|
|
1694
|
-
for (const log of supplyLogs) {
|
|
1695
|
-
// Non-indexed data: (uint256 assets, uint256 shares)
|
|
1696
|
-
const decoded = ethers.AbiCoder.defaultAbiCoder().decode(['uint256', 'uint256'], log.data);
|
|
1697
|
-
totalSupplied += decoded[0];
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
catch (e) {
|
|
1701
|
-
console.warn('[agether] _computeNetDeposited: Supply logs fetch failed:', e instanceof Error ? e.message : e);
|
|
1702
|
-
}
|
|
1703
|
-
try {
|
|
1704
|
-
const withdrawLogs = await this.provider.getLogs(withdrawFilter);
|
|
1705
|
-
for (const log of withdrawLogs) {
|
|
1706
|
-
// Non-indexed data for Withdraw: (address caller, uint256 assets, uint256 shares)
|
|
1707
|
-
// Because `caller` is NOT indexed in Withdraw, it appears in data
|
|
1708
|
-
const decoded = ethers.AbiCoder.defaultAbiCoder().decode(['address', 'uint256', 'uint256'], log.data);
|
|
1709
|
-
totalWithdrawn += decoded[1]; // assets is second field
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
catch (e) {
|
|
1713
|
-
console.warn('[agether] _computeNetDeposited: Withdraw logs fetch failed:', e instanceof Error ? e.message : e);
|
|
1714
|
-
}
|
|
1715
|
-
return totalSupplied > totalWithdrawn ? totalSupplied - totalWithdrawn : 0n;
|
|
1716
|
-
}
|
|
1717
|
-
}
|