@agether/agether 2.12.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/package.json +2 -2
- package/skills/agether/SKILL.md +6 -4
- package/src/index.ts +393 -279
package/README.md
CHANGED
|
@@ -115,7 +115,9 @@ Once installed, the following tools are available to your AI agent:
|
|
|
115
115
|
| `morpho_repay` | Repay USDC debt |
|
|
116
116
|
| `morpho_withdraw` | Withdraw collateral back to EOA |
|
|
117
117
|
| `morpho_sponsor` | Deposit collateral for another agent (by ID or address) |
|
|
118
|
-
| `
|
|
118
|
+
| `wallet_fund_token` | Transfer any ERC-20 from EOA into AgentAccount |
|
|
119
|
+
| `wallet_transfer` | Transfer any token/ETH from AgentAccount to any address or agent |
|
|
120
|
+
| `wallet_approve` | Approve a spender for tokens in AgentAccount |
|
|
119
121
|
| `x402_pay` | Make paid API calls via x402 protocol (supports auto-draw) |
|
|
120
122
|
|
|
121
123
|
## Slash Commands
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agether/agether",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "OpenClaw plugin for Agether — onchain credit for AI agents on Ethereum & Base",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"openclaw": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
]
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@agether/sdk": "^2.
|
|
12
|
+
"@agether/sdk": "^2.15.0",
|
|
13
13
|
"axios": "^1.6.0",
|
|
14
14
|
"ethers": "^6.9.0"
|
|
15
15
|
},
|
package/skills/agether/SKILL.md
CHANGED
|
@@ -127,7 +127,7 @@ Both `agether_set_agent` and `agether_register` save the agentId to config perma
|
|
|
127
127
|
### Wallet (move funds between EOA ↔ AgentAccount)
|
|
128
128
|
| Tool | Params | What it does |
|
|
129
129
|
|------|--------|-------------|
|
|
130
|
-
| `
|
|
130
|
+
| `wallet_fund_token` | `token`, `amount` | Transfer any ERC-20 token from EOA → AgentAccount |
|
|
131
131
|
| `wallet_withdraw_token` | `token`, `amount` | Withdraw any ERC-20 token from AgentAccount → EOA. Use `amount: "all"` for full balance. |
|
|
132
132
|
| `wallet_withdraw_eth` | `amount` | Withdraw ETH from AgentAccount → EOA. Use `amount: "all"` for full balance. |
|
|
133
133
|
|
|
@@ -251,7 +251,7 @@ IF no auto-funding configured:
|
|
|
251
251
|
```
|
|
252
252
|
1. Check token availability:
|
|
253
253
|
- agether_health for AgentAccount balances
|
|
254
|
-
- If no tokens in AgentAccount but exists in EOA →
|
|
254
|
+
- If no tokens in AgentAccount but exists in EOA → wallet_fund_token first
|
|
255
255
|
2. morpho_markets ← show supply APYs to pick best market
|
|
256
256
|
3. morpho_supply(amount, market?, loanToken?) ← supply tokens to earn yield
|
|
257
257
|
4. Later: morpho_supply_status ← check earned yield
|
|
@@ -450,7 +450,7 @@ If something **fails**:
|
|
|
450
450
|
| `Payment rejected (402)` | No USDC for x402 payment | Borrow USDC first, or enable autoDraw/autoYield in config |
|
|
451
451
|
| `No collateral deposited` | Trying to borrow without collateral | `morpho_deposit` first |
|
|
452
452
|
| `Insufficient collateral` | EOA doesn't have enough of that token | Tell user to send collateral to EOA address |
|
|
453
|
-
| `Insufficient USDC in AgentAccount` | Not enough loan token for repay/pay | Fund via borrow, yield withdrawal, or `
|
|
453
|
+
| `Insufficient USDC in AgentAccount` | Not enough loan token for repay/pay | Fund via borrow, yield withdrawal, or `wallet_fund_token` |
|
|
454
454
|
| `ExecutionFailed` | Smart contract call reverted | Check inner error, usually LTV or approval issue |
|
|
455
455
|
| `PositionNotActive` | No collateral deposited for this token | Deposit collateral with `morpho_deposit` first |
|
|
456
456
|
|
|
@@ -494,7 +494,9 @@ Morpho Blue (direct lending, overcollateralized 125%)
|
|
|
494
494
|
└── Yield tracked via Morpho GraphQL API (no database needed)
|
|
495
495
|
|
|
496
496
|
Wallet transfers:
|
|
497
|
-
├──
|
|
497
|
+
├── wallet_fund_token → any ERC-20 from EOA → AgentAccount
|
|
498
|
+
├── wallet_transfer → any ERC-20 or ETH from AgentAccount → any address/agent
|
|
499
|
+
├── wallet_approve → approve spender for tokens in AgentAccount
|
|
498
500
|
├── wallet_withdraw_token → any ERC-20 from AgentAccount → EOA
|
|
499
501
|
└── wallet_withdraw_eth → ETH from AgentAccount → EOA
|
|
500
502
|
```
|
package/src/index.ts
CHANGED
|
@@ -99,25 +99,155 @@ interface PluginConfig {
|
|
|
99
99
|
|
|
100
100
|
function txLink(hash: string): string {
|
|
101
101
|
if (!hash) return "";
|
|
102
|
-
const explorer = CHAIN_DEFAULTS[activeChainId]?.explorer ?? CHAIN_DEFAULTS[ChainId.Ethereum].explorer;
|
|
102
|
+
const explorer = CHAIN_DEFAULTS[state.activeChainId]?.explorer ?? CHAIN_DEFAULTS[ChainId.Ethereum].explorer;
|
|
103
103
|
return `${explorer}/${hash}`;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
// ─── Unified State Management ─────────────────────────────
|
|
109
|
+
// Phase 1 Fix: Replace scattered module-level state with single class.
|
|
110
|
+
// Eliminates race conditions between memory cache, config, and disk persistence.
|
|
111
|
+
|
|
112
|
+
class AgetherState {
|
|
113
|
+
private agentsByChain: Record<number, string> = {};
|
|
114
|
+
private _activeChainId: ChainId = ChainId.Ethereum;
|
|
115
|
+
private _chainExplicitlySet: boolean = false;
|
|
116
|
+
private configPath: string;
|
|
117
|
+
|
|
118
|
+
constructor() {
|
|
119
|
+
const home = process.env.HOME || process.env.USERPROFILE || "/root";
|
|
120
|
+
this.configPath = path.join(home, ".openclaw", "openclaw.json");
|
|
121
|
+
this.restore();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Restore state from openclaw.json on startup */
|
|
125
|
+
private restore(): void {
|
|
126
|
+
try {
|
|
127
|
+
if (!fs.existsSync(this.configPath)) return;
|
|
128
|
+
const raw = fs.readFileSync(this.configPath, "utf-8");
|
|
129
|
+
const json = JSON.parse(raw);
|
|
130
|
+
const cfg = json?.plugins?.entries?.agether?.config;
|
|
131
|
+
if (!cfg) return;
|
|
132
|
+
|
|
133
|
+
if (cfg.chain !== undefined && cfg.chain !== null && cfg.chain !== "") {
|
|
134
|
+
this._activeChainId = cfg.chain as ChainId;
|
|
135
|
+
this._chainExplicitlySet = true;
|
|
136
|
+
}
|
|
137
|
+
if (cfg.agentId) {
|
|
138
|
+
this.agentsByChain[this._activeChainId] = String(cfg.agentId);
|
|
139
|
+
}
|
|
140
|
+
if (cfg.agentsByChain) {
|
|
141
|
+
for (const [k, v] of Object.entries(cfg.agentsByChain)) {
|
|
142
|
+
this.agentsByChain[Number(k)] = String(v);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// Silently fail on restore — fresh state is fine
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Persist current state to openclaw.json atomically */
|
|
151
|
+
private persist(): void {
|
|
152
|
+
try {
|
|
153
|
+
let json: any = {};
|
|
154
|
+
if (fs.existsSync(this.configPath)) {
|
|
155
|
+
json = JSON.parse(fs.readFileSync(this.configPath, "utf-8"));
|
|
156
|
+
}
|
|
157
|
+
if (!json.plugins) json.plugins = {};
|
|
158
|
+
if (!json.plugins.entries) json.plugins.entries = {};
|
|
159
|
+
if (!json.plugins.entries.agether) json.plugins.entries.agether = {};
|
|
160
|
+
if (!json.plugins.entries.agether.config) json.plugins.entries.agether.config = {};
|
|
161
|
+
|
|
162
|
+
const cfg = json.plugins.entries.agether.config;
|
|
163
|
+
cfg.chain = this._activeChainId;
|
|
164
|
+
cfg.agentId = this.agentsByChain[this._activeChainId] || cfg.agentId;
|
|
165
|
+
cfg.agentsByChain = { ...this.agentsByChain };
|
|
166
|
+
|
|
167
|
+
fs.writeFileSync(this.configPath, JSON.stringify(json, null, 2));
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.warn("[agether] state persist failed:", e instanceof Error ? e.message : String(e));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
get activeChainId(): ChainId { return this._activeChainId; }
|
|
174
|
+
get chainConfigured(): boolean { return this._chainExplicitlySet; }
|
|
175
|
+
|
|
176
|
+
getAgentId(chainId?: ChainId): string | undefined {
|
|
177
|
+
return this.agentsByChain[chainId ?? this._activeChainId];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
setChain(chainId: ChainId): string {
|
|
181
|
+
this._activeChainId = chainId;
|
|
182
|
+
this._chainExplicitlySet = true;
|
|
183
|
+
this.persist();
|
|
184
|
+
return "saved";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
setAgentId(agentId: string, chainId?: ChainId): string {
|
|
188
|
+
const chain = chainId ?? this._activeChainId;
|
|
189
|
+
this.agentsByChain[chain] = agentId;
|
|
190
|
+
this.persist();
|
|
191
|
+
return "saved";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Single global state instance — replaces scattered cachedAgentIds, activeChainId, chainOverride
|
|
196
|
+
const state = new AgetherState();
|
|
197
|
+
|
|
106
198
|
function ok(text: string) {
|
|
199
|
+
// Enhanced: Extract transaction links and key info for user visibility
|
|
200
|
+
try {
|
|
201
|
+
const data = JSON.parse(text);
|
|
202
|
+
let enhanced = "";
|
|
203
|
+
|
|
204
|
+
// Show transaction link if present
|
|
205
|
+
if (data.tx) {
|
|
206
|
+
enhanced += `🔗 **Transaction:** ${txLink(data.tx)}\n`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Show agent ID if present
|
|
210
|
+
if (data.agentId && data.agentId !== '?') {
|
|
211
|
+
enhanced += `🤖 **Agent:** ${data.agentId}\n`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Show balances if present
|
|
215
|
+
if (data.eth || data.usdc) {
|
|
216
|
+
enhanced += `💰 **EOA:** ${data.eth || '0'} ETH, $${data.usdc || '0'} USDC\n`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (enhanced) {
|
|
220
|
+
return { content: [{ type: "text" as const, text: enhanced + "\n```json\n" + text + "\n```" }] };
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// Not JSON, just return as-is
|
|
224
|
+
}
|
|
225
|
+
|
|
107
226
|
return { content: [{ type: "text" as const, text }] };
|
|
108
227
|
}
|
|
109
228
|
|
|
110
229
|
function fail(err: unknown) {
|
|
111
230
|
let msg = err instanceof Error ? err.message : String(err);
|
|
231
|
+
|
|
232
|
+
// Add helpful suggestions for common errors
|
|
233
|
+
let suggestion = "";
|
|
112
234
|
if (msg.includes("0xda04aecc")) {
|
|
113
235
|
msg = "ExceedsMaxLtv — collateral too low for this borrow amount";
|
|
236
|
+
suggestion = "\n💡 Try: Reduce borrow amount or check morpho_max_borrowable";
|
|
114
237
|
} else if (msg.includes("0xfeca99cb")) {
|
|
115
238
|
msg = "ExecutionFailed — the inner contract call reverted. Likely ExceedsMaxLtv or insufficient collateral.";
|
|
239
|
+
suggestion = "\n💡 Try: Check morpho_status and agether_balance";
|
|
116
240
|
} else if (msg.includes("0xa920ef9f")) {
|
|
117
241
|
msg = "PositionNotActive — no collateral deposited for this token";
|
|
242
|
+
suggestion = "\n💡 Try: morpho_deposit first, then borrow";
|
|
243
|
+
} else if (msg.includes("agentId not set")) {
|
|
244
|
+
suggestion = "\n💡 Try: agether_register() first";
|
|
245
|
+
} else if (msg.includes("Chain not configured")) {
|
|
246
|
+
suggestion = "\n💡 Try: agether_set_chain('base') or agether_set_chain('ethereum')";
|
|
118
247
|
}
|
|
248
|
+
|
|
119
249
|
if (msg.length > 300) msg = msg.slice(0, 250) + "…";
|
|
120
|
-
return { content: [{ type: "text" as const, text: `❌ ${msg}` }], isError: true };
|
|
250
|
+
return { content: [{ type: "text" as const, text: `❌ ${msg}${suggestion}` }], isError: true };
|
|
121
251
|
}
|
|
122
252
|
|
|
123
253
|
// ─── Secrets-first config resolution ──────────────────────
|
|
@@ -163,26 +293,22 @@ function resolvePrivateKey(): string {
|
|
|
163
293
|
}
|
|
164
294
|
|
|
165
295
|
function getConfig(api: any): PluginConfig {
|
|
166
|
-
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
const
|
|
170
|
-
const chainConfigured = rawChain !== undefined && rawChain !== null && rawChain !== "";
|
|
171
|
-
const chainId = (rawChain as ChainId) || ChainId.Ethereum;
|
|
172
|
-
activeChainId = chainId; // Update module-level for txLink
|
|
296
|
+
// Phase 1 Fix: Use unified AgetherState instead of scattered module variables.
|
|
297
|
+
// State reads from single source of truth (state object) which is
|
|
298
|
+
// initialized from openclaw.json and updated atomically on mutations.
|
|
299
|
+
const chainId = state.activeChainId;
|
|
173
300
|
return {
|
|
174
301
|
privateKey: resolvePrivateKey(),
|
|
175
|
-
agentId:
|
|
302
|
+
agentId: state.getAgentId(chainId),
|
|
176
303
|
rpcUrl: resolveRpcUrl(chainId),
|
|
177
304
|
chainId,
|
|
178
|
-
chainConfigured,
|
|
305
|
+
chainConfigured: state.chainConfigured,
|
|
179
306
|
};
|
|
180
307
|
}
|
|
181
308
|
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
let chainOverride: ChainId | undefined; // set by agether_set_chain, survives stale api.config
|
|
309
|
+
// Legacy module-level aliases — kept for backward compatibility with txLink()
|
|
310
|
+
// These now delegate to state object instead of being independent variables.
|
|
311
|
+
function get_activeChainId(): ChainId { return state.activeChainId; }
|
|
186
312
|
|
|
187
313
|
/**
|
|
188
314
|
* Hard guardrail: refuse to proceed if chain was never explicitly configured.
|
|
@@ -204,7 +330,7 @@ function requireChain(cfg: PluginConfig): void {
|
|
|
204
330
|
* client (only register() and getBalances() available).
|
|
205
331
|
*/
|
|
206
332
|
function createAgetherClient(cfg: PluginConfig): AgetherClient {
|
|
207
|
-
const agentId =
|
|
333
|
+
const agentId = state.getAgentId(cfg.chainId) || cfg.agentId;
|
|
208
334
|
if (agentId) {
|
|
209
335
|
return AgetherClient.fromPrivateKey(cfg.privateKey, BigInt(agentId), cfg.chainId);
|
|
210
336
|
}
|
|
@@ -216,7 +342,7 @@ function createAgetherClient(cfg: PluginConfig): AgetherClient {
|
|
|
216
342
|
* Requires agentId — use AgetherClient.register() first.
|
|
217
343
|
*/
|
|
218
344
|
function createMorphoClient(cfg: PluginConfig): MorphoClient {
|
|
219
|
-
const agentId =
|
|
345
|
+
const agentId = state.getAgentId(cfg.chainId) || cfg.agentId;
|
|
220
346
|
if (!agentId) {
|
|
221
347
|
throw new Error("agentId not set. Register first with agether_register, then try again.");
|
|
222
348
|
}
|
|
@@ -233,51 +359,14 @@ function createClient(cfg: PluginConfig): MorphoClient {
|
|
|
233
359
|
return createMorphoClient(cfg);
|
|
234
360
|
}
|
|
235
361
|
|
|
236
|
-
/** Persist agentId to
|
|
362
|
+
/** Persist agentId — now delegates to AgetherState for atomic updates. */
|
|
237
363
|
function persistAgentId(agentId: string): string {
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
const home = process.env.HOME || process.env.USERPROFILE || "/root";
|
|
241
|
-
const cfgPath = path.join(home, ".openclaw", "openclaw.json");
|
|
242
|
-
const raw = fs.readFileSync(cfgPath, "utf-8");
|
|
243
|
-
const json = JSON.parse(raw);
|
|
244
|
-
|
|
245
|
-
// Ensure config path exists
|
|
246
|
-
if (!json.plugins) json.plugins = {};
|
|
247
|
-
if (!json.plugins.entries) json.plugins.entries = {};
|
|
248
|
-
if (!json.plugins.entries.agether) json.plugins.entries.agether = {};
|
|
249
|
-
if (!json.plugins.entries.agether.config) json.plugins.entries.agether.config = {};
|
|
250
|
-
|
|
251
|
-
json.plugins.entries.agether.config.agentId = agentId;
|
|
252
|
-
fs.writeFileSync(cfgPath, JSON.stringify(json, null, 2));
|
|
253
|
-
return "saved";
|
|
254
|
-
} catch (e) {
|
|
255
|
-
return `write failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
256
|
-
}
|
|
364
|
+
return state.setAgentId(agentId);
|
|
257
365
|
}
|
|
258
366
|
|
|
259
|
-
/** Persist chain to
|
|
367
|
+
/** Persist chain — now delegates to AgetherState for atomic updates. */
|
|
260
368
|
function persistChainId(chainId: ChainId): string {
|
|
261
|
-
|
|
262
|
-
chainOverride = chainId; // In-memory override so getConfig() picks it up immediately
|
|
263
|
-
try {
|
|
264
|
-
const home = process.env.HOME || process.env.USERPROFILE || "/root";
|
|
265
|
-
const cfgPath = path.join(home, ".openclaw", "openclaw.json");
|
|
266
|
-
const raw = fs.readFileSync(cfgPath, "utf-8");
|
|
267
|
-
const json = JSON.parse(raw);
|
|
268
|
-
|
|
269
|
-
// Ensure config path exists
|
|
270
|
-
if (!json.plugins) json.plugins = {};
|
|
271
|
-
if (!json.plugins.entries) json.plugins.entries = {};
|
|
272
|
-
if (!json.plugins.entries.agether) json.plugins.entries.agether = {};
|
|
273
|
-
if (!json.plugins.entries.agether.config) json.plugins.entries.agether.config = {};
|
|
274
|
-
|
|
275
|
-
json.plugins.entries.agether.config.chain = chainId;
|
|
276
|
-
fs.writeFileSync(cfgPath, JSON.stringify(json, null, 2));
|
|
277
|
-
return "saved";
|
|
278
|
-
} catch (e) {
|
|
279
|
-
return `write failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
280
|
-
}
|
|
369
|
+
return state.setChain(chainId);
|
|
281
370
|
}
|
|
282
371
|
|
|
283
372
|
// ─── Plugin Entry ─────────────────────────────────────────
|
|
@@ -288,8 +377,7 @@ export default function register(api: any) {
|
|
|
288
377
|
// ═══════════════════════════════════════════════════════
|
|
289
378
|
api.registerTool({
|
|
290
379
|
name: "agether_balance",
|
|
291
|
-
description:
|
|
292
|
-
"Check ETH and USDC balances for the agent's EOA wallet and AgentAccount.",
|
|
380
|
+
description: "Check all token balances (ETH, USDC, collateral) for EOA and AgentAccount.",
|
|
293
381
|
parameters: { type: "object", properties: {}, required: [] },
|
|
294
382
|
async execute() {
|
|
295
383
|
try {
|
|
@@ -306,9 +394,7 @@ export default function register(api: any) {
|
|
|
306
394
|
// ═══════════════════════════════════════════════════════
|
|
307
395
|
api.registerTool({
|
|
308
396
|
name: "agether_register",
|
|
309
|
-
description:
|
|
310
|
-
"Register a new ERC-8004 agent identity and create a Safe smart account (via Safe7579). " +
|
|
311
|
-
"REQUIRES chain to be configured first — call agether_set_chain before this tool. Returns the new agentId.",
|
|
397
|
+
description: "Register new agent identity (ERC-8004) and deploy Safe account. Set chain first via agether_set_chain.",
|
|
312
398
|
parameters: {
|
|
313
399
|
type: "object",
|
|
314
400
|
properties: {
|
|
@@ -321,6 +407,29 @@ export default function register(api: any) {
|
|
|
321
407
|
const cfg = getConfig(api);
|
|
322
408
|
requireChain(cfg);
|
|
323
409
|
const client = createAgetherClient(cfg);
|
|
410
|
+
|
|
411
|
+
// Phase 1 Fix: Warn if agentId already cached for this chain
|
|
412
|
+
// The SDK's register() handles double-reg gracefully (returns alreadyRegistered=true),
|
|
413
|
+
// but this gives the user an earlier, clearer signal.
|
|
414
|
+
const existingAgentId = state.getAgentId(cfg.chainId);
|
|
415
|
+
if (existingAgentId) {
|
|
416
|
+
// Agent already registered on this chain — verify before re-registering
|
|
417
|
+
try {
|
|
418
|
+
const verifyClient = AgetherClient.fromPrivateKey(cfg.privateKey, BigInt(existingAgentId), cfg.chainId);
|
|
419
|
+
const acctAddr = await verifyClient.getAccountAddress();
|
|
420
|
+
if (acctAddr) {
|
|
421
|
+
return ok(JSON.stringify({
|
|
422
|
+
status: "already_registered",
|
|
423
|
+
agentId: existingAgentId,
|
|
424
|
+
agentAccount: acctAddr,
|
|
425
|
+
warning: "Agent already registered on this chain. Use agether_set_agent if you need to change agentId.",
|
|
426
|
+
}));
|
|
427
|
+
}
|
|
428
|
+
} catch {
|
|
429
|
+
// Verification failed — maybe stale cache, allow re-registration
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
324
433
|
const result = await client.register({ name: _params.name });
|
|
325
434
|
const persistStatus = persistAgentId(result.agentId);
|
|
326
435
|
const kyaMessage = result.kyaRequired
|
|
@@ -563,9 +672,8 @@ export default function register(api: any) {
|
|
|
563
672
|
// TOOL: morpho_deposit
|
|
564
673
|
// ═══════════════════════════════════════════════════════
|
|
565
674
|
api.registerTool({
|
|
566
|
-
name: "
|
|
567
|
-
description:
|
|
568
|
-
"Deposit collateral into Morpho Blue via AgentAccount. Enables borrowing against it. Token is auto-discovered from available markets.",
|
|
675
|
+
name: "morpho_deposit_collateral",
|
|
676
|
+
description: "Deposit collateral into Morpho Blue to enable borrowing. Validates balance first.",
|
|
569
677
|
parameters: {
|
|
570
678
|
type: "object",
|
|
571
679
|
properties: {
|
|
@@ -579,6 +687,26 @@ export default function register(api: any) {
|
|
|
579
687
|
const cfg = getConfig(api);
|
|
580
688
|
requireChain(cfg);
|
|
581
689
|
const client = createMorphoClient(cfg);
|
|
690
|
+
|
|
691
|
+
// Phase 1 Fix: Validate balance before attempting deposit
|
|
692
|
+
try {
|
|
693
|
+
const agether = createAgetherClient(cfg);
|
|
694
|
+
const balances = await agether.getBalances();
|
|
695
|
+
const tokenBalance = balances.agentAccount?.collateral?.[params.token.toUpperCase()]
|
|
696
|
+
|| balances.collateral?.[params.token.toUpperCase()]
|
|
697
|
+
|| "0";
|
|
698
|
+
const available = parseFloat(tokenBalance);
|
|
699
|
+
const requested = parseFloat(params.amount);
|
|
700
|
+
if (requested > available && available >= 0) {
|
|
701
|
+
return fail(new Error(
|
|
702
|
+
`Insufficient ${params.token} balance. Have: ${available}, Need: ${requested}. ` +
|
|
703
|
+
`Fund your wallet with ${params.token} first, or use wallet_fund_token.`
|
|
704
|
+
));
|
|
705
|
+
}
|
|
706
|
+
} catch {
|
|
707
|
+
// Balance check failed — proceed anyway, on-chain will validate
|
|
708
|
+
}
|
|
709
|
+
|
|
582
710
|
const result = await client.supplyCollateral(params.token, params.amount);
|
|
583
711
|
return ok(JSON.stringify({
|
|
584
712
|
status: "deposited",
|
|
@@ -595,35 +723,32 @@ export default function register(api: any) {
|
|
|
595
723
|
// ═══════════════════════════════════════════════════════
|
|
596
724
|
api.registerTool({
|
|
597
725
|
name: "morpho_deposit_and_borrow",
|
|
598
|
-
description:
|
|
599
|
-
"Deposit collateral AND borrow in one batched transaction via AgentAccount (ERC-7579 batch mode). " +
|
|
600
|
-
"Deposits collateral from EOA into Morpho, then borrows loan token. All in one tx. " +
|
|
601
|
-
"Supports any loan token (USDC, WETH, etc.).",
|
|
726
|
+
description: "Deposit collateral and borrow in one atomic transaction. Gas-efficient batch operation.",
|
|
602
727
|
parameters: {
|
|
603
728
|
type: "object",
|
|
604
729
|
properties: {
|
|
605
|
-
|
|
730
|
+
amount: { type: "string", description: "Collateral amount (e.g. '0.05')" },
|
|
606
731
|
token: { type: "string", description: "Collateral token" },
|
|
607
732
|
borrowAmount: { type: "string", description: "Amount to borrow (e.g. '50')" },
|
|
608
733
|
loanToken: { type: "string", description: "Loan token to borrow (e.g. 'USDC', 'WETH'). Optional — defaults to most liquid market." },
|
|
609
734
|
},
|
|
610
|
-
required: ["
|
|
735
|
+
required: ["amount", "token", "borrowAmount"],
|
|
611
736
|
},
|
|
612
|
-
async execute(_id: string, params: {
|
|
737
|
+
async execute(_id: string, params: { amount: string; token: string; borrowAmount: string; loanToken?: string }) {
|
|
613
738
|
try {
|
|
614
739
|
const cfg = getConfig(api);
|
|
615
740
|
requireChain(cfg);
|
|
616
741
|
const client = createMorphoClient(cfg);
|
|
617
742
|
const result = await client.depositAndBorrow(
|
|
618
743
|
params.token,
|
|
619
|
-
params.
|
|
744
|
+
params.amount,
|
|
620
745
|
params.borrowAmount,
|
|
621
746
|
undefined,
|
|
622
747
|
params.loanToken,
|
|
623
748
|
);
|
|
624
749
|
return ok(JSON.stringify({
|
|
625
750
|
status: "deposited_and_borrowed",
|
|
626
|
-
collateral: `${params.
|
|
751
|
+
collateral: `${params.amount} ${params.token}`,
|
|
627
752
|
borrowed: `${params.borrowAmount}`,
|
|
628
753
|
agentAccount: result.agentAccount,
|
|
629
754
|
tx: txLink(result.tx),
|
|
@@ -632,56 +757,13 @@ export default function register(api: any) {
|
|
|
632
757
|
},
|
|
633
758
|
});
|
|
634
759
|
|
|
635
|
-
// ═══════════════════════════════════════════════════════
|
|
636
|
-
// TOOL: morpho_sponsor
|
|
637
|
-
// ═══════════════════════════════════════════════════════
|
|
638
|
-
api.registerTool({
|
|
639
|
-
name: "agether_sponsor",
|
|
640
|
-
description:
|
|
641
|
-
"Transfer tokens to another agent's Safe account (or any address). " +
|
|
642
|
-
"The target agent can then supply the tokens to Morpho Blue or use them directly.",
|
|
643
|
-
parameters: {
|
|
644
|
-
type: "object",
|
|
645
|
-
properties: {
|
|
646
|
-
agentId: { type: "string", description: "Target agent's ERC-8004 ID (e.g. '17676')" },
|
|
647
|
-
agentAddress: { type: "string", description: "Target account address (alternative to agentId)" },
|
|
648
|
-
amount: { type: "string", description: "Token amount (e.g. '0.05')" },
|
|
649
|
-
token: { type: "string", description: "Token to send (e.g. 'WETH', 'USDC')" },
|
|
650
|
-
},
|
|
651
|
-
required: ["amount", "token"],
|
|
652
|
-
},
|
|
653
|
-
async execute(_id: string, params: { agentId?: string; agentAddress?: string; amount: string; token: string }) {
|
|
654
|
-
try {
|
|
655
|
-
if (!params.agentId && !params.agentAddress) return fail("Provide either agentId or agentAddress");
|
|
656
|
-
const cfg = getConfig(api);
|
|
657
|
-
requireChain(cfg);
|
|
658
|
-
const client = createAgetherClient(cfg);
|
|
659
|
-
|
|
660
|
-
const target = params.agentId
|
|
661
|
-
? { agentId: params.agentId }
|
|
662
|
-
: { address: params.agentAddress! };
|
|
663
|
-
|
|
664
|
-
const result = await client.sponsor(target, params.token, params.amount);
|
|
665
|
-
return ok(JSON.stringify({
|
|
666
|
-
status: "sponsored",
|
|
667
|
-
target: result.targetAccount,
|
|
668
|
-
targetAgentId: result.targetAgentId || "by address",
|
|
669
|
-
amount: `${params.amount} ${params.token}`,
|
|
670
|
-
tx: txLink(result.tx),
|
|
671
|
-
}));
|
|
672
|
-
} catch (e) { return fail(e); }
|
|
673
|
-
},
|
|
674
|
-
});
|
|
675
760
|
|
|
676
761
|
// ═══════════════════════════════════════════════════════
|
|
677
762
|
// TOOL: morpho_borrow
|
|
678
763
|
// ═══════════════════════════════════════════════════════
|
|
679
764
|
api.registerTool({
|
|
680
765
|
name: "morpho_borrow",
|
|
681
|
-
description:
|
|
682
|
-
"Borrow loan token against deposited collateral via Morpho Blue. " +
|
|
683
|
-
"Borrowed tokens land in AgentAccount. Requires collateral deposited first. " +
|
|
684
|
-
"Supports any loan token (USDC, WETH, etc.).",
|
|
766
|
+
description: "Borrow against deposited collateral. Pre-checks health factor and max borrowable.",
|
|
685
767
|
parameters: {
|
|
686
768
|
type: "object",
|
|
687
769
|
properties: {
|
|
@@ -696,14 +778,59 @@ export default function register(api: any) {
|
|
|
696
778
|
const cfg = getConfig(api);
|
|
697
779
|
requireChain(cfg);
|
|
698
780
|
const client = createMorphoClient(cfg);
|
|
781
|
+
|
|
782
|
+
// Phase 1 Fix: Pre-check health factor before borrow
|
|
783
|
+
// Warns if resulting HF would be dangerously low
|
|
784
|
+
let healthWarning = "";
|
|
785
|
+
try {
|
|
786
|
+
const [status, maxBorrow] = await Promise.all([
|
|
787
|
+
client.getStatus(),
|
|
788
|
+
client.getMaxBorrowable(),
|
|
789
|
+
]);
|
|
790
|
+
const maxBorrowUsd = Number(maxBorrow.total) / 1e6;
|
|
791
|
+
const requestedAmount = parseFloat(params.amount);
|
|
792
|
+
|
|
793
|
+
if (requestedAmount > maxBorrowUsd && maxBorrowUsd > 0) {
|
|
794
|
+
return fail(new Error(
|
|
795
|
+
`Borrow amount $${requestedAmount} exceeds max borrowable $${maxBorrowUsd.toFixed(2)}. ` +
|
|
796
|
+
`Reduce amount or deposit more collateral first.`
|
|
797
|
+
));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Estimate resulting health factor
|
|
801
|
+
if (status.positions.length > 0) {
|
|
802
|
+
const currentDebt = parseFloat(status.totalDebt);
|
|
803
|
+
const totalCollateralValue = maxBorrow.byMarket.reduce(
|
|
804
|
+
(sum: number, m: any) => sum + Number(m.collateralValue) / 1e6, 0
|
|
805
|
+
);
|
|
806
|
+
const newDebt = currentDebt + requestedAmount;
|
|
807
|
+
const lltv = 0.80; // Approximate LLTV
|
|
808
|
+
const estimatedHF = totalCollateralValue > 0 ? (totalCollateralValue * lltv) / newDebt : Infinity;
|
|
809
|
+
|
|
810
|
+
if (estimatedHF < 1.1 && estimatedHF !== Infinity) {
|
|
811
|
+
return fail(new Error(
|
|
812
|
+
`⚠️ This borrow would result in estimated Health Factor of ${estimatedHF.toFixed(2)} — DANGEROUSLY CLOSE to liquidation (HF < 1.0). ` +
|
|
813
|
+
`Current debt: $${currentDebt.toFixed(2)}, Collateral value: $${totalCollateralValue.toFixed(2)}. ` +
|
|
814
|
+
`Reduce borrow amount or add more collateral.`
|
|
815
|
+
));
|
|
816
|
+
} else if (estimatedHF < 1.5 && estimatedHF !== Infinity) {
|
|
817
|
+
healthWarning = `⚠️ Warning: Estimated HF after borrow: ${estimatedHF.toFixed(2)} (below safe threshold of 1.5). Consider a smaller amount.`;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
} catch {
|
|
821
|
+
// Pre-check failed — proceed with borrow anyway, on-chain will catch errors
|
|
822
|
+
}
|
|
823
|
+
|
|
699
824
|
const result = await client.borrow(params.amount, params.token, undefined, params.loanToken);
|
|
700
|
-
|
|
825
|
+
const response: any = {
|
|
701
826
|
status: "borrowed",
|
|
702
827
|
amount: params.amount,
|
|
703
828
|
collateral: result.collateralToken,
|
|
704
829
|
agentAccount: result.agentAccount,
|
|
705
830
|
tx: txLink(result.tx),
|
|
706
|
-
}
|
|
831
|
+
};
|
|
832
|
+
if (healthWarning) response.healthWarning = healthWarning;
|
|
833
|
+
return ok(JSON.stringify(response));
|
|
707
834
|
} catch (e) { return fail(e); }
|
|
708
835
|
},
|
|
709
836
|
});
|
|
@@ -713,10 +840,7 @@ export default function register(api: any) {
|
|
|
713
840
|
// ═══════════════════════════════════════════════════════
|
|
714
841
|
api.registerTool({
|
|
715
842
|
name: "morpho_repay",
|
|
716
|
-
description:
|
|
717
|
-
"Repay borrowed loan token back to Morpho Blue from AgentAccount. Reduces debt. " +
|
|
718
|
-
"Use amount 'all' to repay full debt and clear dust shares. " +
|
|
719
|
-
"Supports any loan token (USDC, WETH, etc.).",
|
|
843
|
+
description: "Repay debt to Morpho Blue. Use amount='all' for full repayment including dust.",
|
|
720
844
|
parameters: {
|
|
721
845
|
type: "object",
|
|
722
846
|
properties: {
|
|
@@ -784,7 +908,7 @@ export default function register(api: any) {
|
|
|
784
908
|
// TOOL: morpho_supply (lender-side — earn yield)
|
|
785
909
|
// ═══════════════════════════════════════════════════════
|
|
786
910
|
api.registerTool({
|
|
787
|
-
name: "
|
|
911
|
+
name: "morpho_lend",
|
|
788
912
|
description:
|
|
789
913
|
"Supply a loan token (USDC, WETH, etc.) to a Morpho Blue market as a LENDER to earn yield. " +
|
|
790
914
|
"This is the lending side — your tokens earn interest from borrowers. " +
|
|
@@ -828,7 +952,7 @@ export default function register(api: any) {
|
|
|
828
952
|
// TOOL: morpho_supply_status (supply positions + yield)
|
|
829
953
|
// ═══════════════════════════════════════════════════════
|
|
830
954
|
api.registerTool({
|
|
831
|
-
name: "
|
|
955
|
+
name: "morpho_lending_status",
|
|
832
956
|
description:
|
|
833
957
|
"Show all supply (lending) positions with earned yield. " +
|
|
834
958
|
"Tracks yield WITHOUT any database — reads Morpho events directly from blockchain. " +
|
|
@@ -881,7 +1005,7 @@ export default function register(api: any) {
|
|
|
881
1005
|
// TOOL: morpho_withdraw_supply (withdraw lent tokens)
|
|
882
1006
|
// ═══════════════════════════════════════════════════════
|
|
883
1007
|
api.registerTool({
|
|
884
|
-
name: "
|
|
1008
|
+
name: "morpho_withdraw_lending",
|
|
885
1009
|
description:
|
|
886
1010
|
"Withdraw supplied tokens (principal + earned interest) from a Morpho Blue lending position. " +
|
|
887
1011
|
"By default keeps tokens in AgentAccount. Set toEoa=true to send to EOA wallet. " +
|
|
@@ -923,9 +1047,7 @@ export default function register(api: any) {
|
|
|
923
1047
|
// ═══════════════════════════════════════════════════════
|
|
924
1048
|
api.registerTool({
|
|
925
1049
|
name: "morpho_markets",
|
|
926
|
-
description:
|
|
927
|
-
"List available Morpho Blue markets — liquidity, supply/borrow APY, utilization, LLTV. " +
|
|
928
|
-
"Supports all loan tokens (USDC, WETH, etc.). Optionally filter by collateral or loan token.",
|
|
1050
|
+
description: "List Morpho Blue markets — collateral/loan pairs, APY rates, liquidity.",
|
|
929
1051
|
parameters: {
|
|
930
1052
|
type: "object",
|
|
931
1053
|
properties: {
|
|
@@ -1060,7 +1182,7 @@ export default function register(api: any) {
|
|
|
1060
1182
|
// TOOL: morpho_supply_options (find lending opportunities)
|
|
1061
1183
|
// ═══════════════════════════════════════════════════════
|
|
1062
1184
|
api.registerTool({
|
|
1063
|
-
name: "
|
|
1185
|
+
name: "morpho_lending_options",
|
|
1064
1186
|
description:
|
|
1065
1187
|
"Find supply/lending opportunities for a specific loan token. " +
|
|
1066
1188
|
"Use when user asks 'what can I supply to earn WETH?', 'where can I lend USDC?', " +
|
|
@@ -1133,9 +1255,7 @@ export default function register(api: any) {
|
|
|
1133
1255
|
// ═══════════════════════════════════════════════════════
|
|
1134
1256
|
api.registerTool({
|
|
1135
1257
|
name: "agether_score",
|
|
1136
|
-
description:
|
|
1137
|
-
"Get the agent's current onchain credit score. " +
|
|
1138
|
-
"Use 'refresh' param to request a fresh score computation (costs USDC via x402).",
|
|
1258
|
+
description: "Get credit score. Set refresh=true to recompute (costs USDC via x402). Checks staleness first.",
|
|
1139
1259
|
parameters: {
|
|
1140
1260
|
type: "object",
|
|
1141
1261
|
properties: {
|
|
@@ -1151,6 +1271,25 @@ export default function register(api: any) {
|
|
|
1151
1271
|
const agentId = client.getAgentId().toString();
|
|
1152
1272
|
|
|
1153
1273
|
if (params.refresh) {
|
|
1274
|
+
// Phase 1 Fix: Check if current score is still fresh before paying for refresh
|
|
1275
|
+
try {
|
|
1276
|
+
const scoring0 = new ScoringClient({ endpoint: BACKEND_URL, chainId: cfg.chainId });
|
|
1277
|
+
const current = await scoring0.getCurrentScore(agentId);
|
|
1278
|
+
if (current && current.fresh) {
|
|
1279
|
+
return ok(JSON.stringify({
|
|
1280
|
+
status: "score_still_fresh",
|
|
1281
|
+
warning: "Current score is still fresh — paying for a new score is unnecessary.",
|
|
1282
|
+
currentScore: current.score,
|
|
1283
|
+
scoreAge: current.age,
|
|
1284
|
+
suggestion: "Score will need refresh when age exceeds freshness threshold. " +
|
|
1285
|
+
"To force refresh anyway, call agether_score with refresh=true again.",
|
|
1286
|
+
...current,
|
|
1287
|
+
}));
|
|
1288
|
+
}
|
|
1289
|
+
} catch {
|
|
1290
|
+
// If staleness check fails, proceed with refresh anyway
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1154
1293
|
// x402-gated fresh score via ScoringClient
|
|
1155
1294
|
const accountAddress = await client.getAccountAddress();
|
|
1156
1295
|
const contracts = getContractAddresses(cfg.chainId);
|
|
@@ -1181,15 +1320,46 @@ export default function register(api: any) {
|
|
|
1181
1320
|
});
|
|
1182
1321
|
|
|
1183
1322
|
// ═══════════════════════════════════════════════════════
|
|
1184
|
-
// TOOL:
|
|
1323
|
+
// TOOL: wallet_withdraw_token
|
|
1324
|
+
// ═══════════════════════════════════════════════════════
|
|
1325
|
+
api.registerTool({
|
|
1326
|
+
name: "wallet_withdraw_token",
|
|
1327
|
+
description: "Withdraw ERC-20 token from AgentAccount to EOA. Use amount='all' for full balance.",
|
|
1328
|
+
parameters: {
|
|
1329
|
+
type: "object",
|
|
1330
|
+
properties: {
|
|
1331
|
+
token: { type: "string", description: "Token to withdraw (e.g. 'USDC', 'WETH')" },
|
|
1332
|
+
amount: { type: "string", description: "Amount to withdraw (e.g. '100' or 'all')" },
|
|
1333
|
+
},
|
|
1334
|
+
required: ["token", "amount"],
|
|
1335
|
+
},
|
|
1336
|
+
async execute(_id: string, params: { token: string; amount: string }) {
|
|
1337
|
+
try {
|
|
1338
|
+
const cfg = getConfig(api);
|
|
1339
|
+
requireChain(cfg);
|
|
1340
|
+
const client = createAgetherClient(cfg);
|
|
1341
|
+
const result = await client.withdrawToken(params.token, params.amount);
|
|
1342
|
+
return ok(JSON.stringify({
|
|
1343
|
+
status: "withdrawn",
|
|
1344
|
+
token: result.token,
|
|
1345
|
+
amount: result.amount,
|
|
1346
|
+
destination: result.destination,
|
|
1347
|
+
tx: txLink(result.tx),
|
|
1348
|
+
}));
|
|
1349
|
+
} catch (e) { return fail(e); }
|
|
1350
|
+
},
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
// ═══════════════════════════════════════════════════════
|
|
1354
|
+
// TOOL: wallet_withdraw_eth
|
|
1185
1355
|
// ═══════════════════════════════════════════════════════
|
|
1186
1356
|
api.registerTool({
|
|
1187
|
-
name: "
|
|
1188
|
-
description: "
|
|
1357
|
+
name: "wallet_withdraw_eth",
|
|
1358
|
+
description: "Withdraw ETH from AgentAccount to EOA. Use amount='all' for full balance.",
|
|
1189
1359
|
parameters: {
|
|
1190
1360
|
type: "object",
|
|
1191
1361
|
properties: {
|
|
1192
|
-
amount: { type: "string", description: "
|
|
1362
|
+
amount: { type: "string", description: "ETH amount to withdraw (e.g. '0.01' or 'all')" },
|
|
1193
1363
|
},
|
|
1194
1364
|
required: ["amount"],
|
|
1195
1365
|
},
|
|
@@ -1198,10 +1368,43 @@ export default function register(api: any) {
|
|
|
1198
1368
|
const cfg = getConfig(api);
|
|
1199
1369
|
requireChain(cfg);
|
|
1200
1370
|
const client = createAgetherClient(cfg);
|
|
1201
|
-
const result = await client.
|
|
1371
|
+
const result = await client.withdrawEth(params.amount);
|
|
1372
|
+
return ok(JSON.stringify({
|
|
1373
|
+
status: "withdrawn",
|
|
1374
|
+
token: "ETH",
|
|
1375
|
+
amount: `${result.amount} ETH`,
|
|
1376
|
+
destination: result.destination,
|
|
1377
|
+
tx: txLink(result.tx),
|
|
1378
|
+
}));
|
|
1379
|
+
} catch (e) { return fail(e); }
|
|
1380
|
+
},
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
// ═══════════════════════════════════════════════════════
|
|
1384
|
+
// TOOL: wallet_fund_token
|
|
1385
|
+
// ═══════════════════════════════════════════════════════
|
|
1386
|
+
api.registerTool({
|
|
1387
|
+
name: "wallet_fund_token",
|
|
1388
|
+
description:
|
|
1389
|
+
"Transfer any ERC-20 token from EOA wallet into AgentAccount. " +
|
|
1390
|
+
"Works with USDC, WETH, wstETH, cbBTC, or any token supported by Morpho markets.",
|
|
1391
|
+
parameters: {
|
|
1392
|
+
type: "object",
|
|
1393
|
+
properties: {
|
|
1394
|
+
token: { type: "string", description: "Token to transfer (e.g. 'USDC', 'WETH')" },
|
|
1395
|
+
amount: { type: "string", description: "Amount to transfer (e.g. '100')" },
|
|
1396
|
+
},
|
|
1397
|
+
required: ["token", "amount"],
|
|
1398
|
+
},
|
|
1399
|
+
async execute(_id: string, params: { token: string; amount: string }) {
|
|
1400
|
+
try {
|
|
1401
|
+
const cfg = getConfig(api);
|
|
1402
|
+
requireChain(cfg);
|
|
1403
|
+
const client = createAgetherClient(cfg);
|
|
1404
|
+
const result = await client.fundAccountToken(params.token, params.amount);
|
|
1202
1405
|
return ok(JSON.stringify({
|
|
1203
1406
|
status: "funded",
|
|
1204
|
-
amount:
|
|
1407
|
+
amount: `${params.amount} ${params.token}`,
|
|
1205
1408
|
tx: txLink(result.txHash),
|
|
1206
1409
|
}));
|
|
1207
1410
|
} catch (e) { return fail(e); }
|
|
@@ -1209,32 +1412,45 @@ export default function register(api: any) {
|
|
|
1209
1412
|
});
|
|
1210
1413
|
|
|
1211
1414
|
// ═══════════════════════════════════════════════════════
|
|
1212
|
-
// TOOL:
|
|
1415
|
+
// TOOL: wallet_transfer
|
|
1213
1416
|
// ═══════════════════════════════════════════════════════
|
|
1214
1417
|
api.registerTool({
|
|
1215
|
-
name: "
|
|
1418
|
+
name: "wallet_transfer",
|
|
1216
1419
|
description:
|
|
1217
|
-
"
|
|
1218
|
-
"
|
|
1219
|
-
"Use amount 'all' to
|
|
1420
|
+
"Transfer any ERC-20 token or ETH from AgentAccount to any address or agent. " +
|
|
1421
|
+
"Specify either 'address' (0x...) or 'agentId' as destination. " +
|
|
1422
|
+
"Set token to 'ETH' for native ETH transfers. Use amount 'all' to send full balance.",
|
|
1220
1423
|
parameters: {
|
|
1221
1424
|
type: "object",
|
|
1222
1425
|
properties: {
|
|
1223
|
-
token: { type: "string", description: "Token to
|
|
1224
|
-
amount: { type: "string", description: "Amount to
|
|
1426
|
+
token: { type: "string", description: "Token to send (e.g. 'USDC', 'WETH', 'ETH')" },
|
|
1427
|
+
amount: { type: "string", description: "Amount to send (e.g. '100' or 'all')" },
|
|
1428
|
+
address: { type: "string", description: "Destination address (0x...)" },
|
|
1429
|
+
agentId: { type: "string", description: "Destination agent ID" },
|
|
1225
1430
|
},
|
|
1226
1431
|
required: ["token", "amount"],
|
|
1227
1432
|
},
|
|
1228
|
-
async execute(_id: string, params: { token: string; amount: string }) {
|
|
1433
|
+
async execute(_id: string, params: { token: string; amount: string; address?: string; agentId?: string }) {
|
|
1229
1434
|
try {
|
|
1230
1435
|
const cfg = getConfig(api);
|
|
1231
1436
|
requireChain(cfg);
|
|
1232
1437
|
const client = createAgetherClient(cfg);
|
|
1233
|
-
|
|
1438
|
+
|
|
1439
|
+
if (!params.address && !params.agentId) {
|
|
1440
|
+
return fail(new Error("Provide either 'address' or 'agentId' as destination"));
|
|
1441
|
+
}
|
|
1442
|
+
const to = params.address ? { address: params.address } : { agentId: params.agentId };
|
|
1443
|
+
|
|
1444
|
+
let result;
|
|
1445
|
+
if (params.token.toUpperCase() === 'ETH') {
|
|
1446
|
+
result = await client.transferEth(params.amount, to);
|
|
1447
|
+
} else {
|
|
1448
|
+
result = await client.transferToken(params.token, params.amount, to);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1234
1451
|
return ok(JSON.stringify({
|
|
1235
|
-
status: "
|
|
1236
|
-
|
|
1237
|
-
amount: result.amount,
|
|
1452
|
+
status: "transferred",
|
|
1453
|
+
amount: `${result.amount} ${result.token}`,
|
|
1238
1454
|
destination: result.destination,
|
|
1239
1455
|
tx: txLink(result.tx),
|
|
1240
1456
|
}));
|
|
@@ -1243,31 +1459,41 @@ export default function register(api: any) {
|
|
|
1243
1459
|
});
|
|
1244
1460
|
|
|
1245
1461
|
// ═══════════════════════════════════════════════════════
|
|
1246
|
-
// TOOL:
|
|
1462
|
+
// TOOL: wallet_approve
|
|
1247
1463
|
// ═══════════════════════════════════════════════════════
|
|
1248
1464
|
api.registerTool({
|
|
1249
|
-
name: "
|
|
1465
|
+
name: "wallet_approve",
|
|
1250
1466
|
description:
|
|
1251
|
-
"
|
|
1252
|
-
"
|
|
1467
|
+
"Approve a spender (address or agent) to use ERC-20 tokens from AgentAccount. " +
|
|
1468
|
+
"Specify either 'address' (0x...) or 'agentId' as spender. " +
|
|
1469
|
+
"Use amount 'max' for unlimited approval.",
|
|
1253
1470
|
parameters: {
|
|
1254
1471
|
type: "object",
|
|
1255
1472
|
properties: {
|
|
1256
|
-
|
|
1473
|
+
token: { type: "string", description: "Token to approve (e.g. 'USDC', 'WETH')" },
|
|
1474
|
+
amount: { type: "string", description: "Allowance amount (e.g. '1000' or 'max')" },
|
|
1475
|
+
address: { type: "string", description: "Spender address (0x...)" },
|
|
1476
|
+
agentId: { type: "string", description: "Spender agent ID" },
|
|
1257
1477
|
},
|
|
1258
|
-
required: ["amount"],
|
|
1478
|
+
required: ["token", "amount"],
|
|
1259
1479
|
},
|
|
1260
|
-
async execute(_id: string, params: { amount: string }) {
|
|
1480
|
+
async execute(_id: string, params: { token: string; amount: string; address?: string; agentId?: string }) {
|
|
1261
1481
|
try {
|
|
1262
1482
|
const cfg = getConfig(api);
|
|
1263
1483
|
requireChain(cfg);
|
|
1264
1484
|
const client = createAgetherClient(cfg);
|
|
1265
|
-
|
|
1485
|
+
|
|
1486
|
+
if (!params.address && !params.agentId) {
|
|
1487
|
+
return fail(new Error("Provide either 'address' or 'agentId' as spender"));
|
|
1488
|
+
}
|
|
1489
|
+
const spender = params.address ? { address: params.address } : { agentId: params.agentId };
|
|
1490
|
+
|
|
1491
|
+
const result = await client.approveToken(params.token, params.amount, spender);
|
|
1266
1492
|
return ok(JSON.stringify({
|
|
1267
|
-
status: "
|
|
1268
|
-
token:
|
|
1269
|
-
amount:
|
|
1270
|
-
|
|
1493
|
+
status: "approved",
|
|
1494
|
+
token: result.token,
|
|
1495
|
+
amount: result.amount,
|
|
1496
|
+
spender: result.spender,
|
|
1271
1497
|
tx: txLink(result.tx),
|
|
1272
1498
|
}));
|
|
1273
1499
|
} catch (e) { return fail(e); }
|
|
@@ -1568,7 +1794,7 @@ export default function register(api: any) {
|
|
|
1568
1794
|
if (safeUsdc > 0) {
|
|
1569
1795
|
checks.push({ check: "USDC (AgentAccount)", status: `✅ $${safeUsdc.toFixed(2)}` });
|
|
1570
1796
|
} else if (eoaUsdc > 0) {
|
|
1571
|
-
checks.push({ check: "USDC", status: `⚠️ $${eoaUsdc.toFixed(2)} in EOA only`, detail: "Use
|
|
1797
|
+
checks.push({ check: "USDC", status: `⚠️ $${eoaUsdc.toFixed(2)} in EOA only`, detail: "Use wallet_fund_token or morpho_deposit_and_borrow to get USDC into AgentAccount" });
|
|
1572
1798
|
} else {
|
|
1573
1799
|
checks.push({ check: "USDC", status: "❌ no USDC anywhere", detail: "Deposit collateral and borrow, or send USDC to EOA" });
|
|
1574
1800
|
}
|
|
@@ -1584,120 +1810,8 @@ export default function register(api: any) {
|
|
|
1584
1810
|
// SLASH COMMANDS (no AI needed)
|
|
1585
1811
|
// ═══════════════════════════════════════════════════════
|
|
1586
1812
|
|
|
1587
|
-
api.registerCommand({
|
|
1588
|
-
name: "balance",
|
|
1589
|
-
description: "Show agent wallet balances (no AI)",
|
|
1590
|
-
handler: async () => {
|
|
1591
|
-
try {
|
|
1592
|
-
const cfg = getConfig(api);
|
|
1593
|
-
const client = createAgetherClient(cfg);
|
|
1594
|
-
const b = await client.getBalances();
|
|
1595
|
-
|
|
1596
|
-
const nz = (v: string) => parseFloat(v) > 0;
|
|
1597
|
-
|
|
1598
|
-
let text = `💰 Agent #${b.agentId}\n`;
|
|
1599
|
-
text += `Address: ${b.address}`;
|
|
1600
|
-
if (nz(b.eth)) text += `\nETH: ${parseFloat(b.eth).toFixed(6)}`;
|
|
1601
|
-
if (nz(b.usdc)) text += `\nUSDC: $${b.usdc}`;
|
|
1602
|
-
for (const [sym, val] of Object.entries(b.collateral ?? {})) {
|
|
1603
|
-
if (nz(val)) text += `\n${sym}: ${parseFloat(val).toFixed(6)}`;
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
if (b.agentAccount) {
|
|
1607
|
-
const a = b.agentAccount;
|
|
1608
|
-
const hasAny = nz(a.eth) || nz(a.usdc) ||
|
|
1609
|
-
Object.values(a.collateral ?? {}).some(nz);
|
|
1610
|
-
if (hasAny) {
|
|
1611
|
-
text += `\n\n🏦 AgentAccount: ${a.address}`;
|
|
1612
|
-
if (nz(a.eth)) text += `\nETH: ${parseFloat(a.eth).toFixed(6)}`;
|
|
1613
|
-
if (nz(a.usdc)) text += `\nUSDC: $${a.usdc}`;
|
|
1614
|
-
for (const [sym, val] of Object.entries(a.collateral ?? {})) {
|
|
1615
|
-
if (nz(val)) text += `\n${sym}: ${parseFloat(val).toFixed(6)}`;
|
|
1616
|
-
}
|
|
1617
|
-
} else {
|
|
1618
|
-
text += `\n\n🏦 AgentAccount: ${a.address}\n(empty)`;
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
return { text };
|
|
1623
|
-
} catch (e: any) {
|
|
1624
|
-
return { text: `❌ ${e.message}` };
|
|
1625
|
-
}
|
|
1626
|
-
},
|
|
1627
|
-
});
|
|
1628
|
-
|
|
1629
|
-
api.registerCommand({
|
|
1630
|
-
name: "morpho",
|
|
1631
|
-
description: "Show Morpho positions with Health Factor (no AI)",
|
|
1632
|
-
handler: async () => {
|
|
1633
|
-
try {
|
|
1634
|
-
const cfg = getConfig(api);
|
|
1635
|
-
const client = createMorphoClient(cfg);
|
|
1636
|
-
const [s, maxBorrow] = await Promise.all([
|
|
1637
|
-
client.getStatus(),
|
|
1638
|
-
client.getMaxBorrowable(),
|
|
1639
|
-
]);
|
|
1640
|
-
|
|
1641
|
-
let text = `📊 Morpho\nAccount: ${s.agentAccount}\nTotal debt: $${s.totalDebt}\n`;
|
|
1642
|
-
for (const p of s.positions) {
|
|
1643
|
-
const mm = maxBorrow.byMarket.find((m: any) => m.collateralToken === p.collateralToken);
|
|
1644
|
-
const cv = mm ? Number(mm.collateralValue) / 1e6 : 0;
|
|
1645
|
-
const debt = parseFloat(p.debt);
|
|
1646
|
-
const ltv = cv > 0 ? debt / cv : 0;
|
|
1647
|
-
const hf = ltv > 0 ? 0.80 / ltv : Infinity;
|
|
1648
|
-
const hfStr = debt === 0 ? "∞" : hf.toFixed(2);
|
|
1649
|
-
const icon = debt === 0 ? "🟢" : hf <= 1.0 ? "🔴" : hf <= 1.15 ? "🟡" : "🟢";
|
|
1650
|
-
text += `\n${icon} ${p.collateralToken}: ${p.collateral} col, $${p.debt} debt, HF ${hfStr}`;
|
|
1651
|
-
}
|
|
1652
|
-
if (s.positions.length === 0) text += "\nNo active positions.";
|
|
1653
|
-
return { text };
|
|
1654
|
-
} catch (e: any) {
|
|
1655
|
-
return { text: `❌ ${e.message}` };
|
|
1656
|
-
}
|
|
1657
|
-
},
|
|
1658
|
-
});
|
|
1659
|
-
|
|
1660
|
-
api.registerCommand({
|
|
1661
|
-
name: "health",
|
|
1662
|
-
description: "Quick position health check — LTV, liquidation risk, balances (no AI)",
|
|
1663
|
-
handler: async () => {
|
|
1664
|
-
try {
|
|
1665
|
-
const cfg = getConfig(api);
|
|
1666
|
-
const agetherCfg = api.config?.plugins?.entries?.agether?.config ?? {};
|
|
1667
|
-
const healthThreshold = agetherCfg.healthAlertThreshold ?? 70;
|
|
1668
|
-
const agether = createAgetherClient(cfg);
|
|
1669
|
-
const morpho = createMorphoClient(cfg);
|
|
1670
|
-
|
|
1671
|
-
const [balances, status, maxBorrow] = await Promise.all([
|
|
1672
|
-
agether.getBalances(),
|
|
1673
|
-
morpho.getStatus(),
|
|
1674
|
-
morpho.getMaxBorrowable(),
|
|
1675
|
-
]);
|
|
1676
|
-
|
|
1677
|
-
let text = `🏥 Health — Agent #${balances.agentId}\n`;
|
|
1678
|
-
text += `EOA: ${balances.eth} ETH, $${balances.usdc} USDC\n`;
|
|
1679
|
-
if (balances.agentAccount) {
|
|
1680
|
-
text += `Safe: $${balances.agentAccount.usdc} USDC\n`;
|
|
1681
|
-
}
|
|
1682
|
-
text += `Total debt: $${status.totalDebt}\n`;
|
|
1683
|
-
text += `Headroom: $${(Number(maxBorrow.total) / 1e6).toFixed(2)}\n`;
|
|
1684
1813
|
|
|
1685
|
-
for (const p of status.positions) {
|
|
1686
|
-
const mm = maxBorrow.byMarket.find((m: any) => m.collateralToken === p.collateralToken);
|
|
1687
|
-
const cv = mm ? Number(mm.collateralValue) / 1e6 : 0;
|
|
1688
|
-
const debt = parseFloat(p.debt);
|
|
1689
|
-
const ltv = cv > 0 ? (debt / cv) * 100 : 0;
|
|
1690
|
-
const icon = debt === 0 ? "🟢" : ltv >= 80 ? "🔴" : ltv >= healthThreshold ? "🟡" : "🟢";
|
|
1691
|
-
text += `\n${icon} ${p.collateralToken}: ${p.collateral} col, $${p.debt} debt, LTV ${ltv.toFixed(1)}%`;
|
|
1692
|
-
}
|
|
1693
|
-
if (status.positions.length === 0) text += "\nNo active positions.";
|
|
1694
1814
|
|
|
1695
|
-
return { text };
|
|
1696
|
-
} catch (e: any) {
|
|
1697
|
-
return { text: `❌ ${e.message}` };
|
|
1698
|
-
}
|
|
1699
|
-
},
|
|
1700
|
-
});
|
|
1701
1815
|
|
|
1702
1816
|
api.registerCommand({
|
|
1703
1817
|
name: "rates",
|
|
@@ -1736,7 +1850,7 @@ export default function register(api: any) {
|
|
|
1736
1850
|
const balances = await client.getBalances();
|
|
1737
1851
|
const agentId = balances.agentId ?? "?";
|
|
1738
1852
|
const safeUsdc = balances.agentAccount?.usdc ?? "0";
|
|
1739
|
-
const chainName = CHAIN_DEFAULTS[
|
|
1853
|
+
const chainName = CHAIN_DEFAULTS[state.activeChainId]?.chainName ?? `Chain ${state.activeChainId}`;
|
|
1740
1854
|
api.logger?.info?.(
|
|
1741
1855
|
`[agether] Session start — ${chainName}, Agent #${agentId}, EOA: ${balances.eth} ETH / $${balances.usdc} USDC, Safe: $${safeUsdc} USDC`,
|
|
1742
1856
|
);
|