@agether/sdk 1.11.1 → 2.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/dist/cli.js +2375 -0
- package/dist/index.d.mts +77 -32
- package/dist/index.d.ts +77 -32
- package/dist/index.js +304 -169
- package/dist/index.mjs +299 -167
- package/package.json +1 -1
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2375 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
|
|
33
|
+
// src/types/index.ts
|
|
34
|
+
var AgetherError;
|
|
35
|
+
var init_types = __esm({
|
|
36
|
+
"src/types/index.ts"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
AgetherError = class extends Error {
|
|
39
|
+
constructor(message, code, details) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.code = code;
|
|
42
|
+
this.details = details;
|
|
43
|
+
this.name = "AgetherError";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// src/utils/abis.ts
|
|
50
|
+
var IDENTITY_REGISTRY_ABI, SAFE_AGENT_FACTORY_ABI, ACCOUNT_FACTORY_ABI, ERC8004_VALIDATION_MODULE_ABI, AGENT_REPUTATION_ABI, MORPHO_BLUE_ABI, ERC20_ABI, ENTRYPOINT_V07_ABI, SAFE7579_ACCOUNT_ABI;
|
|
51
|
+
var init_abis = __esm({
|
|
52
|
+
"src/utils/abis.ts"() {
|
|
53
|
+
"use strict";
|
|
54
|
+
IDENTITY_REGISTRY_ABI = [
|
|
55
|
+
"function ownerOf(uint256 agentId) view returns (address)",
|
|
56
|
+
"function balanceOf(address owner) view returns (uint256)",
|
|
57
|
+
"function totalSupply() view returns (uint256)",
|
|
58
|
+
"function exists(uint256 agentId) view returns (bool)",
|
|
59
|
+
"function register() returns (uint256 agentId)",
|
|
60
|
+
"event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)"
|
|
61
|
+
];
|
|
62
|
+
SAFE_AGENT_FACTORY_ABI = [
|
|
63
|
+
"function getAccount(uint256 agentId) view returns (address)",
|
|
64
|
+
"function accountExists(uint256 agentId) view returns (bool)",
|
|
65
|
+
"function totalAccounts() view returns (uint256)",
|
|
66
|
+
"function getAgentId(address account) view returns (uint256)",
|
|
67
|
+
"function getAgentIdByIndex(uint256 index) view returns (uint256)",
|
|
68
|
+
"function getAllAgentIds() view returns (uint256[])",
|
|
69
|
+
"function createAccount(uint256 agentId) returns (address safeAccount)",
|
|
70
|
+
"function identityRegistry() view returns (address)",
|
|
71
|
+
"function validationModule() view returns (address)",
|
|
72
|
+
"function hookMultiplexer() view returns (address)",
|
|
73
|
+
"function safeSingleton() view returns (address)",
|
|
74
|
+
"function safe7579() view returns (address)",
|
|
75
|
+
"function bootstrap() view returns (address)",
|
|
76
|
+
"function SENTINEL_OWNER() view returns (address)",
|
|
77
|
+
"event AccountCreated(uint256 indexed agentId, address indexed safeAccount, address indexed owner)"
|
|
78
|
+
];
|
|
79
|
+
ACCOUNT_FACTORY_ABI = SAFE_AGENT_FACTORY_ABI;
|
|
80
|
+
ERC8004_VALIDATION_MODULE_ABI = [
|
|
81
|
+
// View
|
|
82
|
+
"function getConfig(address account) view returns (address registry, uint256 agentId)",
|
|
83
|
+
"function getOwner(address account) view returns (address)",
|
|
84
|
+
"function isInstalled(address account) view returns (bool)",
|
|
85
|
+
"function isKYAApproved(address account) view returns (bool)",
|
|
86
|
+
"function validationRegistry() view returns (address)",
|
|
87
|
+
"function owner() view returns (address)",
|
|
88
|
+
// Admin (via TimelockController)
|
|
89
|
+
"function setValidationRegistry(address registry_)"
|
|
90
|
+
];
|
|
91
|
+
AGENT_REPUTATION_ABI = [
|
|
92
|
+
"function getCreditScore(uint256 agentId) view returns (uint256)",
|
|
93
|
+
"function getAttestation(uint256 agentId) view returns (tuple(uint256 score, uint256 timestamp, address signer))",
|
|
94
|
+
"function isScoreFresh(uint256 agentId) view returns (bool fresh, uint256 age)",
|
|
95
|
+
"function isEligible(uint256 agentId, uint256 minScore) view returns (bool eligible, uint256 currentScore)",
|
|
96
|
+
"function oracleSigner() view returns (address)",
|
|
97
|
+
"function submitScore(uint256 agentId, uint256 score_, uint256 timestamp_, bytes signature)",
|
|
98
|
+
"function setOracleSigner(address signer_)",
|
|
99
|
+
"event ScoreUpdated(uint256 indexed agentId, uint256 score, uint256 timestamp, address signer)"
|
|
100
|
+
];
|
|
101
|
+
MORPHO_BLUE_ABI = [
|
|
102
|
+
// Supply & Withdraw (lending side)
|
|
103
|
+
"function supply(tuple(address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) returns (uint256 assetsSupplied, uint256 sharesSupplied)",
|
|
104
|
+
"function withdraw(tuple(address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) returns (uint256 assetsWithdrawn, uint256 sharesWithdrawn)",
|
|
105
|
+
// Collateral
|
|
106
|
+
"function supplyCollateral(tuple(address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, address onBehalf, bytes data)",
|
|
107
|
+
"function withdrawCollateral(tuple(address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, address onBehalf, address receiver)",
|
|
108
|
+
// Borrow & Repay
|
|
109
|
+
"function borrow(tuple(address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, uint256 shares, address onBehalf, address receiver) returns (uint256 assetsBorrowed, uint256 sharesBorrowed)",
|
|
110
|
+
"function repay(tuple(address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, uint256 assets, uint256 shares, address onBehalf, bytes data) returns (uint256 assetsRepaid, uint256 sharesRepaid)",
|
|
111
|
+
// Authorization
|
|
112
|
+
"function setAuthorization(address authorized, bool newIsAuthorized)",
|
|
113
|
+
"function isAuthorized(address authorizer, address authorized) view returns (bool)",
|
|
114
|
+
// Views
|
|
115
|
+
"function position(bytes32 id, address user) view returns (uint256 supplyShares, uint128 borrowShares, uint128 collateral)",
|
|
116
|
+
"function market(bytes32 id) view returns (uint128 totalSupplyAssets, uint128 totalSupplyShares, uint128 totalBorrowAssets, uint128 totalBorrowShares, uint128 lastUpdate, uint128 fee)",
|
|
117
|
+
"function idToMarketParams(bytes32 id) view returns (tuple(address loanToken, address collateralToken, address oracle, address irm, uint256 lltv))"
|
|
118
|
+
];
|
|
119
|
+
ERC20_ABI = [
|
|
120
|
+
"function balanceOf(address account) view returns (uint256)",
|
|
121
|
+
"function allowance(address owner, address spender) view returns (uint256)",
|
|
122
|
+
"function approve(address spender, uint256 amount) returns (bool)",
|
|
123
|
+
"function transfer(address to, uint256 amount) returns (bool)",
|
|
124
|
+
"function transferFrom(address from, address to, uint256 amount) returns (bool)",
|
|
125
|
+
"function decimals() view returns (uint8)",
|
|
126
|
+
"function symbol() view returns (string)",
|
|
127
|
+
"function name() view returns (string)"
|
|
128
|
+
];
|
|
129
|
+
ENTRYPOINT_V07_ABI = [
|
|
130
|
+
"function handleOps(tuple(address sender, uint256 nonce, bytes initCode, bytes callData, bytes32 accountGasLimits, uint256 preVerificationGas, bytes32 gasFees, bytes paymasterAndData, bytes signature)[] ops, address payable beneficiary)",
|
|
131
|
+
"function getUserOpHash(tuple(address sender, uint256 nonce, bytes initCode, bytes callData, bytes32 accountGasLimits, uint256 preVerificationGas, bytes32 gasFees, bytes paymasterAndData, bytes signature) userOp) view returns (bytes32)",
|
|
132
|
+
"function getNonce(address sender, uint192 key) view returns (uint256 nonce)",
|
|
133
|
+
"function balanceOf(address account) view returns (uint256)",
|
|
134
|
+
"function depositTo(address account) payable",
|
|
135
|
+
"event UserOperationEvent(bytes32 indexed userOpHash, address indexed sender, address indexed paymaster, uint256 nonce, bool success, uint256 actualGasCost, uint256 actualGasUsed)"
|
|
136
|
+
];
|
|
137
|
+
SAFE7579_ACCOUNT_ABI = [
|
|
138
|
+
// ERC-7579 execution (called via UserOp through Safe7579 fallback)
|
|
139
|
+
"function execute(bytes32 mode, bytes executionCalldata) payable",
|
|
140
|
+
"function executeFromExecutor(bytes32 mode, bytes executionCalldata) payable returns (bytes[])",
|
|
141
|
+
// ERC-7579 module queries
|
|
142
|
+
"function isModuleInstalled(uint256 moduleTypeId, address module, bytes additionalContext) view returns (bool)",
|
|
143
|
+
// EIP-1271
|
|
144
|
+
"function isValidSignature(bytes32 hash, bytes signature) view returns (bytes4)"
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// src/utils/config.ts
|
|
150
|
+
function getDefaultConfig(chainId) {
|
|
151
|
+
return {
|
|
152
|
+
chainId,
|
|
153
|
+
rpcUrl: RPC_URLS[chainId],
|
|
154
|
+
contracts: CONTRACT_ADDRESSES[chainId],
|
|
155
|
+
scoringEndpoint: SCORING_ENDPOINTS[chainId]
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
var ZERO, ENTRYPOINT_V07, ERC8004_IDENTITY_REGISTRY, ERC8004_IDENTITY_REGISTRY_TESTNET, MORPHO_BLUE, SAFE_SINGLETON, SAFE_PROXY_FACTORY, SAFE7579, CONTRACT_ADDRESSES, RPC_URLS, SCORING_ENDPOINTS;
|
|
159
|
+
var init_config = __esm({
|
|
160
|
+
"src/utils/config.ts"() {
|
|
161
|
+
"use strict";
|
|
162
|
+
init_types();
|
|
163
|
+
ZERO = "0x0000000000000000000000000000000000000000";
|
|
164
|
+
ENTRYPOINT_V07 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";
|
|
165
|
+
ERC8004_IDENTITY_REGISTRY = "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432";
|
|
166
|
+
ERC8004_IDENTITY_REGISTRY_TESTNET = "0x8004A818BFB912233c491871b3d84c89A494BD9e";
|
|
167
|
+
MORPHO_BLUE = "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb";
|
|
168
|
+
SAFE_SINGLETON = "0x41675C099F32341bf84BFc5382aF534df5C7461a";
|
|
169
|
+
SAFE_PROXY_FACTORY = "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67";
|
|
170
|
+
SAFE7579 = "0x7579EE8307284F293B1927136486880611F20002";
|
|
171
|
+
CONTRACT_ADDRESSES = {
|
|
172
|
+
[1 /* Ethereum */]: {
|
|
173
|
+
safeSingleton: SAFE_SINGLETON,
|
|
174
|
+
safeProxyFactory: SAFE_PROXY_FACTORY,
|
|
175
|
+
safe7579: SAFE7579,
|
|
176
|
+
entryPoint: ENTRYPOINT_V07,
|
|
177
|
+
safeAgentFactory: ZERO,
|
|
178
|
+
safe7579Bootstrap: ZERO,
|
|
179
|
+
erc8004ValidationModule: ZERO,
|
|
180
|
+
hookMultiplexer: ZERO,
|
|
181
|
+
validationRegistry: ZERO,
|
|
182
|
+
agentReputation: ZERO,
|
|
183
|
+
timelockController: ZERO,
|
|
184
|
+
usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
185
|
+
identityRegistry: ERC8004_IDENTITY_REGISTRY,
|
|
186
|
+
morphoBlue: MORPHO_BLUE
|
|
187
|
+
},
|
|
188
|
+
[8453 /* Base */]: {
|
|
189
|
+
safeSingleton: SAFE_SINGLETON,
|
|
190
|
+
safeProxyFactory: SAFE_PROXY_FACTORY,
|
|
191
|
+
safe7579: SAFE7579,
|
|
192
|
+
entryPoint: ENTRYPOINT_V07,
|
|
193
|
+
safeAgentFactory: "0xB0A88ffe28491E793F7706829278f20d724947d1",
|
|
194
|
+
safe7579Bootstrap: "0x72A636bc23B2644138489c3bBE3B05a0a7184b33",
|
|
195
|
+
erc8004ValidationModule: "0x49e27A6B4d012B87271897b51d0296ABcFCb0BBd",
|
|
196
|
+
hookMultiplexer: "0x12c77f17F91f06a11C2C34C618ce9d78f9a34541",
|
|
197
|
+
validationRegistry: "0x88E21e8883c093E4c8d0d0cE68f1c93Cf6190f51",
|
|
198
|
+
agentReputation: "0x4C2d42cbD35f6541f0902499CFEC27C1Cf5683E3",
|
|
199
|
+
timelockController: "0x0517b4f73b61774C88A2B1c5745141315E831015",
|
|
200
|
+
usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
201
|
+
identityRegistry: ERC8004_IDENTITY_REGISTRY,
|
|
202
|
+
morphoBlue: MORPHO_BLUE
|
|
203
|
+
},
|
|
204
|
+
[84532 /* BaseSepolia */]: {
|
|
205
|
+
safeSingleton: SAFE_SINGLETON,
|
|
206
|
+
safeProxyFactory: SAFE_PROXY_FACTORY,
|
|
207
|
+
safe7579: SAFE7579,
|
|
208
|
+
entryPoint: ENTRYPOINT_V07,
|
|
209
|
+
safeAgentFactory: ZERO,
|
|
210
|
+
safe7579Bootstrap: ZERO,
|
|
211
|
+
erc8004ValidationModule: ZERO,
|
|
212
|
+
hookMultiplexer: ZERO,
|
|
213
|
+
validationRegistry: ZERO,
|
|
214
|
+
agentReputation: ZERO,
|
|
215
|
+
timelockController: ZERO,
|
|
216
|
+
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
217
|
+
identityRegistry: ERC8004_IDENTITY_REGISTRY_TESTNET,
|
|
218
|
+
morphoBlue: ZERO
|
|
219
|
+
},
|
|
220
|
+
[11155111 /* Sepolia */]: {
|
|
221
|
+
safeSingleton: SAFE_SINGLETON,
|
|
222
|
+
safeProxyFactory: SAFE_PROXY_FACTORY,
|
|
223
|
+
safe7579: SAFE7579,
|
|
224
|
+
entryPoint: ENTRYPOINT_V07,
|
|
225
|
+
safeAgentFactory: ZERO,
|
|
226
|
+
safe7579Bootstrap: ZERO,
|
|
227
|
+
erc8004ValidationModule: ZERO,
|
|
228
|
+
hookMultiplexer: ZERO,
|
|
229
|
+
validationRegistry: ZERO,
|
|
230
|
+
agentReputation: ZERO,
|
|
231
|
+
timelockController: ZERO,
|
|
232
|
+
usdc: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
|
|
233
|
+
identityRegistry: ERC8004_IDENTITY_REGISTRY_TESTNET,
|
|
234
|
+
morphoBlue: ZERO
|
|
235
|
+
},
|
|
236
|
+
[31337 /* Hardhat */]: {
|
|
237
|
+
safeSingleton: SAFE_SINGLETON,
|
|
238
|
+
safeProxyFactory: SAFE_PROXY_FACTORY,
|
|
239
|
+
safe7579: SAFE7579,
|
|
240
|
+
entryPoint: ENTRYPOINT_V07,
|
|
241
|
+
safeAgentFactory: ZERO,
|
|
242
|
+
safe7579Bootstrap: ZERO,
|
|
243
|
+
erc8004ValidationModule: ZERO,
|
|
244
|
+
hookMultiplexer: ZERO,
|
|
245
|
+
validationRegistry: ZERO,
|
|
246
|
+
agentReputation: ZERO,
|
|
247
|
+
timelockController: ZERO,
|
|
248
|
+
usdc: "0x56d4d6aEe0278c5Df2FA23Ecb32eC146C9446FDf",
|
|
249
|
+
identityRegistry: ERC8004_IDENTITY_REGISTRY,
|
|
250
|
+
morphoBlue: ZERO
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
RPC_URLS = {
|
|
254
|
+
[1 /* Ethereum */]: "https://ethereum-rpc.publicnode.com",
|
|
255
|
+
[8453 /* Base */]: "https://base-rpc.publicnode.com",
|
|
256
|
+
[84532 /* BaseSepolia */]: "https://sepolia.base.org",
|
|
257
|
+
[11155111 /* Sepolia */]: "https://rpc.sepolia.org",
|
|
258
|
+
[31337 /* Hardhat */]: "http://127.0.0.1:8545"
|
|
259
|
+
};
|
|
260
|
+
SCORING_ENDPOINTS = {
|
|
261
|
+
[1 /* Ethereum */]: "https://scoring.agether.ai/v1",
|
|
262
|
+
[8453 /* Base */]: "http://95.179.189.214:3001",
|
|
263
|
+
[84532 /* BaseSepolia */]: "http://95.179.189.214:3001",
|
|
264
|
+
[11155111 /* Sepolia */]: "https://scoring-testnet.agether.ai/v1",
|
|
265
|
+
[31337 /* Hardhat */]: "http://127.0.0.1:3001"
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// src/clients/MorphoClient.ts
|
|
271
|
+
var MorphoClient_exports = {};
|
|
272
|
+
__export(MorphoClient_exports, {
|
|
273
|
+
MorphoClient: () => MorphoClient
|
|
274
|
+
});
|
|
275
|
+
var import_ethers, import_axios, BASE_COLLATERALS, MORPHO_API_URL, MODE_SINGLE, MODE_BATCH, morphoIface, erc20Iface, MorphoClient;
|
|
276
|
+
var init_MorphoClient = __esm({
|
|
277
|
+
"src/clients/MorphoClient.ts"() {
|
|
278
|
+
"use strict";
|
|
279
|
+
import_ethers = require("ethers");
|
|
280
|
+
import_axios = __toESM(require("axios"));
|
|
281
|
+
init_types();
|
|
282
|
+
init_abis();
|
|
283
|
+
init_config();
|
|
284
|
+
BASE_COLLATERALS = {
|
|
285
|
+
WETH: { address: "0x4200000000000000000000000000000000000006", symbol: "WETH", decimals: 18 },
|
|
286
|
+
wstETH: { address: "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", symbol: "wstETH", decimals: 18 },
|
|
287
|
+
cbETH: { address: "0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22", symbol: "cbETH", decimals: 18 }
|
|
288
|
+
};
|
|
289
|
+
MORPHO_API_URL = "https://api.morpho.org/graphql";
|
|
290
|
+
MODE_SINGLE = "0x0000000000000000000000000000000000000000000000000000000000000000";
|
|
291
|
+
MODE_BATCH = "0x0100000000000000000000000000000000000000000000000000000000000000";
|
|
292
|
+
morphoIface = new import_ethers.ethers.Interface(MORPHO_BLUE_ABI);
|
|
293
|
+
erc20Iface = new import_ethers.ethers.Interface(ERC20_ABI);
|
|
294
|
+
MorphoClient = class {
|
|
295
|
+
constructor(config) {
|
|
296
|
+
this._marketCache = /* @__PURE__ */ new Map();
|
|
297
|
+
this._discoveredAt = 0;
|
|
298
|
+
const chainId = config.chainId ?? 8453 /* Base */;
|
|
299
|
+
const defaultCfg = getDefaultConfig(chainId);
|
|
300
|
+
this.config = defaultCfg;
|
|
301
|
+
this.agentId = config.agentId;
|
|
302
|
+
this._rpcUrl = config.rpcUrl || defaultCfg.rpcUrl;
|
|
303
|
+
if ("signer" in config && config.signer) {
|
|
304
|
+
this._useExternalSigner = true;
|
|
305
|
+
const signerProvider = config.signer.provider;
|
|
306
|
+
if (signerProvider) {
|
|
307
|
+
this.provider = signerProvider;
|
|
308
|
+
this._signer = config.signer;
|
|
309
|
+
} else {
|
|
310
|
+
this.provider = new import_ethers.ethers.JsonRpcProvider(this._rpcUrl);
|
|
311
|
+
this._signer = config.signer.connect(this.provider);
|
|
312
|
+
}
|
|
313
|
+
if ("address" in config.signer && typeof config.signer.address === "string") {
|
|
314
|
+
this._eoaAddress = config.signer.address;
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
this._privateKey = config.privateKey;
|
|
318
|
+
this._useExternalSigner = false;
|
|
319
|
+
this.provider = new import_ethers.ethers.JsonRpcProvider(this._rpcUrl);
|
|
320
|
+
const wallet = new import_ethers.ethers.Wallet(this._privateKey, this.provider);
|
|
321
|
+
this._signer = wallet;
|
|
322
|
+
this._eoaAddress = wallet.address;
|
|
323
|
+
}
|
|
324
|
+
const addrs = { ...defaultCfg.contracts, ...config.contracts };
|
|
325
|
+
this.safeAgentFactory = new import_ethers.Contract(addrs.safeAgentFactory, ACCOUNT_FACTORY_ABI, this._signer);
|
|
326
|
+
this.morphoBlue = new import_ethers.Contract(addrs.morphoBlue, MORPHO_BLUE_ABI, this.provider);
|
|
327
|
+
this.agentReputation = new import_ethers.Contract(addrs.agentReputation, AGENT_REPUTATION_ABI, this._signer);
|
|
328
|
+
this.identityRegistry = new import_ethers.Contract(addrs.identityRegistry, IDENTITY_REGISTRY_ABI, this._signer);
|
|
329
|
+
this.entryPoint = new import_ethers.Contract(addrs.entryPoint, ENTRYPOINT_V07_ABI, this._signer);
|
|
330
|
+
this.validationModule = new import_ethers.Contract(addrs.erc8004ValidationModule, ERC8004_VALIDATION_MODULE_ABI, this.provider);
|
|
331
|
+
}
|
|
332
|
+
// ════════════════════════════════════════════════════════
|
|
333
|
+
// KYA Gate Check
|
|
334
|
+
// ════════════════════════════════════════════════════════
|
|
335
|
+
/**
|
|
336
|
+
* Check whether the KYA (Know Your Agent) code verification gate is active.
|
|
337
|
+
* Reads the ERC8004ValidationModule's validationRegistry — when set to
|
|
338
|
+
* a non-zero address, the module enforces KYA code approval.
|
|
339
|
+
*/
|
|
340
|
+
async isKyaRequired() {
|
|
341
|
+
try {
|
|
342
|
+
const registryAddr = await this.validationModule.validationRegistry();
|
|
343
|
+
return registryAddr !== import_ethers.ethers.ZeroAddress;
|
|
344
|
+
} catch {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// ════════════════════════════════════════════════════════
|
|
349
|
+
// Account Management
|
|
350
|
+
// ════════════════════════════════════════════════════════
|
|
351
|
+
/** Resolve the AgentAccount address (cached, with retry for flaky RPCs). */
|
|
352
|
+
async getAccountAddress() {
|
|
353
|
+
if (this._accountAddress) return this._accountAddress;
|
|
354
|
+
if (!this.agentId) throw new AgetherError("agentId not set", "NO_AGENT_ID");
|
|
355
|
+
const MAX_RETRIES = 3;
|
|
356
|
+
let lastErr;
|
|
357
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
358
|
+
try {
|
|
359
|
+
const addr = await this.safeAgentFactory.getAccount(BigInt(this.agentId));
|
|
360
|
+
if (addr === import_ethers.ethers.ZeroAddress) {
|
|
361
|
+
throw new AgetherError("No AgentAccount found. Call register() first.", "NO_ACCOUNT");
|
|
362
|
+
}
|
|
363
|
+
this._accountAddress = addr;
|
|
364
|
+
return addr;
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (err instanceof AgetherError) throw err;
|
|
367
|
+
lastErr = err;
|
|
368
|
+
if (attempt < MAX_RETRIES) {
|
|
369
|
+
await new Promise((r) => setTimeout(r, 500 * attempt));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
throw lastErr;
|
|
374
|
+
}
|
|
375
|
+
getAgentId() {
|
|
376
|
+
if (!this.agentId) throw new AgetherError("agentId not set", "NO_AGENT_ID");
|
|
377
|
+
return this.agentId;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Get the EOA wallet address (synchronous, best-effort).
|
|
381
|
+
*
|
|
382
|
+
* For the `privateKey` path this always works. For the `signer` path
|
|
383
|
+
* it works if the signer exposes `.address` synchronously (e.g. ethers.Wallet).
|
|
384
|
+
* If the address has not been resolved yet, throws — call `getSignerAddress()` first.
|
|
385
|
+
*/
|
|
386
|
+
getWalletAddress() {
|
|
387
|
+
if (this._eoaAddress) return this._eoaAddress;
|
|
388
|
+
const signer = this._signer;
|
|
389
|
+
if (signer.address && typeof signer.address === "string") {
|
|
390
|
+
const addr = signer.address;
|
|
391
|
+
this._eoaAddress = addr;
|
|
392
|
+
return addr;
|
|
393
|
+
}
|
|
394
|
+
throw new AgetherError(
|
|
395
|
+
"EOA address not yet resolved. Call getSignerAddress() (async) first, or use a signer that exposes .address synchronously.",
|
|
396
|
+
"ADDRESS_NOT_RESOLVED"
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Resolve the EOA signer address (async, works with all signer types).
|
|
401
|
+
* Result is cached after the first call.
|
|
402
|
+
*/
|
|
403
|
+
async getSignerAddress() {
|
|
404
|
+
if (!this._eoaAddress) {
|
|
405
|
+
this._eoaAddress = await this._signer.getAddress();
|
|
406
|
+
}
|
|
407
|
+
return this._eoaAddress;
|
|
408
|
+
}
|
|
409
|
+
/** Mint a new ERC-8004 identity and return the agentId. */
|
|
410
|
+
async _mintNewIdentity() {
|
|
411
|
+
const regTx = await this.identityRegistry.register();
|
|
412
|
+
const regReceipt = await regTx.wait();
|
|
413
|
+
this._refreshSigner();
|
|
414
|
+
let agentId = 0n;
|
|
415
|
+
for (const log of regReceipt.logs) {
|
|
416
|
+
try {
|
|
417
|
+
const parsed = this.identityRegistry.interface.parseLog({ topics: log.topics, data: log.data });
|
|
418
|
+
if (parsed?.name === "Transfer") {
|
|
419
|
+
agentId = parsed.args[2];
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
} catch (e) {
|
|
423
|
+
console.warn("[agether] parseLog skip:", e instanceof Error ? e.message : e);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (agentId === 0n) throw new AgetherError("Failed to parse agentId from registration", "PARSE_ERROR");
|
|
428
|
+
return agentId;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Register: create ERC-8004 identity + AgentAccount in one flow.
|
|
432
|
+
* If already registered, returns existing state.
|
|
433
|
+
*/
|
|
434
|
+
async register(_name) {
|
|
435
|
+
const eoaAddr = await this.getSignerAddress();
|
|
436
|
+
if (this.agentId) {
|
|
437
|
+
const exists = await this.safeAgentFactory.accountExists(BigInt(this.agentId));
|
|
438
|
+
if (exists) {
|
|
439
|
+
const acct = await this.safeAgentFactory.getAccount(BigInt(this.agentId));
|
|
440
|
+
this._accountAddress = acct;
|
|
441
|
+
const kyaRequired2 = await this.isKyaRequired();
|
|
442
|
+
return { agentId: this.agentId, address: eoaAddr, agentAccount: acct, alreadyRegistered: true, kyaRequired: kyaRequired2 };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
let agentId;
|
|
446
|
+
if (this.agentId) {
|
|
447
|
+
const balance = await this.identityRegistry.balanceOf(eoaAddr);
|
|
448
|
+
if (balance > 0n) {
|
|
449
|
+
agentId = BigInt(this.agentId);
|
|
450
|
+
} else {
|
|
451
|
+
agentId = await this._mintNewIdentity();
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
agentId = await this._mintNewIdentity();
|
|
455
|
+
}
|
|
456
|
+
this.agentId = agentId.toString();
|
|
457
|
+
const acctExists = await this.safeAgentFactory.accountExists(agentId);
|
|
458
|
+
let txHash;
|
|
459
|
+
if (!acctExists) {
|
|
460
|
+
const tx = await this.safeAgentFactory.createAccount(agentId);
|
|
461
|
+
const receipt = await tx.wait();
|
|
462
|
+
this._refreshSigner();
|
|
463
|
+
txHash = receipt.hash;
|
|
464
|
+
}
|
|
465
|
+
const acctAddr = await this.safeAgentFactory.getAccount(agentId);
|
|
466
|
+
this._accountAddress = acctAddr;
|
|
467
|
+
const kyaRequired = await this.isKyaRequired();
|
|
468
|
+
return {
|
|
469
|
+
agentId: this.agentId,
|
|
470
|
+
address: eoaAddr,
|
|
471
|
+
agentAccount: acctAddr,
|
|
472
|
+
alreadyRegistered: acctExists,
|
|
473
|
+
kyaRequired,
|
|
474
|
+
tx: txHash
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
/** Get ETH / USDC / collateral balances for EOA and AgentAccount. */
|
|
478
|
+
async getBalances() {
|
|
479
|
+
const eoaAddr = await this.getSignerAddress();
|
|
480
|
+
const usdc = new import_ethers.Contract(this.config.contracts.usdc, ERC20_ABI, this.provider);
|
|
481
|
+
const ethBal = await this.provider.getBalance(eoaAddr);
|
|
482
|
+
const usdcBal = await usdc.balanceOf(eoaAddr);
|
|
483
|
+
const eoaCollateral = {};
|
|
484
|
+
for (const [symbol, info] of Object.entries(BASE_COLLATERALS)) {
|
|
485
|
+
try {
|
|
486
|
+
const token = new import_ethers.Contract(info.address, ERC20_ABI, this.provider);
|
|
487
|
+
const bal = await token.balanceOf(eoaAddr);
|
|
488
|
+
eoaCollateral[symbol] = import_ethers.ethers.formatUnits(bal, info.decimals);
|
|
489
|
+
} catch (e) {
|
|
490
|
+
console.warn(`[agether] EOA collateral fetch failed for ${symbol}:`, e instanceof Error ? e.message : e);
|
|
491
|
+
eoaCollateral[symbol] = "0";
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const result = {
|
|
495
|
+
agentId: this.agentId || "?",
|
|
496
|
+
address: eoaAddr,
|
|
497
|
+
eth: import_ethers.ethers.formatEther(ethBal),
|
|
498
|
+
usdc: import_ethers.ethers.formatUnits(usdcBal, 6),
|
|
499
|
+
collateral: eoaCollateral
|
|
500
|
+
};
|
|
501
|
+
try {
|
|
502
|
+
const acctAddr = await this.getAccountAddress();
|
|
503
|
+
const acctEth = await this.provider.getBalance(acctAddr);
|
|
504
|
+
const acctUsdc = await usdc.balanceOf(acctAddr);
|
|
505
|
+
const acctCollateral = {};
|
|
506
|
+
for (const [symbol, info] of Object.entries(BASE_COLLATERALS)) {
|
|
507
|
+
try {
|
|
508
|
+
const token = new import_ethers.Contract(info.address, ERC20_ABI, this.provider);
|
|
509
|
+
const bal = await token.balanceOf(acctAddr);
|
|
510
|
+
acctCollateral[symbol] = import_ethers.ethers.formatUnits(bal, info.decimals);
|
|
511
|
+
} catch (e) {
|
|
512
|
+
console.warn(`[agether] AgentAccount collateral fetch failed for ${symbol}:`, e instanceof Error ? e.message : e);
|
|
513
|
+
acctCollateral[symbol] = "0";
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
result.agentAccount = {
|
|
517
|
+
address: acctAddr,
|
|
518
|
+
eth: import_ethers.ethers.formatEther(acctEth),
|
|
519
|
+
usdc: import_ethers.ethers.formatUnits(acctUsdc, 6),
|
|
520
|
+
collateral: acctCollateral
|
|
521
|
+
};
|
|
522
|
+
} catch (err) {
|
|
523
|
+
if (err instanceof AgetherError && err.code === "NO_ACCOUNT") {
|
|
524
|
+
} else {
|
|
525
|
+
console.warn("[agether] getBalances: failed to fetch AgentAccount data:", err.message ?? err);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return result;
|
|
529
|
+
}
|
|
530
|
+
/** Transfer USDC from EOA to AgentAccount. */
|
|
531
|
+
async fundAccount(usdcAmount) {
|
|
532
|
+
const acctAddr = await this.getAccountAddress();
|
|
533
|
+
const usdc = new import_ethers.Contract(this.config.contracts.usdc, ERC20_ABI, this._signer);
|
|
534
|
+
const amount = import_ethers.ethers.parseUnits(usdcAmount, 6);
|
|
535
|
+
const tx = await usdc.transfer(acctAddr, amount);
|
|
536
|
+
const receipt = await tx.wait();
|
|
537
|
+
this._refreshSigner();
|
|
538
|
+
return { tx: receipt.hash, amount: usdcAmount, agentAccount: acctAddr };
|
|
539
|
+
}
|
|
540
|
+
// ════════════════════════════════════════════════════════
|
|
541
|
+
// Market Discovery (Morpho GraphQL API)
|
|
542
|
+
// ════════════════════════════════════════════════════════
|
|
543
|
+
/**
|
|
544
|
+
* Fetch USDC borrow markets on Base from Morpho API.
|
|
545
|
+
* Caches results for 5 minutes.
|
|
546
|
+
*/
|
|
547
|
+
async getMarkets(forceRefresh = false) {
|
|
548
|
+
if (!forceRefresh && this._discoveredMarkets && Date.now() - this._discoveredAt < 3e5) {
|
|
549
|
+
return this._discoveredMarkets;
|
|
550
|
+
}
|
|
551
|
+
const chainId = this.config.chainId;
|
|
552
|
+
const usdcAddr = this.config.contracts.usdc.toLowerCase();
|
|
553
|
+
const query = `{
|
|
554
|
+
markets(
|
|
555
|
+
first: 50
|
|
556
|
+
orderBy: SupplyAssetsUsd
|
|
557
|
+
orderDirection: Desc
|
|
558
|
+
where: { chainId_in: [${chainId}], loanAssetAddress_in: ["${usdcAddr}"] }
|
|
559
|
+
) {
|
|
560
|
+
items {
|
|
561
|
+
uniqueKey
|
|
562
|
+
lltv
|
|
563
|
+
oracleAddress
|
|
564
|
+
irmAddress
|
|
565
|
+
loanAsset { address symbol decimals }
|
|
566
|
+
collateralAsset { address symbol decimals }
|
|
567
|
+
state {
|
|
568
|
+
borrowAssets
|
|
569
|
+
supplyAssets
|
|
570
|
+
utilization
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}`;
|
|
575
|
+
try {
|
|
576
|
+
const resp = await import_axios.default.post(MORPHO_API_URL, { query }, { timeout: 1e4 });
|
|
577
|
+
const items = resp.data?.data?.markets?.items ?? [];
|
|
578
|
+
this._discoveredMarkets = items.map((m) => ({
|
|
579
|
+
uniqueKey: m.uniqueKey,
|
|
580
|
+
loanAsset: m.loanAsset,
|
|
581
|
+
collateralAsset: m.collateralAsset ?? { address: import_ethers.ethers.ZeroAddress, symbol: "N/A", decimals: 0 },
|
|
582
|
+
oracle: m.oracleAddress,
|
|
583
|
+
irm: m.irmAddress,
|
|
584
|
+
lltv: BigInt(m.lltv),
|
|
585
|
+
totalSupplyAssets: BigInt(m.state?.supplyAssets ?? "0"),
|
|
586
|
+
totalBorrowAssets: BigInt(m.state?.borrowAssets ?? "0"),
|
|
587
|
+
utilization: m.state?.utilization ? Number(m.state.utilization) : 0
|
|
588
|
+
}));
|
|
589
|
+
this._discoveredAt = Date.now();
|
|
590
|
+
for (const mi of this._discoveredMarkets) {
|
|
591
|
+
if (mi.collateralAsset.address !== import_ethers.ethers.ZeroAddress) {
|
|
592
|
+
this._marketCache.set(mi.collateralAsset.address.toLowerCase(), {
|
|
593
|
+
loanToken: mi.loanAsset.address,
|
|
594
|
+
collateralToken: mi.collateralAsset.address,
|
|
595
|
+
oracle: mi.oracle,
|
|
596
|
+
irm: mi.irm,
|
|
597
|
+
lltv: mi.lltv
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return this._discoveredMarkets;
|
|
602
|
+
} catch (e) {
|
|
603
|
+
console.warn("[agether] getMarkets failed, using cache:", e instanceof Error ? e.message : e);
|
|
604
|
+
return this._discoveredMarkets ?? [];
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Get MarketParams for a collateral token.
|
|
609
|
+
* Tries cache → API → on-chain idToMarketParams.
|
|
610
|
+
*/
|
|
611
|
+
async findMarketForCollateral(collateralSymbolOrAddress) {
|
|
612
|
+
const colInfo = BASE_COLLATERALS[collateralSymbolOrAddress];
|
|
613
|
+
const colAddr = (colInfo?.address ?? collateralSymbolOrAddress).toLowerCase();
|
|
614
|
+
const cached = this._marketCache.get(colAddr);
|
|
615
|
+
if (cached) return cached;
|
|
616
|
+
await this.getMarkets();
|
|
617
|
+
const fromApi = this._marketCache.get(colAddr);
|
|
618
|
+
if (fromApi) return fromApi;
|
|
619
|
+
throw new AgetherError(
|
|
620
|
+
`No Morpho market found for collateral ${collateralSymbolOrAddress}`,
|
|
621
|
+
"MARKET_NOT_FOUND"
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
/** Read MarketParams on-chain by market ID (bytes32). */
|
|
625
|
+
async getMarketParams(marketId) {
|
|
626
|
+
const result = await this.morphoBlue.idToMarketParams(marketId);
|
|
627
|
+
return {
|
|
628
|
+
loanToken: result.loanToken,
|
|
629
|
+
collateralToken: result.collateralToken,
|
|
630
|
+
oracle: result.oracle,
|
|
631
|
+
irm: result.irm,
|
|
632
|
+
lltv: result.lltv
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
// ════════════════════════════════════════════════════════
|
|
636
|
+
// Position Reads
|
|
637
|
+
// ════════════════════════════════════════════════════════
|
|
638
|
+
/** Read on-chain position for a specific market. */
|
|
639
|
+
async getPosition(marketId) {
|
|
640
|
+
const acctAddr = await this.getAccountAddress();
|
|
641
|
+
const pos = await this.morphoBlue.position(marketId, acctAddr);
|
|
642
|
+
return {
|
|
643
|
+
supplyShares: pos.supplyShares,
|
|
644
|
+
borrowShares: pos.borrowShares,
|
|
645
|
+
collateral: pos.collateral
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Full status: positions across all discovered markets.
|
|
650
|
+
*/
|
|
651
|
+
async getStatus() {
|
|
652
|
+
const acctAddr = await this.getAccountAddress();
|
|
653
|
+
const markets = await this.getMarkets();
|
|
654
|
+
const positions = [];
|
|
655
|
+
let totalDebt = 0n;
|
|
656
|
+
for (const m of markets) {
|
|
657
|
+
if (!m.collateralAsset || m.collateralAsset.address === import_ethers.ethers.ZeroAddress) continue;
|
|
658
|
+
try {
|
|
659
|
+
const pos = await this.morphoBlue.position(m.uniqueKey, acctAddr);
|
|
660
|
+
if (pos.collateral === 0n && pos.borrowShares === 0n && pos.supplyShares === 0n) continue;
|
|
661
|
+
let debt = 0n;
|
|
662
|
+
if (pos.borrowShares > 0n) {
|
|
663
|
+
try {
|
|
664
|
+
const mkt = await this.morphoBlue.market(m.uniqueKey);
|
|
665
|
+
const totalBorrowShares = BigInt(mkt.totalBorrowShares);
|
|
666
|
+
const totalBorrowAssets = BigInt(mkt.totalBorrowAssets);
|
|
667
|
+
debt = totalBorrowShares > 0n ? (BigInt(pos.borrowShares) * totalBorrowAssets + totalBorrowShares - 1n) / totalBorrowShares : 0n;
|
|
668
|
+
totalDebt += debt;
|
|
669
|
+
} catch (e) {
|
|
670
|
+
console.warn(`[agether] debt calc failed for market ${m.uniqueKey}:`, e instanceof Error ? e.message : e);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
positions.push({
|
|
674
|
+
marketId: m.uniqueKey,
|
|
675
|
+
collateralToken: m.collateralAsset.symbol,
|
|
676
|
+
collateral: import_ethers.ethers.formatUnits(pos.collateral, m.collateralAsset.decimals),
|
|
677
|
+
borrowShares: pos.borrowShares.toString(),
|
|
678
|
+
supplyShares: pos.supplyShares.toString(),
|
|
679
|
+
debt: import_ethers.ethers.formatUnits(debt, 6)
|
|
680
|
+
});
|
|
681
|
+
} catch (e) {
|
|
682
|
+
console.warn(`[agether] position read failed for market:`, e instanceof Error ? e.message : e);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
agentId: this.agentId || "?",
|
|
688
|
+
agentAccount: acctAddr,
|
|
689
|
+
totalDebt: import_ethers.ethers.formatUnits(totalDebt, 6),
|
|
690
|
+
positions
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
// ════════════════════════════════════════════════════════
|
|
694
|
+
// Balance & Borrowing Capacity
|
|
695
|
+
// ════════════════════════════════════════════════════════
|
|
696
|
+
/**
|
|
697
|
+
* Get the USDC balance of the AgentAccount.
|
|
698
|
+
* @returns USDC balance in raw units (6 decimals)
|
|
699
|
+
*/
|
|
700
|
+
async getUsdcBalance() {
|
|
701
|
+
const acctAddr = await this.getAccountAddress();
|
|
702
|
+
const usdc = new import_ethers.Contract(this.config.contracts.usdc, ERC20_ABI, this.provider);
|
|
703
|
+
return usdc.balanceOf(acctAddr);
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Calculate the maximum additional USDC that can be borrowed
|
|
707
|
+
* given the agent's current collateral and debt across all markets.
|
|
708
|
+
*
|
|
709
|
+
* For each market with collateral deposited:
|
|
710
|
+
* maxBorrow = (collateralValue * LLTV) - currentDebt
|
|
711
|
+
*
|
|
712
|
+
* Uses the Morpho oracle to price collateral → loan token.
|
|
713
|
+
*
|
|
714
|
+
* @returns Maximum additional USDC borrowable (6 decimals)
|
|
715
|
+
*/
|
|
716
|
+
async getMaxBorrowable() {
|
|
717
|
+
const acctAddr = await this.getAccountAddress();
|
|
718
|
+
const markets = await this.getMarkets();
|
|
719
|
+
let totalAdditional = 0n;
|
|
720
|
+
const byMarket = [];
|
|
721
|
+
for (const m of markets) {
|
|
722
|
+
if (!m.collateralAsset || m.collateralAsset.address === import_ethers.ethers.ZeroAddress) continue;
|
|
723
|
+
try {
|
|
724
|
+
const pos = await this.morphoBlue.position(m.uniqueKey, acctAddr);
|
|
725
|
+
if (pos.collateral === 0n) continue;
|
|
726
|
+
const mktState = await this.morphoBlue.market(m.uniqueKey);
|
|
727
|
+
const totalBorrowShares = BigInt(mktState.totalBorrowShares);
|
|
728
|
+
const totalBorrowAssets = BigInt(mktState.totalBorrowAssets);
|
|
729
|
+
const currentDebt = totalBorrowShares > 0n ? (BigInt(pos.borrowShares) * totalBorrowAssets + totalBorrowShares - 1n) / totalBorrowShares : 0n;
|
|
730
|
+
let collateralValueInLoan;
|
|
731
|
+
try {
|
|
732
|
+
const oracleContract = new import_ethers.Contract(m.oracle, [
|
|
733
|
+
"function price() view returns (uint256)"
|
|
734
|
+
], this.provider);
|
|
735
|
+
const oraclePrice = await oracleContract.price();
|
|
736
|
+
const ORACLE_PRICE_SCALE = 10n ** 36n;
|
|
737
|
+
collateralValueInLoan = BigInt(pos.collateral) * oraclePrice / ORACLE_PRICE_SCALE;
|
|
738
|
+
} catch (e) {
|
|
739
|
+
console.warn(`[agether] oracle price fetch failed:`, e instanceof Error ? e.message : e);
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const maxBorrowTotal = collateralValueInLoan * m.lltv / 10n ** 18n;
|
|
743
|
+
const maxAdditional = maxBorrowTotal > currentDebt ? maxBorrowTotal - currentDebt : 0n;
|
|
744
|
+
totalAdditional += maxAdditional;
|
|
745
|
+
byMarket.push({
|
|
746
|
+
collateralToken: m.collateralAsset.symbol,
|
|
747
|
+
maxAdditional,
|
|
748
|
+
currentDebt,
|
|
749
|
+
collateralValue: collateralValueInLoan
|
|
750
|
+
});
|
|
751
|
+
} catch (e) {
|
|
752
|
+
console.warn(`[agether] maxBorrow calc failed:`, e instanceof Error ? e.message : e);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return { total: totalAdditional, byMarket };
|
|
757
|
+
}
|
|
758
|
+
// ════════════════════════════════════════════════════════
|
|
759
|
+
// Market Rates & Yield Estimation
|
|
760
|
+
// ════════════════════════════════════════════════════════
|
|
761
|
+
/**
|
|
762
|
+
* Fetch current supply/borrow APY for a collateral market from Morpho GraphQL API.
|
|
763
|
+
*
|
|
764
|
+
* Note: On Morpho Blue, collateral does NOT earn yield directly. Supply APY
|
|
765
|
+
* is what lenders earn; borrow APY is what borrowers pay.
|
|
766
|
+
*/
|
|
767
|
+
async getMarketRates(collateralSymbolOrAddress) {
|
|
768
|
+
const chainId = this.config.chainId;
|
|
769
|
+
const usdcAddr = this.config.contracts.usdc.toLowerCase();
|
|
770
|
+
let collateralFilter = "";
|
|
771
|
+
if (collateralSymbolOrAddress) {
|
|
772
|
+
const colInfo = BASE_COLLATERALS[collateralSymbolOrAddress];
|
|
773
|
+
const colAddr = (colInfo?.address ?? collateralSymbolOrAddress).toLowerCase();
|
|
774
|
+
collateralFilter = `, collateralAssetAddress_in: ["${colAddr}"]`;
|
|
775
|
+
}
|
|
776
|
+
const query = `{
|
|
777
|
+
markets(
|
|
778
|
+
first: 50
|
|
779
|
+
orderBy: SupplyAssetsUsd
|
|
780
|
+
orderDirection: Desc
|
|
781
|
+
where: { chainId_in: [${chainId}], loanAssetAddress_in: ["${usdcAddr}"]${collateralFilter} }
|
|
782
|
+
) {
|
|
783
|
+
items {
|
|
784
|
+
uniqueKey
|
|
785
|
+
lltv
|
|
786
|
+
loanAsset { address symbol decimals }
|
|
787
|
+
collateralAsset { address symbol decimals }
|
|
788
|
+
state {
|
|
789
|
+
borrowAssets
|
|
790
|
+
supplyAssets
|
|
791
|
+
utilization
|
|
792
|
+
supplyApy
|
|
793
|
+
borrowApy
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}`;
|
|
798
|
+
try {
|
|
799
|
+
const resp = await import_axios.default.post(MORPHO_API_URL, { query }, { timeout: 1e4 });
|
|
800
|
+
const items = resp.data?.data?.markets?.items ?? [];
|
|
801
|
+
return items.filter((m) => m.collateralAsset?.address && m.collateralAsset.address !== import_ethers.ethers.ZeroAddress).map((m) => ({
|
|
802
|
+
collateralToken: m.collateralAsset.symbol,
|
|
803
|
+
loanToken: m.loanAsset.symbol,
|
|
804
|
+
supplyApy: m.state?.supplyApy ? Number(m.state.supplyApy) : 0,
|
|
805
|
+
borrowApy: m.state?.borrowApy ? Number(m.state.borrowApy) : 0,
|
|
806
|
+
utilization: m.state?.utilization ? Number(m.state.utilization) : 0,
|
|
807
|
+
totalSupplyUsd: m.state?.supplyAssets ? Number(m.state.supplyAssets) / 1e6 : 0,
|
|
808
|
+
totalBorrowUsd: m.state?.borrowAssets ? Number(m.state.borrowAssets) / 1e6 : 0,
|
|
809
|
+
lltv: `${(Number(m.lltv) / 1e16).toFixed(0)}%`,
|
|
810
|
+
marketId: m.uniqueKey
|
|
811
|
+
}));
|
|
812
|
+
} catch (e) {
|
|
813
|
+
console.warn("[agether] getMarketRates failed:", e instanceof Error ? e.message : e);
|
|
814
|
+
return [];
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Estimate theoretical yield for a given collateral amount over a period.
|
|
819
|
+
*
|
|
820
|
+
* ⚠️ IMPORTANT: On Morpho Blue, collateral does NOT earn yield directly.
|
|
821
|
+
* This estimates what the collateral WOULD earn if it were instead supplied
|
|
822
|
+
* as a lender (not used as collateral). This is a theoretical upper bound
|
|
823
|
+
* useful for setting spending caps.
|
|
824
|
+
*
|
|
825
|
+
* @param collateralSymbol - e.g. 'WETH'
|
|
826
|
+
* @param amount - collateral amount in human-readable (e.g. '1.5')
|
|
827
|
+
* @param periodDays - estimation period in days (default: 1)
|
|
828
|
+
* @param ethPriceUsd - ETH price in USD for value conversion (if not provided, uses oracle)
|
|
829
|
+
* @returns Estimated yield in USD for the period
|
|
830
|
+
*/
|
|
831
|
+
async getYieldEstimate(collateralSymbol, amount, periodDays = 1, ethPriceUsd) {
|
|
832
|
+
const colInfo = BASE_COLLATERALS[collateralSymbol];
|
|
833
|
+
if (!colInfo) throw new AgetherError(`Unknown collateral: ${collateralSymbol}`, "UNKNOWN_COLLATERAL");
|
|
834
|
+
const rates = await this.getMarketRates(collateralSymbol);
|
|
835
|
+
if (rates.length === 0) {
|
|
836
|
+
throw new AgetherError(`No market found for ${collateralSymbol}`, "MARKET_NOT_FOUND");
|
|
837
|
+
}
|
|
838
|
+
const market = rates[0];
|
|
839
|
+
const supplyApy = market.supplyApy;
|
|
840
|
+
let collateralValueUsd;
|
|
841
|
+
if (ethPriceUsd) {
|
|
842
|
+
collateralValueUsd = parseFloat(amount) * ethPriceUsd;
|
|
843
|
+
} else {
|
|
844
|
+
try {
|
|
845
|
+
const params = await this.findMarketForCollateral(collateralSymbol);
|
|
846
|
+
const oracleContract = new import_ethers.Contract(params.oracle, [
|
|
847
|
+
"function price() view returns (uint256)"
|
|
848
|
+
], this.provider);
|
|
849
|
+
const oraclePrice = await oracleContract.price();
|
|
850
|
+
const ORACLE_PRICE_SCALE = 10n ** 36n;
|
|
851
|
+
const amountWei = import_ethers.ethers.parseUnits(amount, colInfo.decimals);
|
|
852
|
+
const valueInUsdc = amountWei * oraclePrice / ORACLE_PRICE_SCALE;
|
|
853
|
+
collateralValueUsd = Number(valueInUsdc) / 1e6;
|
|
854
|
+
} catch (e) {
|
|
855
|
+
console.warn("[agether] oracle price fetch for yield estimation failed:", e instanceof Error ? e.message : e);
|
|
856
|
+
throw new AgetherError("Cannot determine collateral value. Provide ethPriceUsd.", "PRICE_UNAVAILABLE");
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const estimatedYieldUsd = collateralValueUsd * supplyApy * (periodDays / 365);
|
|
860
|
+
return {
|
|
861
|
+
collateralToken: collateralSymbol,
|
|
862
|
+
amount,
|
|
863
|
+
periodDays,
|
|
864
|
+
theoreticalSupplyApy: supplyApy,
|
|
865
|
+
estimatedYieldUsd,
|
|
866
|
+
collateralValueUsd,
|
|
867
|
+
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."
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
// ════════════════════════════════════════════════════════
|
|
871
|
+
// Lending Operations (all via AgentAccount.executeBatch)
|
|
872
|
+
// ════════════════════════════════════════════════════════
|
|
873
|
+
/**
|
|
874
|
+
* Deposit collateral into Morpho Blue.
|
|
875
|
+
*
|
|
876
|
+
* Flow:
|
|
877
|
+
* 1. EOA transfers collateral to AgentAccount
|
|
878
|
+
* 2. AgentAccount.executeBatch:
|
|
879
|
+
* [collateral.approve(MorphoBlue), Morpho.supplyCollateral(params)]
|
|
880
|
+
*/
|
|
881
|
+
async supplyCollateral(tokenSymbol, amount, marketParams) {
|
|
882
|
+
const acctAddr = await this.getAccountAddress();
|
|
883
|
+
const colInfo = BASE_COLLATERALS[tokenSymbol];
|
|
884
|
+
if (!colInfo) throw new AgetherError(`Unknown collateral: ${tokenSymbol}`, "UNKNOWN_COLLATERAL");
|
|
885
|
+
const params = marketParams ?? await this.findMarketForCollateral(tokenSymbol);
|
|
886
|
+
const weiAmount = import_ethers.ethers.parseUnits(amount, colInfo.decimals);
|
|
887
|
+
const morphoAddr = this.config.contracts.morphoBlue;
|
|
888
|
+
const colToken = new import_ethers.Contract(colInfo.address, ERC20_ABI, this._signer);
|
|
889
|
+
const transferTx = await colToken.transfer(acctAddr, weiAmount);
|
|
890
|
+
await transferTx.wait();
|
|
891
|
+
this._refreshSigner();
|
|
892
|
+
const targets = [colInfo.address, morphoAddr];
|
|
893
|
+
const values = [0n, 0n];
|
|
894
|
+
const datas = [
|
|
895
|
+
erc20Iface.encodeFunctionData("approve", [morphoAddr, weiAmount]),
|
|
896
|
+
morphoIface.encodeFunctionData("supplyCollateral", [
|
|
897
|
+
this._toTuple(params),
|
|
898
|
+
weiAmount,
|
|
899
|
+
acctAddr,
|
|
900
|
+
"0x"
|
|
901
|
+
])
|
|
902
|
+
];
|
|
903
|
+
const receipt = await this.batch(targets, values, datas);
|
|
904
|
+
return {
|
|
905
|
+
tx: receipt.hash,
|
|
906
|
+
collateralToken: tokenSymbol,
|
|
907
|
+
amount,
|
|
908
|
+
agentAccount: acctAddr
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Borrow USDC against existing collateral.
|
|
913
|
+
*
|
|
914
|
+
* AgentAccount.execute: Morpho.borrow(params, amount, 0, account, account)
|
|
915
|
+
*
|
|
916
|
+
* @param usdcAmount - USDC amount (e.g. '100')
|
|
917
|
+
* @param tokenSymbol - collateral symbol to identify which market (default: first with collateral)
|
|
918
|
+
*/
|
|
919
|
+
async borrow(usdcAmount, tokenSymbol, marketParams) {
|
|
920
|
+
const acctAddr = await this.getAccountAddress();
|
|
921
|
+
const amount = import_ethers.ethers.parseUnits(usdcAmount, 6);
|
|
922
|
+
const morphoAddr = this.config.contracts.morphoBlue;
|
|
923
|
+
let params;
|
|
924
|
+
let usedToken = tokenSymbol || "WETH";
|
|
925
|
+
if (marketParams) {
|
|
926
|
+
params = marketParams;
|
|
927
|
+
} else if (tokenSymbol) {
|
|
928
|
+
params = await this.findMarketForCollateral(tokenSymbol);
|
|
929
|
+
} else {
|
|
930
|
+
const { params: p, symbol } = await this._findActiveMarket();
|
|
931
|
+
params = p;
|
|
932
|
+
usedToken = symbol;
|
|
933
|
+
}
|
|
934
|
+
const data = morphoIface.encodeFunctionData("borrow", [
|
|
935
|
+
this._toTuple(params),
|
|
936
|
+
amount,
|
|
937
|
+
0n,
|
|
938
|
+
acctAddr,
|
|
939
|
+
acctAddr
|
|
940
|
+
]);
|
|
941
|
+
const receipt = await this.exec(morphoAddr, data);
|
|
942
|
+
return {
|
|
943
|
+
tx: receipt.hash,
|
|
944
|
+
amount: usdcAmount,
|
|
945
|
+
collateralToken: usedToken,
|
|
946
|
+
agentAccount: acctAddr
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Deposit collateral AND borrow USDC in one batched transaction.
|
|
951
|
+
*
|
|
952
|
+
* AgentAccount.executeBatch:
|
|
953
|
+
* [collateral.approve, Morpho.supplyCollateral, Morpho.borrow]
|
|
954
|
+
*
|
|
955
|
+
* The collateral must be transferred to AgentAccount first.
|
|
956
|
+
*/
|
|
957
|
+
async depositAndBorrow(tokenSymbol, collateralAmount, borrowUsdcAmount, marketParams) {
|
|
958
|
+
const acctAddr = await this.getAccountAddress();
|
|
959
|
+
const colInfo = BASE_COLLATERALS[tokenSymbol];
|
|
960
|
+
if (!colInfo) throw new AgetherError(`Unknown collateral: ${tokenSymbol}`, "UNKNOWN_COLLATERAL");
|
|
961
|
+
const params = marketParams ?? await this.findMarketForCollateral(tokenSymbol);
|
|
962
|
+
const colWei = import_ethers.ethers.parseUnits(collateralAmount, colInfo.decimals);
|
|
963
|
+
const borrowWei = import_ethers.ethers.parseUnits(borrowUsdcAmount, 6);
|
|
964
|
+
const morphoAddr = this.config.contracts.morphoBlue;
|
|
965
|
+
const colToken = new import_ethers.Contract(colInfo.address, ERC20_ABI, this._signer);
|
|
966
|
+
const transferTx = await colToken.transfer(acctAddr, colWei);
|
|
967
|
+
await transferTx.wait();
|
|
968
|
+
this._refreshSigner();
|
|
969
|
+
const targets = [colInfo.address, morphoAddr, morphoAddr];
|
|
970
|
+
const values = [0n, 0n, 0n];
|
|
971
|
+
const datas = [
|
|
972
|
+
erc20Iface.encodeFunctionData("approve", [morphoAddr, colWei]),
|
|
973
|
+
morphoIface.encodeFunctionData("supplyCollateral", [
|
|
974
|
+
this._toTuple(params),
|
|
975
|
+
colWei,
|
|
976
|
+
acctAddr,
|
|
977
|
+
"0x"
|
|
978
|
+
]),
|
|
979
|
+
morphoIface.encodeFunctionData("borrow", [
|
|
980
|
+
this._toTuple(params),
|
|
981
|
+
borrowWei,
|
|
982
|
+
0n,
|
|
983
|
+
acctAddr,
|
|
984
|
+
acctAddr
|
|
985
|
+
])
|
|
986
|
+
];
|
|
987
|
+
const receipt = await this.batch(targets, values, datas);
|
|
988
|
+
return {
|
|
989
|
+
tx: receipt.hash,
|
|
990
|
+
collateralToken: tokenSymbol,
|
|
991
|
+
collateralAmount,
|
|
992
|
+
borrowAmount: borrowUsdcAmount,
|
|
993
|
+
agentAccount: acctAddr
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Repay borrowed USDC from AgentAccount.
|
|
998
|
+
*
|
|
999
|
+
* AgentAccount.executeBatch:
|
|
1000
|
+
* [USDC.approve(MorphoBlue), Morpho.repay(params)]
|
|
1001
|
+
*/
|
|
1002
|
+
async repay(usdcAmount, tokenSymbol, marketParams) {
|
|
1003
|
+
const acctAddr = await this.getAccountAddress();
|
|
1004
|
+
const morphoAddr = this.config.contracts.morphoBlue;
|
|
1005
|
+
const usdcAddr = this.config.contracts.usdc;
|
|
1006
|
+
let params;
|
|
1007
|
+
if (marketParams) {
|
|
1008
|
+
params = marketParams;
|
|
1009
|
+
} else if (tokenSymbol) {
|
|
1010
|
+
params = await this.findMarketForCollateral(tokenSymbol);
|
|
1011
|
+
} else {
|
|
1012
|
+
const { params: p } = await this._findActiveMarket();
|
|
1013
|
+
params = p;
|
|
1014
|
+
}
|
|
1015
|
+
let repayAssets;
|
|
1016
|
+
let repayShares;
|
|
1017
|
+
let approveAmount;
|
|
1018
|
+
if (usdcAmount === "all") {
|
|
1019
|
+
const markets = await this.getMarkets();
|
|
1020
|
+
const mkt = markets.find(
|
|
1021
|
+
(m) => m.collateralAsset?.address.toLowerCase() === params.collateralToken.toLowerCase()
|
|
1022
|
+
);
|
|
1023
|
+
if (mkt) {
|
|
1024
|
+
const pos = await this.morphoBlue.position(mkt.uniqueKey, acctAddr);
|
|
1025
|
+
repayShares = BigInt(pos.borrowShares);
|
|
1026
|
+
repayAssets = 0n;
|
|
1027
|
+
const onChainMkt = await this.morphoBlue.market(mkt.uniqueKey);
|
|
1028
|
+
const totalBorrowAssets = BigInt(onChainMkt.totalBorrowAssets);
|
|
1029
|
+
const totalBorrowShares = BigInt(onChainMkt.totalBorrowShares);
|
|
1030
|
+
const estimated = totalBorrowShares > 0n ? repayShares * totalBorrowAssets / totalBorrowShares + 10n : 0n;
|
|
1031
|
+
approveAmount = estimated > 0n ? estimated : import_ethers.ethers.parseUnits("1", 6);
|
|
1032
|
+
} else {
|
|
1033
|
+
repayAssets = import_ethers.ethers.parseUnits("999999", 6);
|
|
1034
|
+
repayShares = 0n;
|
|
1035
|
+
approveAmount = repayAssets;
|
|
1036
|
+
}
|
|
1037
|
+
} else {
|
|
1038
|
+
repayAssets = import_ethers.ethers.parseUnits(usdcAmount, 6);
|
|
1039
|
+
repayShares = 0n;
|
|
1040
|
+
approveAmount = repayAssets;
|
|
1041
|
+
}
|
|
1042
|
+
const usdcContract = new import_ethers.Contract(usdcAddr, ERC20_ABI, this._signer);
|
|
1043
|
+
const acctBalance = await usdcContract.balanceOf(acctAddr);
|
|
1044
|
+
if (acctBalance < approveAmount) {
|
|
1045
|
+
const shortfall = approveAmount - acctBalance;
|
|
1046
|
+
const eoaBalance = await usdcContract.balanceOf(await this.getSignerAddress());
|
|
1047
|
+
if (eoaBalance < shortfall) {
|
|
1048
|
+
throw new AgetherError(
|
|
1049
|
+
`Insufficient USDC for repay. Need ${import_ethers.ethers.formatUnits(approveAmount, 6)} USDC, AgentAccount has ${import_ethers.ethers.formatUnits(acctBalance, 6)}, EOA has ${import_ethers.ethers.formatUnits(eoaBalance, 6)}.`,
|
|
1050
|
+
"INSUFFICIENT_BALANCE"
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
const transferTx = await usdcContract.transfer(acctAddr, shortfall);
|
|
1054
|
+
await transferTx.wait();
|
|
1055
|
+
this._refreshSigner();
|
|
1056
|
+
}
|
|
1057
|
+
const targets = [usdcAddr, morphoAddr];
|
|
1058
|
+
const values = [0n, 0n];
|
|
1059
|
+
const datas = [
|
|
1060
|
+
erc20Iface.encodeFunctionData("approve", [morphoAddr, approveAmount]),
|
|
1061
|
+
morphoIface.encodeFunctionData("repay", [
|
|
1062
|
+
this._toTuple(params),
|
|
1063
|
+
repayAssets,
|
|
1064
|
+
repayShares,
|
|
1065
|
+
acctAddr,
|
|
1066
|
+
"0x"
|
|
1067
|
+
])
|
|
1068
|
+
];
|
|
1069
|
+
const receipt = await this.batch(targets, values, datas);
|
|
1070
|
+
let remainingDebt = "0";
|
|
1071
|
+
try {
|
|
1072
|
+
const status = await this.getStatus();
|
|
1073
|
+
remainingDebt = status.totalDebt;
|
|
1074
|
+
} catch (e) {
|
|
1075
|
+
console.warn("[agether] failed to read remaining debt after repay:", e instanceof Error ? e.message : e);
|
|
1076
|
+
}
|
|
1077
|
+
return { tx: receipt.hash, amount: usdcAmount, remainingDebt };
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Withdraw collateral from Morpho Blue.
|
|
1081
|
+
*
|
|
1082
|
+
* AgentAccount.execute: Morpho.withdrawCollateral(params, amount, account, receiver)
|
|
1083
|
+
*
|
|
1084
|
+
* @param receiver - defaults to EOA wallet
|
|
1085
|
+
*/
|
|
1086
|
+
async withdrawCollateral(tokenSymbol, amount, marketParams, receiver) {
|
|
1087
|
+
const acctAddr = await this.getAccountAddress();
|
|
1088
|
+
const colInfo = BASE_COLLATERALS[tokenSymbol];
|
|
1089
|
+
if (!colInfo) throw new AgetherError(`Unknown collateral: ${tokenSymbol}`, "UNKNOWN_COLLATERAL");
|
|
1090
|
+
const params = marketParams ?? await this.findMarketForCollateral(tokenSymbol);
|
|
1091
|
+
const morphoAddr = this.config.contracts.morphoBlue;
|
|
1092
|
+
const usdcAddr = this.config.contracts.usdc;
|
|
1093
|
+
const dest = receiver || await this.getSignerAddress();
|
|
1094
|
+
let weiAmount;
|
|
1095
|
+
const markets = await this.getMarkets();
|
|
1096
|
+
const market = markets.find(
|
|
1097
|
+
(m) => m.collateralAsset?.address.toLowerCase() === colInfo.address.toLowerCase()
|
|
1098
|
+
);
|
|
1099
|
+
if (amount === "all") {
|
|
1100
|
+
if (!market) throw new AgetherError("Market not found", "MARKET_NOT_FOUND");
|
|
1101
|
+
const pos = await this.morphoBlue.position(market.uniqueKey, acctAddr);
|
|
1102
|
+
weiAmount = pos.collateral;
|
|
1103
|
+
if (weiAmount === 0n) throw new AgetherError("No collateral to withdraw", "NO_COLLATERAL");
|
|
1104
|
+
} else {
|
|
1105
|
+
weiAmount = import_ethers.ethers.parseUnits(amount, colInfo.decimals);
|
|
1106
|
+
}
|
|
1107
|
+
let hasDustDebt = false;
|
|
1108
|
+
let dustBorrowShares = 0n;
|
|
1109
|
+
let dustApproveAmount = 0n;
|
|
1110
|
+
if (market) {
|
|
1111
|
+
try {
|
|
1112
|
+
const pos = await this.morphoBlue.position(market.uniqueKey, acctAddr);
|
|
1113
|
+
dustBorrowShares = BigInt(pos.borrowShares);
|
|
1114
|
+
if (dustBorrowShares > 0n) {
|
|
1115
|
+
hasDustDebt = true;
|
|
1116
|
+
const onChainMkt = await this.morphoBlue.market(market.uniqueKey);
|
|
1117
|
+
const totalBorrowAssets = BigInt(onChainMkt.totalBorrowAssets);
|
|
1118
|
+
const totalBorrowShares = BigInt(onChainMkt.totalBorrowShares);
|
|
1119
|
+
const estimated = totalBorrowShares > 0n ? dustBorrowShares * totalBorrowAssets / totalBorrowShares + 10n : 0n;
|
|
1120
|
+
dustApproveAmount = estimated > 0n ? estimated : import_ethers.ethers.parseUnits("1", 6);
|
|
1121
|
+
console.log(`[agether] dust borrow shares detected: ${dustBorrowShares} shares \u2248 ${import_ethers.ethers.formatUnits(dustApproveAmount, 6)} USDC \u2014 auto-repaying before withdraw`);
|
|
1122
|
+
}
|
|
1123
|
+
} catch (e) {
|
|
1124
|
+
console.warn("[agether] failed to check borrow shares before withdraw:", e instanceof Error ? e.message : e);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
const withdrawData = morphoIface.encodeFunctionData("withdrawCollateral", [
|
|
1128
|
+
this._toTuple(params),
|
|
1129
|
+
weiAmount,
|
|
1130
|
+
acctAddr,
|
|
1131
|
+
dest
|
|
1132
|
+
]);
|
|
1133
|
+
let receipt;
|
|
1134
|
+
if (hasDustDebt) {
|
|
1135
|
+
const usdcContract = new import_ethers.Contract(usdcAddr, ERC20_ABI, this._signer);
|
|
1136
|
+
const acctBalance = await usdcContract.balanceOf(acctAddr);
|
|
1137
|
+
if (acctBalance < dustApproveAmount) {
|
|
1138
|
+
const shortfall = dustApproveAmount - acctBalance;
|
|
1139
|
+
const eoaBalance = await usdcContract.balanceOf(await this.getSignerAddress());
|
|
1140
|
+
if (eoaBalance >= shortfall) {
|
|
1141
|
+
console.log(`[agether] transferring ${import_ethers.ethers.formatUnits(shortfall, 6)} USDC from EOA \u2192 AgentAccount for dust repay`);
|
|
1142
|
+
const transferTx = await usdcContract.transfer(acctAddr, shortfall);
|
|
1143
|
+
await transferTx.wait();
|
|
1144
|
+
this._refreshSigner();
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
const targets = [usdcAddr, morphoAddr, morphoAddr];
|
|
1148
|
+
const values = [0n, 0n, 0n];
|
|
1149
|
+
const datas = [
|
|
1150
|
+
erc20Iface.encodeFunctionData("approve", [morphoAddr, dustApproveAmount]),
|
|
1151
|
+
morphoIface.encodeFunctionData("repay", [
|
|
1152
|
+
this._toTuple(params),
|
|
1153
|
+
0n,
|
|
1154
|
+
dustBorrowShares,
|
|
1155
|
+
acctAddr,
|
|
1156
|
+
"0x"
|
|
1157
|
+
]),
|
|
1158
|
+
withdrawData
|
|
1159
|
+
];
|
|
1160
|
+
receipt = await this.batch(targets, values, datas);
|
|
1161
|
+
} else {
|
|
1162
|
+
receipt = await this.exec(morphoAddr, withdrawData);
|
|
1163
|
+
}
|
|
1164
|
+
let remainingCollateral = "0";
|
|
1165
|
+
try {
|
|
1166
|
+
if (market) {
|
|
1167
|
+
const pos = await this.morphoBlue.position(market.uniqueKey, acctAddr);
|
|
1168
|
+
remainingCollateral = import_ethers.ethers.formatUnits(pos.collateral, colInfo.decimals);
|
|
1169
|
+
}
|
|
1170
|
+
} catch (e) {
|
|
1171
|
+
console.warn("[agether] failed to read remaining collateral after withdraw:", e instanceof Error ? e.message : e);
|
|
1172
|
+
}
|
|
1173
|
+
return {
|
|
1174
|
+
tx: receipt.hash,
|
|
1175
|
+
token: tokenSymbol,
|
|
1176
|
+
amount: amount === "all" ? import_ethers.ethers.formatUnits(weiAmount, colInfo.decimals) : amount,
|
|
1177
|
+
remainingCollateral,
|
|
1178
|
+
destination: dest
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Sponsor: transfer collateral to another agent's AgentAccount.
|
|
1183
|
+
* (The agent must then supplyCollateral themselves via their own account.)
|
|
1184
|
+
*/
|
|
1185
|
+
async sponsor(target, tokenSymbol, amount) {
|
|
1186
|
+
const colInfo = BASE_COLLATERALS[tokenSymbol];
|
|
1187
|
+
if (!colInfo) throw new AgetherError(`Unknown collateral: ${tokenSymbol}`, "UNKNOWN_COLLATERAL");
|
|
1188
|
+
let targetAddr;
|
|
1189
|
+
if (target.address) {
|
|
1190
|
+
targetAddr = target.address;
|
|
1191
|
+
} else if (target.agentId) {
|
|
1192
|
+
targetAddr = await this.safeAgentFactory.getAccount(BigInt(target.agentId));
|
|
1193
|
+
if (targetAddr === import_ethers.ethers.ZeroAddress) throw new AgetherError("Target agent has no account", "NO_ACCOUNT");
|
|
1194
|
+
} else {
|
|
1195
|
+
throw new AgetherError("Provide agentId or address", "INVALID_TARGET");
|
|
1196
|
+
}
|
|
1197
|
+
const weiAmount = import_ethers.ethers.parseUnits(amount, colInfo.decimals);
|
|
1198
|
+
const colToken = new import_ethers.Contract(colInfo.address, ERC20_ABI, this._signer);
|
|
1199
|
+
const tx = await colToken.transfer(targetAddr, weiAmount);
|
|
1200
|
+
const receipt = await tx.wait();
|
|
1201
|
+
this._refreshSigner();
|
|
1202
|
+
return { tx: receipt.hash, targetAccount: targetAddr, targetAgentId: target.agentId };
|
|
1203
|
+
}
|
|
1204
|
+
// ════════════════════════════════════════════════════════
|
|
1205
|
+
// Reputation (AgentReputation contract)
|
|
1206
|
+
// ════════════════════════════════════════════════════════
|
|
1207
|
+
async getCreditScore() {
|
|
1208
|
+
if (!this.agentId) throw new AgetherError("agentId not set", "NO_AGENT_ID");
|
|
1209
|
+
return this.agentReputation.getCreditScore(BigInt(this.agentId));
|
|
1210
|
+
}
|
|
1211
|
+
async getAttestation() {
|
|
1212
|
+
if (!this.agentId) throw new AgetherError("agentId not set", "NO_AGENT_ID");
|
|
1213
|
+
const att = await this.agentReputation.getAttestation(BigInt(this.agentId));
|
|
1214
|
+
return { score: att.score, timestamp: att.timestamp, signer: att.signer };
|
|
1215
|
+
}
|
|
1216
|
+
async isEligible(minScore = 500n) {
|
|
1217
|
+
if (!this.agentId) throw new AgetherError("agentId not set", "NO_AGENT_ID");
|
|
1218
|
+
const [eligible, currentScore] = await this.agentReputation.isEligible(BigInt(this.agentId), minScore);
|
|
1219
|
+
return { eligible, currentScore };
|
|
1220
|
+
}
|
|
1221
|
+
async isScoreFresh() {
|
|
1222
|
+
if (!this.agentId) throw new AgetherError("agentId not set", "NO_AGENT_ID");
|
|
1223
|
+
const [fresh, age] = await this.agentReputation.isScoreFresh(BigInt(this.agentId));
|
|
1224
|
+
return { fresh, age };
|
|
1225
|
+
}
|
|
1226
|
+
// ════════════════════════════════════════════════════════
|
|
1227
|
+
// Internal Helpers
|
|
1228
|
+
// ════════════════════════════════════════════════════════
|
|
1229
|
+
/**
|
|
1230
|
+
* Refresh the signer and re-bind contract instances.
|
|
1231
|
+
*
|
|
1232
|
+
* For the **privateKey** path: recreates provider + wallet so the next tx
|
|
1233
|
+
* fetches a fresh nonce from chain. Anvil (and some RPC providers) return a
|
|
1234
|
+
* stale `eth_getTransactionCount` right after a block is mined, causing
|
|
1235
|
+
* "nonce too low" on the follow-up tx.
|
|
1236
|
+
*
|
|
1237
|
+
* For the **external signer** path: the signer is immutable and owned by the
|
|
1238
|
+
* caller (e.g. custody provider). We only re-bind contract instances to
|
|
1239
|
+
* ensure they reference the current signer. Nonce management is the caller's
|
|
1240
|
+
* responsibility.
|
|
1241
|
+
*/
|
|
1242
|
+
_refreshSigner() {
|
|
1243
|
+
if (this._useExternalSigner) {
|
|
1244
|
+
const addrs = this.config.contracts;
|
|
1245
|
+
this.safeAgentFactory = new import_ethers.Contract(addrs.safeAgentFactory, ACCOUNT_FACTORY_ABI, this._signer);
|
|
1246
|
+
this.entryPoint = new import_ethers.Contract(addrs.entryPoint, ENTRYPOINT_V07_ABI, this._signer);
|
|
1247
|
+
this.validationModule = new import_ethers.Contract(addrs.erc8004ValidationModule, ERC8004_VALIDATION_MODULE_ABI, this.provider);
|
|
1248
|
+
this.agentReputation = new import_ethers.Contract(addrs.agentReputation, AGENT_REPUTATION_ABI, this._signer);
|
|
1249
|
+
this.identityRegistry = new import_ethers.Contract(addrs.identityRegistry, IDENTITY_REGISTRY_ABI, this._signer);
|
|
1250
|
+
} else {
|
|
1251
|
+
this.provider = new import_ethers.ethers.JsonRpcProvider(this._rpcUrl);
|
|
1252
|
+
const wallet = new import_ethers.ethers.Wallet(this._privateKey, this.provider);
|
|
1253
|
+
this._signer = wallet;
|
|
1254
|
+
this._eoaAddress = wallet.address;
|
|
1255
|
+
const addrs = this.config.contracts;
|
|
1256
|
+
this.safeAgentFactory = new import_ethers.Contract(addrs.safeAgentFactory, ACCOUNT_FACTORY_ABI, this._signer);
|
|
1257
|
+
this.entryPoint = new import_ethers.Contract(addrs.entryPoint, ENTRYPOINT_V07_ABI, this._signer);
|
|
1258
|
+
this.validationModule = new import_ethers.Contract(addrs.erc8004ValidationModule, ERC8004_VALIDATION_MODULE_ABI, this.provider);
|
|
1259
|
+
this.agentReputation = new import_ethers.Contract(addrs.agentReputation, AGENT_REPUTATION_ABI, this._signer);
|
|
1260
|
+
this.identityRegistry = new import_ethers.Contract(addrs.identityRegistry, IDENTITY_REGISTRY_ABI, this._signer);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
// ────────────────────────────────────────────────────────────
|
|
1264
|
+
// ERC-4337 UserOp helpers (Safe + Safe7579 + EntryPoint v0.7)
|
|
1265
|
+
// ────────────────────────────────────────────────────────────
|
|
1266
|
+
/**
|
|
1267
|
+
* Pack two uint128 values into a single bytes32:
|
|
1268
|
+
* bytes32 = (hi << 128) | lo
|
|
1269
|
+
*/
|
|
1270
|
+
_packUint128(hi, lo) {
|
|
1271
|
+
return import_ethers.ethers.zeroPadValue(import_ethers.ethers.toBeHex(hi << 128n | lo), 32);
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Build, sign and submit a PackedUserOperation through EntryPoint.handleOps.
|
|
1275
|
+
*
|
|
1276
|
+
* @param callData – the ABI-encoded calldata for the Safe7579 account
|
|
1277
|
+
* (e.g. `execute(mode, executionCalldata)`)
|
|
1278
|
+
* @returns the transaction receipt of the handleOps call
|
|
1279
|
+
*/
|
|
1280
|
+
async _submitUserOp(callData) {
|
|
1281
|
+
const sender = await this.getAccountAddress();
|
|
1282
|
+
const nonce = await this.entryPoint.getNonce(sender, 0);
|
|
1283
|
+
const feeData = await this.provider.getFeeData();
|
|
1284
|
+
const maxFeePerGas = feeData.maxFeePerGas ?? import_ethers.ethers.parseUnits("0.5", "gwei");
|
|
1285
|
+
const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ?? import_ethers.ethers.parseUnits("0.1", "gwei");
|
|
1286
|
+
const verificationGasLimit = 500000n;
|
|
1287
|
+
const callGasLimit = 800000n;
|
|
1288
|
+
const preVerificationGas = 100000n;
|
|
1289
|
+
const accountGasLimits = this._packUint128(verificationGasLimit, callGasLimit);
|
|
1290
|
+
const gasFees = this._packUint128(maxPriorityFeePerGas, maxFeePerGas);
|
|
1291
|
+
const userOp = {
|
|
1292
|
+
sender,
|
|
1293
|
+
nonce,
|
|
1294
|
+
initCode: "0x",
|
|
1295
|
+
callData,
|
|
1296
|
+
accountGasLimits,
|
|
1297
|
+
preVerificationGas,
|
|
1298
|
+
gasFees,
|
|
1299
|
+
paymasterAndData: "0x",
|
|
1300
|
+
signature: "0x"
|
|
1301
|
+
// placeholder — replaced after signing
|
|
1302
|
+
};
|
|
1303
|
+
const userOpHash = await this.entryPoint.getUserOpHash(userOp);
|
|
1304
|
+
const signature = await this._signer.signMessage(import_ethers.ethers.getBytes(userOpHash));
|
|
1305
|
+
userOp.signature = signature;
|
|
1306
|
+
const tx = await this.entryPoint.handleOps([userOp], await this.getSignerAddress());
|
|
1307
|
+
const receipt = await tx.wait();
|
|
1308
|
+
this._refreshSigner();
|
|
1309
|
+
return receipt;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Execute a single call via Safe7579 account (ERC-7579 single mode)
|
|
1313
|
+
* through an ERC-4337 UserOperation.
|
|
1314
|
+
*/
|
|
1315
|
+
async exec(target, data, value = 0n) {
|
|
1316
|
+
const executionCalldata = import_ethers.ethers.AbiCoder.defaultAbiCoder().encode(
|
|
1317
|
+
["address", "uint256", "bytes"],
|
|
1318
|
+
[target, value, data]
|
|
1319
|
+
);
|
|
1320
|
+
const safe7579Iface = new import_ethers.ethers.Interface(SAFE7579_ACCOUNT_ABI);
|
|
1321
|
+
const callData = safe7579Iface.encodeFunctionData("execute", [MODE_SINGLE, executionCalldata]);
|
|
1322
|
+
return this._submitUserOp(callData);
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Execute multiple calls via Safe7579 account (ERC-7579 batch mode)
|
|
1326
|
+
* through an ERC-4337 UserOperation.
|
|
1327
|
+
*/
|
|
1328
|
+
async batch(targets, values, datas) {
|
|
1329
|
+
const executions = targets.map((t, i) => [t, values[i], datas[i]]);
|
|
1330
|
+
const executionCalldata = import_ethers.ethers.AbiCoder.defaultAbiCoder().encode(
|
|
1331
|
+
["(address,uint256,bytes)[]"],
|
|
1332
|
+
[executions]
|
|
1333
|
+
);
|
|
1334
|
+
const safe7579Iface = new import_ethers.ethers.Interface(SAFE7579_ACCOUNT_ABI);
|
|
1335
|
+
const callData = safe7579Iface.encodeFunctionData("execute", [MODE_BATCH, executionCalldata]);
|
|
1336
|
+
return this._submitUserOp(callData);
|
|
1337
|
+
}
|
|
1338
|
+
/** Convert MorphoMarketParams to Solidity tuple. */
|
|
1339
|
+
_toTuple(p) {
|
|
1340
|
+
return [p.loanToken, p.collateralToken, p.oracle, p.irm, p.lltv];
|
|
1341
|
+
}
|
|
1342
|
+
/** Find the first market where the agent has collateral deposited. */
|
|
1343
|
+
async _findActiveMarket() {
|
|
1344
|
+
const acctAddr = await this.getAccountAddress();
|
|
1345
|
+
const markets = await this.getMarkets();
|
|
1346
|
+
for (const m of markets) {
|
|
1347
|
+
if (!m.collateralAsset || m.collateralAsset.address === import_ethers.ethers.ZeroAddress) continue;
|
|
1348
|
+
try {
|
|
1349
|
+
const pos = await this.morphoBlue.position(m.uniqueKey, acctAddr);
|
|
1350
|
+
if (pos.collateral > 0n) {
|
|
1351
|
+
return {
|
|
1352
|
+
params: {
|
|
1353
|
+
loanToken: m.loanAsset.address,
|
|
1354
|
+
collateralToken: m.collateralAsset.address,
|
|
1355
|
+
oracle: m.oracle,
|
|
1356
|
+
irm: m.irm,
|
|
1357
|
+
lltv: m.lltv
|
|
1358
|
+
},
|
|
1359
|
+
symbol: m.collateralAsset.symbol
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
} catch (e) {
|
|
1363
|
+
console.warn("[agether] _findActiveMarket position check failed:", e instanceof Error ? e.message : e);
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
const params = await this.findMarketForCollateral("WETH");
|
|
1368
|
+
return { params, symbol: "WETH" };
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
// src/clients/X402Client.ts
|
|
1375
|
+
var X402Client_exports = {};
|
|
1376
|
+
__export(X402Client_exports, {
|
|
1377
|
+
X402Client: () => X402Client
|
|
1378
|
+
});
|
|
1379
|
+
var import_fetch, import_client, import_client2, import_accounts, X402Client;
|
|
1380
|
+
var init_X402Client = __esm({
|
|
1381
|
+
"src/clients/X402Client.ts"() {
|
|
1382
|
+
"use strict";
|
|
1383
|
+
import_fetch = require("@x402/fetch");
|
|
1384
|
+
import_client = require("@x402/core/client");
|
|
1385
|
+
import_client2 = require("@x402/evm/exact/client");
|
|
1386
|
+
import_accounts = require("viem/accounts");
|
|
1387
|
+
X402Client = class {
|
|
1388
|
+
constructor(config) {
|
|
1389
|
+
this.config = config;
|
|
1390
|
+
let baseSigner;
|
|
1391
|
+
if ("walletClient" in config && config.walletClient) {
|
|
1392
|
+
const wc = config.walletClient;
|
|
1393
|
+
const account = wc.account;
|
|
1394
|
+
if (!account) {
|
|
1395
|
+
throw new Error(
|
|
1396
|
+
"X402Client: walletClient must have an attached account. Pass an account when creating the WalletClient or use privateKey mode instead."
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
this.address = account.address;
|
|
1400
|
+
baseSigner = {
|
|
1401
|
+
address: account.address,
|
|
1402
|
+
signTypedData: (msg) => wc.signTypedData({
|
|
1403
|
+
account,
|
|
1404
|
+
domain: msg.domain,
|
|
1405
|
+
types: msg.types,
|
|
1406
|
+
primaryType: msg.primaryType,
|
|
1407
|
+
message: msg.message
|
|
1408
|
+
})
|
|
1409
|
+
};
|
|
1410
|
+
} else if ("privateKey" in config && config.privateKey) {
|
|
1411
|
+
const privateKey = config.privateKey.startsWith("0x") ? config.privateKey : `0x${config.privateKey}`;
|
|
1412
|
+
const account = (0, import_accounts.privateKeyToAccount)(privateKey);
|
|
1413
|
+
this.address = account.address;
|
|
1414
|
+
baseSigner = account;
|
|
1415
|
+
} else {
|
|
1416
|
+
throw new Error(
|
|
1417
|
+
"X402Client: provide either privateKey or walletClient in config."
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
let signer;
|
|
1421
|
+
if (config.accountAddress) {
|
|
1422
|
+
const accountAddr = config.accountAddress;
|
|
1423
|
+
const inner = baseSigner;
|
|
1424
|
+
signer = (0, import_accounts.toAccount)({
|
|
1425
|
+
address: accountAddr,
|
|
1426
|
+
async signMessage({ message }) {
|
|
1427
|
+
if ("signMessage" in inner && typeof inner.signMessage === "function") {
|
|
1428
|
+
return inner.signMessage({ message });
|
|
1429
|
+
}
|
|
1430
|
+
throw new Error("signMessage not supported by underlying signer");
|
|
1431
|
+
},
|
|
1432
|
+
async signTransaction(tx) {
|
|
1433
|
+
if ("signTransaction" in inner && typeof inner.signTransaction === "function") {
|
|
1434
|
+
return inner.signTransaction(tx);
|
|
1435
|
+
}
|
|
1436
|
+
throw new Error("signTransaction not supported by underlying signer");
|
|
1437
|
+
},
|
|
1438
|
+
async signTypedData(typedData) {
|
|
1439
|
+
const sig = await inner.signTypedData(typedData);
|
|
1440
|
+
return `${sig}00`;
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
this.address = accountAddr;
|
|
1444
|
+
} else {
|
|
1445
|
+
signer = baseSigner;
|
|
1446
|
+
}
|
|
1447
|
+
const client = new import_client.x402Client();
|
|
1448
|
+
(0, import_client2.registerExactEvmScheme)(client, { signer });
|
|
1449
|
+
this.paidFetch = (0, import_fetch.wrapFetchWithPayment)(fetch, client);
|
|
1450
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1451
|
+
const dailyLimit = config.dailySpendLimitUsdc ? BigInt(Math.round(parseFloat(config.dailySpendLimitUsdc) * 1e6)) : 0n;
|
|
1452
|
+
this._spendingTracker = { date: today, totalBorrowed: 0n, dailyLimit };
|
|
1453
|
+
}
|
|
1454
|
+
async get(url, opts) {
|
|
1455
|
+
return this.request(url, { ...opts, method: "GET" });
|
|
1456
|
+
}
|
|
1457
|
+
async post(url, body, opts) {
|
|
1458
|
+
return this.request(url, {
|
|
1459
|
+
...opts,
|
|
1460
|
+
method: "POST",
|
|
1461
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
1462
|
+
headers: { "Content-Type": "application/json", ...opts?.headers }
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
getAddress() {
|
|
1466
|
+
return this.address;
|
|
1467
|
+
}
|
|
1468
|
+
/** Get the current spending tracker state */
|
|
1469
|
+
getSpendingTracker() {
|
|
1470
|
+
this._resetTrackerIfNewDay();
|
|
1471
|
+
return { ...this._spendingTracker };
|
|
1472
|
+
}
|
|
1473
|
+
/** Get remaining daily spending allowance in USDC (human-readable) */
|
|
1474
|
+
getRemainingDailyAllowance() {
|
|
1475
|
+
this._resetTrackerIfNewDay();
|
|
1476
|
+
if (this._spendingTracker.dailyLimit === 0n) return "unlimited";
|
|
1477
|
+
const remaining = this._spendingTracker.dailyLimit - this._spendingTracker.totalBorrowed;
|
|
1478
|
+
return (Number(remaining > 0n ? remaining : 0n) / 1e6).toFixed(2);
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Pay with auto-draw: Make an x402 request with automatic Morpho borrowing.
|
|
1482
|
+
*
|
|
1483
|
+
* Flow:
|
|
1484
|
+
* 1. Check USDC balance on AgentAccount
|
|
1485
|
+
* 2. Probe the URL to discover payment amount (if 402)
|
|
1486
|
+
* 3. If insufficient USDC, calculate deficit
|
|
1487
|
+
* 4. Check spending limit
|
|
1488
|
+
* 5. Borrow from Morpho via MorphoClient
|
|
1489
|
+
* 6. Proceed with x402 payment
|
|
1490
|
+
*/
|
|
1491
|
+
async payWithAutoDraw(url, opts) {
|
|
1492
|
+
const { morphoClient, ...fetchOpts } = opts || {};
|
|
1493
|
+
if (!this.config.autoDraw || !morphoClient) {
|
|
1494
|
+
return this.request(url, fetchOpts);
|
|
1495
|
+
}
|
|
1496
|
+
try {
|
|
1497
|
+
const usdcBalance = await morphoClient.getUsdcBalance();
|
|
1498
|
+
console.log(` [auto-draw] AgentAccount USDC balance: ${(Number(usdcBalance) / 1e6).toFixed(2)}`);
|
|
1499
|
+
const paymentAmount = await this._probePaymentAmount(url, fetchOpts);
|
|
1500
|
+
if (paymentAmount !== null) {
|
|
1501
|
+
console.log(` [auto-draw] Payment required: ${(Number(paymentAmount) / 1e6).toFixed(6)} USDC`);
|
|
1502
|
+
const bufferStr = this.config.autoDrawBuffer || "0.5";
|
|
1503
|
+
const buffer = BigInt(Math.round(parseFloat(bufferStr) * 1e6));
|
|
1504
|
+
const needed = paymentAmount + buffer;
|
|
1505
|
+
if (usdcBalance < needed) {
|
|
1506
|
+
const deficit = needed - usdcBalance;
|
|
1507
|
+
console.log(` [auto-draw] Insufficient balance. Need to borrow ${(Number(deficit) / 1e6).toFixed(2)} USDC`);
|
|
1508
|
+
const limitCheck = await this._checkSpendingLimit(deficit, morphoClient);
|
|
1509
|
+
if (!limitCheck.allowed) {
|
|
1510
|
+
return {
|
|
1511
|
+
success: false,
|
|
1512
|
+
error: `Auto-draw blocked: ${limitCheck.reason}`
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
const maxBorrowable = await morphoClient.getMaxBorrowable();
|
|
1516
|
+
if (maxBorrowable.total < deficit) {
|
|
1517
|
+
return {
|
|
1518
|
+
success: false,
|
|
1519
|
+
error: `Auto-draw failed: insufficient collateral. Need ${(Number(deficit) / 1e6).toFixed(2)} USDC but can only borrow ${(Number(maxBorrowable.total) / 1e6).toFixed(2)} USDC more.`
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
const borrowAmount = (Number(deficit) / 1e6).toFixed(6);
|
|
1523
|
+
console.log(` [auto-draw] Borrowing ${borrowAmount} USDC from Morpho...`);
|
|
1524
|
+
const borrowResult = await morphoClient.borrow(borrowAmount);
|
|
1525
|
+
console.log(` [auto-draw] Borrow tx: ${borrowResult.tx}`);
|
|
1526
|
+
this._trackSpending(deficit);
|
|
1527
|
+
const result = await this.request(url, fetchOpts);
|
|
1528
|
+
return {
|
|
1529
|
+
...result,
|
|
1530
|
+
autoDrawInfo: {
|
|
1531
|
+
borrowed: borrowAmount,
|
|
1532
|
+
borrowTx: borrowResult.tx,
|
|
1533
|
+
reason: `USDC balance insufficient (had ${(Number(usdcBalance) / 1e6).toFixed(2)}, needed ${(Number(needed) / 1e6).toFixed(2)})`
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return this.request(url, fetchOpts);
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
console.log(` [auto-draw] Auto-draw check failed: ${error instanceof Error ? error.message : String(error)}. Proceeding with normal request.`);
|
|
1541
|
+
return this.request(url, fetchOpts);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
// ──────────── Core request — @x402/fetch handles 402 automatically ────────────
|
|
1545
|
+
async request(url, options) {
|
|
1546
|
+
try {
|
|
1547
|
+
console.log(` [x402] ${options?.method || "GET"} ${url}`);
|
|
1548
|
+
const response = await this.paidFetch(url, {
|
|
1549
|
+
...options,
|
|
1550
|
+
headers: {
|
|
1551
|
+
...options?.headers,
|
|
1552
|
+
"X-Agent-Id": this.config.agentId || ""
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
if (response.ok) {
|
|
1556
|
+
const data = await response.json();
|
|
1557
|
+
const paymentResponse = response.headers.get("PAYMENT-RESPONSE");
|
|
1558
|
+
let txHash;
|
|
1559
|
+
if (paymentResponse) {
|
|
1560
|
+
try {
|
|
1561
|
+
const settlement = JSON.parse(
|
|
1562
|
+
Buffer.from(paymentResponse, "base64").toString("utf-8")
|
|
1563
|
+
);
|
|
1564
|
+
txHash = settlement.transaction;
|
|
1565
|
+
} catch (e) {
|
|
1566
|
+
console.warn("[agether] x402 payment response parse failed:", e instanceof Error ? e.message : e);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
return {
|
|
1570
|
+
success: true,
|
|
1571
|
+
data,
|
|
1572
|
+
...txHash ? {
|
|
1573
|
+
paymentInfo: {
|
|
1574
|
+
amount: "",
|
|
1575
|
+
asset: "USDC",
|
|
1576
|
+
network: "eip155:8453",
|
|
1577
|
+
txHash
|
|
1578
|
+
}
|
|
1579
|
+
} : {}
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
const errBody = await response.text();
|
|
1583
|
+
return {
|
|
1584
|
+
success: false,
|
|
1585
|
+
error: `HTTP ${response.status}: ${errBody}`
|
|
1586
|
+
};
|
|
1587
|
+
} catch (error) {
|
|
1588
|
+
return {
|
|
1589
|
+
success: false,
|
|
1590
|
+
error: `Request failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
// ──────────── Auto-Draw Helpers ────────────
|
|
1595
|
+
/**
|
|
1596
|
+
* Probe a URL to discover payment requirements without paying.
|
|
1597
|
+
* Makes a request and parses the 402 PAYMENT-REQUIRED header.
|
|
1598
|
+
* @returns Payment amount in raw USDC units (6 decimals), or null if not a 402.
|
|
1599
|
+
*/
|
|
1600
|
+
async _probePaymentAmount(url, options) {
|
|
1601
|
+
try {
|
|
1602
|
+
const response = await fetch(url, {
|
|
1603
|
+
...options,
|
|
1604
|
+
headers: {
|
|
1605
|
+
...options?.headers,
|
|
1606
|
+
"X-Agent-Id": this.config.agentId || ""
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
if (response.status !== 402) return null;
|
|
1610
|
+
const paymentHeader = response.headers.get("X-PAYMENT") || response.headers.get("PAYMENT-REQUIRED");
|
|
1611
|
+
if (!paymentHeader) return null;
|
|
1612
|
+
try {
|
|
1613
|
+
const decoded = JSON.parse(Buffer.from(paymentHeader, "base64").toString("utf-8"));
|
|
1614
|
+
const requirements = Array.isArray(decoded) ? decoded : decoded.accepts || [decoded];
|
|
1615
|
+
if (requirements.length > 0) {
|
|
1616
|
+
const amount = requirements[0].maxAmountRequired || requirements[0].amount;
|
|
1617
|
+
if (amount) {
|
|
1618
|
+
return BigInt(amount);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
} catch (e) {
|
|
1622
|
+
console.warn("[agether] x402 payment header parse failed:", e instanceof Error ? e.message : e);
|
|
1623
|
+
}
|
|
1624
|
+
return null;
|
|
1625
|
+
} catch (e) {
|
|
1626
|
+
console.warn("[agether] x402 getPaymentRequired failed:", e instanceof Error ? e.message : e);
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
/** Reset spending tracker if it's a new day */
|
|
1631
|
+
_resetTrackerIfNewDay() {
|
|
1632
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1633
|
+
if (this._spendingTracker.date !== today) {
|
|
1634
|
+
this._spendingTracker = {
|
|
1635
|
+
date: today,
|
|
1636
|
+
totalBorrowed: 0n,
|
|
1637
|
+
dailyLimit: this._spendingTracker.dailyLimit
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
/** Track a new spending amount */
|
|
1642
|
+
_trackSpending(amount) {
|
|
1643
|
+
this._resetTrackerIfNewDay();
|
|
1644
|
+
this._spendingTracker.totalBorrowed += amount;
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Check if a borrow amount is within spending limits.
|
|
1648
|
+
* Considers both fixed daily limits and yield-limited spending.
|
|
1649
|
+
*/
|
|
1650
|
+
async _checkSpendingLimit(amount, morphoClient) {
|
|
1651
|
+
this._resetTrackerIfNewDay();
|
|
1652
|
+
if (this.config.yieldLimitedSpending) {
|
|
1653
|
+
try {
|
|
1654
|
+
const status = await morphoClient.getStatus();
|
|
1655
|
+
let totalDailyYieldUsdc = 0;
|
|
1656
|
+
for (const pos of status.positions) {
|
|
1657
|
+
if (parseFloat(pos.collateral) > 0) {
|
|
1658
|
+
try {
|
|
1659
|
+
const estimate = await morphoClient.getYieldEstimate(
|
|
1660
|
+
pos.collateralToken,
|
|
1661
|
+
pos.collateral,
|
|
1662
|
+
1
|
|
1663
|
+
// 1 day
|
|
1664
|
+
);
|
|
1665
|
+
totalDailyYieldUsdc += estimate.estimatedYieldUsd;
|
|
1666
|
+
} catch (e) {
|
|
1667
|
+
console.warn(`[agether] yield calc failed for ${pos.collateralToken}:`, e instanceof Error ? e.message : e);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
const yieldLimit = BigInt(Math.round(totalDailyYieldUsdc * 1e6));
|
|
1672
|
+
const newTotal = this._spendingTracker.totalBorrowed + amount;
|
|
1673
|
+
if (yieldLimit > 0n && newTotal > yieldLimit) {
|
|
1674
|
+
return {
|
|
1675
|
+
allowed: false,
|
|
1676
|
+
reason: `Yield-limited spending exceeded. Daily yield cap: $${(Number(yieldLimit) / 1e6).toFixed(2)}, already spent: $${(Number(this._spendingTracker.totalBorrowed) / 1e6).toFixed(2)}, requested: $${(Number(amount) / 1e6).toFixed(2)}`
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
} catch (e) {
|
|
1680
|
+
console.warn("[agether] yield-limited spending check failed, falling through to fixed limit:", e instanceof Error ? e.message : e);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
if (this._spendingTracker.dailyLimit > 0n) {
|
|
1684
|
+
const newTotal = this._spendingTracker.totalBorrowed + amount;
|
|
1685
|
+
if (newTotal > this._spendingTracker.dailyLimit) {
|
|
1686
|
+
return {
|
|
1687
|
+
allowed: false,
|
|
1688
|
+
reason: `Daily spending limit exceeded. Limit: $${(Number(this._spendingTracker.dailyLimit) / 1e6).toFixed(2)}, already spent: $${(Number(this._spendingTracker.totalBorrowed) / 1e6).toFixed(2)}, requested: $${(Number(amount) / 1e6).toFixed(2)}`
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
return { allowed: true };
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
// src/cli.ts
|
|
1699
|
+
var import_ethers2 = require("ethers");
|
|
1700
|
+
var fs = __toESM(require("fs"));
|
|
1701
|
+
var path = __toESM(require("path"));
|
|
1702
|
+
var os = __toESM(require("os"));
|
|
1703
|
+
var CONFIG_PATH = path.join(os.homedir(), ".agether", "config.json");
|
|
1704
|
+
var DEFAULT_RPC = "https://base-rpc.publicnode.com";
|
|
1705
|
+
var DEFAULT_BACKEND = "http://95.179.189.214:3001";
|
|
1706
|
+
function loadConfig() {
|
|
1707
|
+
try {
|
|
1708
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
1709
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
1710
|
+
}
|
|
1711
|
+
} catch {
|
|
1712
|
+
}
|
|
1713
|
+
return null;
|
|
1714
|
+
}
|
|
1715
|
+
function saveConfig(config) {
|
|
1716
|
+
const dir = path.dirname(CONFIG_PATH);
|
|
1717
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1718
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
1719
|
+
}
|
|
1720
|
+
function requireConfig() {
|
|
1721
|
+
const config = loadConfig();
|
|
1722
|
+
if (!config) {
|
|
1723
|
+
console.error("\u274C Not initialized. Run: agether init <private-key>");
|
|
1724
|
+
process.exit(1);
|
|
1725
|
+
}
|
|
1726
|
+
return config;
|
|
1727
|
+
}
|
|
1728
|
+
async function waitForTx(tx, retries = 5) {
|
|
1729
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
1730
|
+
try {
|
|
1731
|
+
const receipt = await tx.wait();
|
|
1732
|
+
if (receipt) return receipt;
|
|
1733
|
+
} catch (e) {
|
|
1734
|
+
if (attempt === retries) throw e;
|
|
1735
|
+
console.log(` \u23F3 Receipt fetch failed (attempt ${attempt}/${retries}), retrying...`);
|
|
1736
|
+
await new Promise((r) => setTimeout(r, 2e3 * attempt));
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
throw new Error("Failed to get transaction receipt after retries");
|
|
1740
|
+
}
|
|
1741
|
+
async function getMorphoClient(config) {
|
|
1742
|
+
const { MorphoClient: MorphoClient2 } = await Promise.resolve().then(() => (init_MorphoClient(), MorphoClient_exports));
|
|
1743
|
+
return new MorphoClient2({
|
|
1744
|
+
privateKey: config.privateKey,
|
|
1745
|
+
rpcUrl: config.rpcUrl,
|
|
1746
|
+
agentId: config.agentId !== "0" ? config.agentId : void 0
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
async function getX402Client(config) {
|
|
1750
|
+
const { X402Client: X402Client2 } = await Promise.resolve().then(() => (init_X402Client(), X402Client_exports));
|
|
1751
|
+
let accountAddress;
|
|
1752
|
+
try {
|
|
1753
|
+
const mc = await getMorphoClient(config);
|
|
1754
|
+
accountAddress = await mc.getAccountAddress();
|
|
1755
|
+
} catch {
|
|
1756
|
+
}
|
|
1757
|
+
return new X402Client2({
|
|
1758
|
+
privateKey: config.privateKey,
|
|
1759
|
+
rpcUrl: config.rpcUrl,
|
|
1760
|
+
backendUrl: config.backendUrl,
|
|
1761
|
+
agentId: config.agentId,
|
|
1762
|
+
accountAddress
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
var ERC8004_ABI = [
|
|
1766
|
+
"function register(string agentURI) external returns (uint256 agentId)",
|
|
1767
|
+
"function register() external returns (uint256 agentId)",
|
|
1768
|
+
"function ownerOf(uint256 tokenId) view returns (address)",
|
|
1769
|
+
"function balanceOf(address owner) view returns (uint256)",
|
|
1770
|
+
"event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)"
|
|
1771
|
+
];
|
|
1772
|
+
var ACCOUNT_FACTORY_ABI2 = [
|
|
1773
|
+
"function getAccount(uint256 agentId) view returns (address)",
|
|
1774
|
+
"function accountExists(uint256 agentId) view returns (bool)",
|
|
1775
|
+
"function createAccount(uint256 agentId) returns (address account)",
|
|
1776
|
+
"event AccountCreated(uint256 indexed agentId, address indexed account, address indexed owner)"
|
|
1777
|
+
];
|
|
1778
|
+
var VALIDATION_REGISTRY_ABI = [
|
|
1779
|
+
"function isAgentCodeApproved(uint256 agentId) view returns (bool)"
|
|
1780
|
+
];
|
|
1781
|
+
var MOCK_ERC20_ABI = [
|
|
1782
|
+
"function mint(address to, uint256 amount) external"
|
|
1783
|
+
];
|
|
1784
|
+
async function apiGet(backendUrl, endpoint) {
|
|
1785
|
+
const res = await fetch(`${backendUrl}${endpoint}`);
|
|
1786
|
+
return res.json();
|
|
1787
|
+
}
|
|
1788
|
+
function decodeError(error) {
|
|
1789
|
+
const msg = error?.message || String(error);
|
|
1790
|
+
if (msg.includes("CodeNotApproved")) return "Agent code not approved. Complete KYA first.";
|
|
1791
|
+
if (msg.includes("0xda04aecc") || msg.includes("ExceedsMaxLtv"))
|
|
1792
|
+
return "ExceedsMaxLtv \u2014 collateral too low for this borrow amount.";
|
|
1793
|
+
if (msg.includes("0xfeca99cb") || msg.includes("ExecutionFailed"))
|
|
1794
|
+
return "ExecutionFailed \u2014 the inner contract call reverted.";
|
|
1795
|
+
if (msg.includes("0xa920ef9f"))
|
|
1796
|
+
return "PositionNotActive \u2014 no collateral deposited for this token.";
|
|
1797
|
+
if (msg.length > 200) return msg.slice(0, 200) + "...";
|
|
1798
|
+
return msg;
|
|
1799
|
+
}
|
|
1800
|
+
async function cmdInit(privateKey, agentId) {
|
|
1801
|
+
const rpcUrl = process.env.AGETHER_RPC_URL || DEFAULT_RPC;
|
|
1802
|
+
const backendUrl = process.env.AGETHER_BACKEND_URL || DEFAULT_BACKEND;
|
|
1803
|
+
let wallet;
|
|
1804
|
+
try {
|
|
1805
|
+
wallet = new import_ethers2.ethers.Wallet(privateKey);
|
|
1806
|
+
} catch {
|
|
1807
|
+
console.error("\u274C Invalid private key");
|
|
1808
|
+
process.exit(1);
|
|
1809
|
+
}
|
|
1810
|
+
const config = { privateKey, agentId: agentId || "0", rpcUrl, backendUrl };
|
|
1811
|
+
saveConfig(config);
|
|
1812
|
+
console.log("\u2705 Initialized Agether CLI");
|
|
1813
|
+
console.log(` Address: ${wallet.address}`);
|
|
1814
|
+
console.log(` RPC: ${rpcUrl}`);
|
|
1815
|
+
console.log(` Backend: ${backendUrl}`);
|
|
1816
|
+
console.log(` Config: ${CONFIG_PATH}`);
|
|
1817
|
+
}
|
|
1818
|
+
async function cmdRegister(name) {
|
|
1819
|
+
const config = requireConfig();
|
|
1820
|
+
const provider = new import_ethers2.ethers.JsonRpcProvider(config.rpcUrl);
|
|
1821
|
+
const signer = new import_ethers2.ethers.Wallet(config.privateKey, provider);
|
|
1822
|
+
const agentName = name || `Agent-${signer.address.slice(0, 8)}`;
|
|
1823
|
+
console.log(`\u{1F916} Registering agent: ${agentName}
|
|
1824
|
+
`);
|
|
1825
|
+
console.log(` Wallet: ${signer.address}`);
|
|
1826
|
+
console.log("\n [1/4] Fetching contract addresses...");
|
|
1827
|
+
let contracts;
|
|
1828
|
+
try {
|
|
1829
|
+
const statusResp = await apiGet(config.backendUrl, "/status");
|
|
1830
|
+
contracts = statusResp.contracts || {};
|
|
1831
|
+
console.log(" \u2713 Backend OK");
|
|
1832
|
+
} catch (e) {
|
|
1833
|
+
console.error(` \u274C Failed to reach backend: ${e.message}`);
|
|
1834
|
+
process.exit(1);
|
|
1835
|
+
}
|
|
1836
|
+
const registryAddr = contracts.agentRegistry || contracts.identityRegistry;
|
|
1837
|
+
const factoryAddr = contracts.safeAgentFactory || contracts.accountFactory;
|
|
1838
|
+
const validationAddr = contracts.validationRegistry;
|
|
1839
|
+
if (!registryAddr || !factoryAddr) {
|
|
1840
|
+
console.error(" \u274C Backend missing agentRegistry or safeAgentFactory");
|
|
1841
|
+
process.exit(1);
|
|
1842
|
+
}
|
|
1843
|
+
console.log(" [2/4] Registering on ERC-8004 IdentityRegistry...");
|
|
1844
|
+
const agentRegistry = new import_ethers2.ethers.Contract(registryAddr, ERC8004_ABI, signer);
|
|
1845
|
+
let agentId;
|
|
1846
|
+
if (config.agentId && config.agentId !== "0") {
|
|
1847
|
+
agentId = BigInt(config.agentId);
|
|
1848
|
+
try {
|
|
1849
|
+
const owner = await agentRegistry.ownerOf(agentId);
|
|
1850
|
+
if (owner.toLowerCase() === signer.address.toLowerCase()) {
|
|
1851
|
+
console.log(` \u2713 Already registered as Agent #${agentId}`);
|
|
1852
|
+
} else {
|
|
1853
|
+
console.error(" \u274C agentId does not belong to this wallet");
|
|
1854
|
+
process.exit(1);
|
|
1855
|
+
}
|
|
1856
|
+
} catch {
|
|
1857
|
+
console.error(` \u274C agentId ${agentId} does not exist on-chain`);
|
|
1858
|
+
process.exit(1);
|
|
1859
|
+
}
|
|
1860
|
+
} else {
|
|
1861
|
+
const existingBalance = await agentRegistry.balanceOf(signer.address);
|
|
1862
|
+
if (existingBalance > 0n) {
|
|
1863
|
+
console.log(` \u26A0 Wallet already owns ${existingBalance} token(s), minting another`);
|
|
1864
|
+
}
|
|
1865
|
+
try {
|
|
1866
|
+
const registrationFile = JSON.stringify({
|
|
1867
|
+
type: "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
|
|
1868
|
+
name: agentName,
|
|
1869
|
+
description: "AI agent registered via Agether CLI",
|
|
1870
|
+
active: true
|
|
1871
|
+
});
|
|
1872
|
+
const agentURI = `data:application/json;base64,${Buffer.from(registrationFile).toString("base64")}`;
|
|
1873
|
+
const tx = await agentRegistry["register(string)"](agentURI);
|
|
1874
|
+
const receipt = await waitForTx(tx);
|
|
1875
|
+
const transferTopic = import_ethers2.ethers.id("Transfer(address,address,uint256)");
|
|
1876
|
+
const transferLog = receipt.logs.find((log) => log.topics[0] === transferTopic);
|
|
1877
|
+
if (transferLog && transferLog.topics.length >= 4) {
|
|
1878
|
+
agentId = BigInt(transferLog.topics[3]);
|
|
1879
|
+
} else {
|
|
1880
|
+
console.error(" \u274C Could not parse agentId from receipt");
|
|
1881
|
+
process.exit(1);
|
|
1882
|
+
}
|
|
1883
|
+
console.log(` \u2713 Agent #${agentId} registered`);
|
|
1884
|
+
console.log(` TX: ${tx.hash}`);
|
|
1885
|
+
config.agentId = agentId.toString();
|
|
1886
|
+
saveConfig(config);
|
|
1887
|
+
} catch (e) {
|
|
1888
|
+
if (config.agentId && config.agentId !== "0") {
|
|
1889
|
+
agentId = BigInt(config.agentId);
|
|
1890
|
+
console.log(` Using existing Agent #${agentId}`);
|
|
1891
|
+
} else {
|
|
1892
|
+
const balance = await agentRegistry.balanceOf(signer.address);
|
|
1893
|
+
if (balance > 0n) {
|
|
1894
|
+
console.error(` Wallet owns ${balance} token(s) but agentId unknown.`);
|
|
1895
|
+
console.error(" Set manually: agether init <pk> --agent-id <id>");
|
|
1896
|
+
} else {
|
|
1897
|
+
console.error(` \u274C Registration failed: ${e.message}`);
|
|
1898
|
+
}
|
|
1899
|
+
process.exit(1);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
const network = await provider.getNetwork();
|
|
1904
|
+
const chainId = Number(network.chainId);
|
|
1905
|
+
if (chainId === 31337 || chainId === 1) {
|
|
1906
|
+
console.log(" [3/4] Minting test USDC (Hardhat fork)...");
|
|
1907
|
+
const deployerPk = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
|
|
1908
|
+
const deployer = new import_ethers2.ethers.Wallet(deployerPk, provider);
|
|
1909
|
+
const usdcAddr = contracts.usdc;
|
|
1910
|
+
if (usdcAddr) {
|
|
1911
|
+
try {
|
|
1912
|
+
const usdc = new import_ethers2.ethers.Contract(usdcAddr, MOCK_ERC20_ABI, deployer);
|
|
1913
|
+
const tx = await usdc.mint(signer.address, BigInt(5e10));
|
|
1914
|
+
await waitForTx(tx);
|
|
1915
|
+
console.log(" \u2713 Minted $50,000 USDC");
|
|
1916
|
+
} catch {
|
|
1917
|
+
console.log(" \u26A0 Mint failed (probably real network)");
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
} else {
|
|
1921
|
+
console.log(" [3/4] Skipping USDC mint (real network)");
|
|
1922
|
+
}
|
|
1923
|
+
console.log(" [4/4] Creating Safe account...");
|
|
1924
|
+
if (factoryAddr) {
|
|
1925
|
+
const factory = new import_ethers2.ethers.Contract(factoryAddr, ACCOUNT_FACTORY_ABI2, signer);
|
|
1926
|
+
try {
|
|
1927
|
+
const exists = await factory.accountExists(agentId);
|
|
1928
|
+
if (exists) {
|
|
1929
|
+
const addr = await factory.getAccount(agentId);
|
|
1930
|
+
console.log(` Already exists: ${addr}`);
|
|
1931
|
+
} else {
|
|
1932
|
+
const tx = await factory.createAccount(agentId);
|
|
1933
|
+
await waitForTx(tx);
|
|
1934
|
+
const addr = await factory.getAccount(agentId);
|
|
1935
|
+
console.log(` \u2713 Created: ${addr}`);
|
|
1936
|
+
console.log(` TX: ${tx.hash}`);
|
|
1937
|
+
}
|
|
1938
|
+
} catch (e) {
|
|
1939
|
+
console.error(` \u274C Failed: ${e.message}`);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
if (validationAddr) {
|
|
1943
|
+
const vr = new import_ethers2.ethers.Contract(validationAddr, VALIDATION_REGISTRY_ABI, provider);
|
|
1944
|
+
try {
|
|
1945
|
+
const approved = await vr.isAgentCodeApproved(agentId);
|
|
1946
|
+
console.log(`
|
|
1947
|
+
KYA Status: ${approved ? "\u2705 Approved" : "\u23F3 Pending"}`);
|
|
1948
|
+
} catch {
|
|
1949
|
+
console.log("\n KYA Status: \u26A0 Could not check");
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
console.log(`
|
|
1953
|
+
\u2705 Agent #${agentId} ready!`);
|
|
1954
|
+
console.log(` Config: ${CONFIG_PATH}`);
|
|
1955
|
+
console.log("\n Next steps:");
|
|
1956
|
+
console.log(" agether balance");
|
|
1957
|
+
console.log(" agether deposit --amount 0.05 --token WETH");
|
|
1958
|
+
console.log(" agether borrow --amount 100");
|
|
1959
|
+
}
|
|
1960
|
+
async function cmdBalance() {
|
|
1961
|
+
const config = requireConfig();
|
|
1962
|
+
const mc = await getMorphoClient(config);
|
|
1963
|
+
const balances = await mc.getBalances();
|
|
1964
|
+
console.log(`
|
|
1965
|
+
\u{1F4B0} Agent #${balances.agentId} Balances
|
|
1966
|
+
`);
|
|
1967
|
+
console.log(` EOA: ${balances.address}`);
|
|
1968
|
+
console.log(` ETH: ${parseFloat(balances.eth).toFixed(6)}`);
|
|
1969
|
+
console.log(` USDC: $${parseFloat(balances.usdc).toFixed(2)}`);
|
|
1970
|
+
if (balances.agentAccount) {
|
|
1971
|
+
console.log(`
|
|
1972
|
+
AgentAccount: ${balances.agentAccount.address}`);
|
|
1973
|
+
console.log(` ETH: ${parseFloat(balances.agentAccount.eth).toFixed(6)}`);
|
|
1974
|
+
console.log(` USDC: $${parseFloat(balances.agentAccount.usdc).toFixed(2)}`);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
async function cmdStatus() {
|
|
1978
|
+
const config = requireConfig();
|
|
1979
|
+
const mc = await getMorphoClient(config);
|
|
1980
|
+
console.log("\n\u{1F4CA} Morpho Positions\n");
|
|
1981
|
+
const status = await mc.getStatus();
|
|
1982
|
+
console.log(` Agent #${status.agentId}`);
|
|
1983
|
+
console.log(` Account: ${status.agentAccount}`);
|
|
1984
|
+
console.log(` Total Debt: $${parseFloat(status.totalDebt).toFixed(2)}`);
|
|
1985
|
+
if (status.positions.length === 0) {
|
|
1986
|
+
console.log("\n No active positions. Deposit collateral first:");
|
|
1987
|
+
console.log(" agether deposit --amount 0.05 --token WETH");
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
console.log("\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
1991
|
+
console.log(" \u2502 Token \u2502 Collateral \u2502 Debt (USDC) \u2502");
|
|
1992
|
+
console.log(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
1993
|
+
for (const p of status.positions) {
|
|
1994
|
+
const tok = p.collateralToken.padEnd(9);
|
|
1995
|
+
const col = parseFloat(p.collateral).toFixed(6).padStart(16);
|
|
1996
|
+
const debt = ("$" + parseFloat(p.debt).toFixed(2)).padStart(16);
|
|
1997
|
+
console.log(` \u2502 ${tok} \u2502 ${col} \u2502 ${debt} \u2502`);
|
|
1998
|
+
}
|
|
1999
|
+
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
2000
|
+
try {
|
|
2001
|
+
const score = await mc.getCreditScore();
|
|
2002
|
+
const { fresh, age } = await mc.isScoreFresh();
|
|
2003
|
+
const ageHrs = Number(age) / 3600;
|
|
2004
|
+
console.log(`
|
|
2005
|
+
Credit Score: ${score} ${fresh ? "\u2705" : `\u26A0\uFE0F stale (${ageHrs.toFixed(1)}h old)`}`);
|
|
2006
|
+
} catch {
|
|
2007
|
+
console.log("\n Credit Score: not yet computed");
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
async function cmdScore() {
|
|
2011
|
+
const config = requireConfig();
|
|
2012
|
+
console.log(`
|
|
2013
|
+
\u{1F4C8} Agent #${config.agentId} Score
|
|
2014
|
+
`);
|
|
2015
|
+
try {
|
|
2016
|
+
const data = await apiGet(config.backendUrl, `/score/${config.agentId}/current`);
|
|
2017
|
+
if (data.score !== void 0) {
|
|
2018
|
+
console.log(` On-chain Score: ${data.score}`);
|
|
2019
|
+
console.log(` Timestamp: ${data.timestamp ? new Date(Number(data.timestamp) * 1e3).toISOString() : "N/A"}`);
|
|
2020
|
+
console.log(` Signer: ${data.signer || "N/A"}`);
|
|
2021
|
+
console.log(` Fresh: ${data.fresh ? "\u2705" : "\u26A0\uFE0F stale"}`);
|
|
2022
|
+
} else {
|
|
2023
|
+
console.log(" No score on-chain yet.");
|
|
2024
|
+
}
|
|
2025
|
+
} catch {
|
|
2026
|
+
console.log(" Could not fetch current score.");
|
|
2027
|
+
}
|
|
2028
|
+
console.log("\n To compute a fresh score (x402-gated, costs USDC):");
|
|
2029
|
+
console.log(` agether x402 ${config.backendUrl}/score/${config.agentId}`);
|
|
2030
|
+
}
|
|
2031
|
+
async function cmdMarkets() {
|
|
2032
|
+
const config = requireConfig();
|
|
2033
|
+
const mc = await getMorphoClient(config);
|
|
2034
|
+
console.log("\n\u{1F4C8} Morpho Blue Markets (Base USDC)\n");
|
|
2035
|
+
const markets = await mc.getMarkets();
|
|
2036
|
+
if (markets.length === 0) {
|
|
2037
|
+
console.log(" No markets found. API may be unavailable.");
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
2041
|
+
console.log(" \u2502 Collateral \u2502 LLTV \u2502 Util % \u2502 Supply (USDC) \u2502");
|
|
2042
|
+
console.log(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
2043
|
+
for (const m of markets.slice(0, 15)) {
|
|
2044
|
+
const col = (m.collateralAsset?.symbol || "N/A").padEnd(13);
|
|
2045
|
+
const lltv = (Number(m.lltv) / 1e18 * 100).toFixed(0).padStart(6) + "%";
|
|
2046
|
+
const util = (m.utilization * 100).toFixed(1).padStart(8) + "%";
|
|
2047
|
+
const supply = ("$" + (Number(m.totalSupplyAssets) / 1e6).toLocaleString()).padStart(16);
|
|
2048
|
+
console.log(` \u2502 ${col} \u2502 ${lltv} \u2502 ${util} \u2502 ${supply} \u2502`);
|
|
2049
|
+
}
|
|
2050
|
+
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
2051
|
+
}
|
|
2052
|
+
async function cmdDeposit(amount, token) {
|
|
2053
|
+
const config = requireConfig();
|
|
2054
|
+
const mc = await getMorphoClient(config);
|
|
2055
|
+
console.log(`
|
|
2056
|
+
\u{1F4B0} Depositing ${amount} ${token} as collateral...
|
|
2057
|
+
`);
|
|
2058
|
+
try {
|
|
2059
|
+
const result = await mc.supplyCollateral(token, amount);
|
|
2060
|
+
console.log(`\u2705 Deposited ${amount} ${token}`);
|
|
2061
|
+
console.log(` AgentAccount: ${result.agentAccount}`);
|
|
2062
|
+
console.log(` TX: ${result.tx}`);
|
|
2063
|
+
} catch (e) {
|
|
2064
|
+
console.error(`\u274C ${decodeError(e)}`);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
async function cmdBorrow(amount, token) {
|
|
2068
|
+
const config = requireConfig();
|
|
2069
|
+
const mc = await getMorphoClient(config);
|
|
2070
|
+
console.log(`
|
|
2071
|
+
\u{1F4B8} Borrowing $${amount} USDC...
|
|
2072
|
+
`);
|
|
2073
|
+
try {
|
|
2074
|
+
const result = await mc.borrow(amount, token);
|
|
2075
|
+
console.log(`\u2705 Borrowed $${amount} USDC`);
|
|
2076
|
+
console.log(` Collateral: ${result.collateralToken}`);
|
|
2077
|
+
console.log(` AgentAccount: ${result.agentAccount}`);
|
|
2078
|
+
console.log(` TX: ${result.tx}`);
|
|
2079
|
+
} catch (e) {
|
|
2080
|
+
console.error(`\u274C ${decodeError(e)}`);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
async function cmdDepositAndBorrow(collateralAmount, token, borrowAmount) {
|
|
2084
|
+
const config = requireConfig();
|
|
2085
|
+
const mc = await getMorphoClient(config);
|
|
2086
|
+
console.log(`
|
|
2087
|
+
\uFFFD\uFFFD Depositing ${collateralAmount} ${token} + Borrowing $${borrowAmount}...
|
|
2088
|
+
`);
|
|
2089
|
+
try {
|
|
2090
|
+
const result = await mc.depositAndBorrow(token, collateralAmount, borrowAmount);
|
|
2091
|
+
console.log(`\u2705 Deposited ${collateralAmount} ${token} & Borrowed $${borrowAmount} USDC`);
|
|
2092
|
+
console.log(` AgentAccount: ${result.agentAccount}`);
|
|
2093
|
+
console.log(` TX: ${result.tx}`);
|
|
2094
|
+
} catch (e) {
|
|
2095
|
+
console.error(`\u274C ${decodeError(e)}`);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
async function cmdRepay(amount, token) {
|
|
2099
|
+
const config = requireConfig();
|
|
2100
|
+
const mc = await getMorphoClient(config);
|
|
2101
|
+
console.log(`
|
|
2102
|
+
\u{1F4B3} Repaying $${amount} USDC...
|
|
2103
|
+
`);
|
|
2104
|
+
try {
|
|
2105
|
+
const result = await mc.repay(amount, token);
|
|
2106
|
+
console.log(`\u2705 Repaid $${amount}`);
|
|
2107
|
+
console.log(` Remaining debt: $${parseFloat(result.remainingDebt).toFixed(2)}`);
|
|
2108
|
+
console.log(` TX: ${result.tx}`);
|
|
2109
|
+
} catch (e) {
|
|
2110
|
+
console.error(`\u274C ${decodeError(e)}`);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
async function cmdWithdraw(amount, token) {
|
|
2114
|
+
const config = requireConfig();
|
|
2115
|
+
const mc = await getMorphoClient(config);
|
|
2116
|
+
console.log(`
|
|
2117
|
+
\u{1F4E4} Withdrawing ${amount} ${token}...
|
|
2118
|
+
`);
|
|
2119
|
+
try {
|
|
2120
|
+
const result = await mc.withdrawCollateral(token, amount);
|
|
2121
|
+
console.log(`\u2705 Withdrew ${result.amount} ${token}`);
|
|
2122
|
+
console.log(` Remaining collateral: ${result.remainingCollateral}`);
|
|
2123
|
+
console.log(` Destination: ${result.destination}`);
|
|
2124
|
+
console.log(` TX: ${result.tx}`);
|
|
2125
|
+
} catch (e) {
|
|
2126
|
+
console.error(`\u274C ${decodeError(e)}`);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
async function cmdSponsor(amount, token, agentId, address) {
|
|
2130
|
+
const config = requireConfig();
|
|
2131
|
+
const mc = await getMorphoClient(config);
|
|
2132
|
+
const target = agentId ? { agentId } : { address };
|
|
2133
|
+
console.log(`
|
|
2134
|
+
\u{1F381} Sponsoring ${amount} ${token} to ${agentId || address}...
|
|
2135
|
+
`);
|
|
2136
|
+
try {
|
|
2137
|
+
const result = await mc.sponsor(target, token, amount);
|
|
2138
|
+
console.log(`\u2705 Sponsored ${amount} ${token}`);
|
|
2139
|
+
console.log(` Target: ${result.targetAccount}`);
|
|
2140
|
+
console.log(` TX: ${result.tx}`);
|
|
2141
|
+
} catch (e) {
|
|
2142
|
+
console.error(`\u274C ${decodeError(e)}`);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
async function cmdFund(amount) {
|
|
2146
|
+
const config = requireConfig();
|
|
2147
|
+
const mc = await getMorphoClient(config);
|
|
2148
|
+
console.log(`
|
|
2149
|
+
\u{1F4B5} Funding AgentAccount with $${amount} USDC...
|
|
2150
|
+
`);
|
|
2151
|
+
try {
|
|
2152
|
+
const result = await mc.fundAccount(amount);
|
|
2153
|
+
console.log(`\u2705 Funded $${amount} USDC`);
|
|
2154
|
+
console.log(` AgentAccount: ${result.agentAccount}`);
|
|
2155
|
+
console.log(` TX: ${result.tx}`);
|
|
2156
|
+
} catch (e) {
|
|
2157
|
+
console.error(`\u274C ${decodeError(e)}`);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
async function cmdX402Call(url, method = "GET", body) {
|
|
2161
|
+
const config = requireConfig();
|
|
2162
|
+
console.log("\n\u{1F510} x402 Paid API Call\n");
|
|
2163
|
+
const x402 = await getX402Client(config);
|
|
2164
|
+
console.log(` Wallet: ${new import_ethers2.ethers.Wallet(config.privateKey).address}`);
|
|
2165
|
+
console.log(`
|
|
2166
|
+
\u{1F4E1} ${method} ${url}`);
|
|
2167
|
+
if (body) console.log(`\u{1F4E6} Body: ${body}`);
|
|
2168
|
+
try {
|
|
2169
|
+
let result;
|
|
2170
|
+
if (method === "POST" && body) {
|
|
2171
|
+
result = await x402.post(url, JSON.parse(body));
|
|
2172
|
+
} else {
|
|
2173
|
+
result = await x402.get(url);
|
|
2174
|
+
}
|
|
2175
|
+
if (result.success) {
|
|
2176
|
+
console.log("\n\u2705 Success!");
|
|
2177
|
+
if (result.paymentInfo) {
|
|
2178
|
+
console.log(`\u{1F4B0} Paid: ${result.paymentInfo.amount} ${result.paymentInfo.asset} on ${result.paymentInfo.network}`);
|
|
2179
|
+
if (result.paymentInfo.txHash) console.log(`\u{1F4DC} TX: ${result.paymentInfo.txHash}`);
|
|
2180
|
+
}
|
|
2181
|
+
console.log("\n\u{1F4C4} Response:");
|
|
2182
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
2183
|
+
} else {
|
|
2184
|
+
console.error(`
|
|
2185
|
+
\u274C Failed: ${result.error}`);
|
|
2186
|
+
}
|
|
2187
|
+
} catch (e) {
|
|
2188
|
+
console.error(`\u274C Error: ${e.message}`);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
function cmdHelp() {
|
|
2192
|
+
console.log(`
|
|
2193
|
+
\u{1F3E6} Agether CLI \u2014 Direct Morpho Blue Credit for AI Agents (Safe + Safe7579)
|
|
2194
|
+
|
|
2195
|
+
USAGE:
|
|
2196
|
+
agether <command> [options]
|
|
2197
|
+
|
|
2198
|
+
SETUP:
|
|
2199
|
+
init <private-key> [--agent-id <id>] Initialize with private key
|
|
2200
|
+
register [--name <n>] Register ERC-8004 + create Safe account
|
|
2201
|
+
|
|
2202
|
+
INFO:
|
|
2203
|
+
balance Check ETH + USDC balances
|
|
2204
|
+
status Show Morpho positions & credit score
|
|
2205
|
+
score Get credit score
|
|
2206
|
+
markets List Morpho Blue USDC markets
|
|
2207
|
+
|
|
2208
|
+
MORPHO LENDING:
|
|
2209
|
+
deposit --amount <n> --token <t> Deposit collateral (WETH, wstETH, cbETH)
|
|
2210
|
+
borrow --amount <usd> [--token <t>] Borrow USDC against collateral
|
|
2211
|
+
deposit-and-borrow --amount <n> --token <t> --borrow <usd>
|
|
2212
|
+
Deposit + borrow in one batched tx
|
|
2213
|
+
repay --amount <usd> [--token <t>] Repay borrowed USDC
|
|
2214
|
+
withdraw --amount <n> --token <t> Withdraw collateral (use 'all' for max)
|
|
2215
|
+
sponsor --amount <n> --token <t> --agent-id <id> Send collateral to another agent
|
|
2216
|
+
fund --amount <usd> Transfer USDC from EOA to Safe account
|
|
2217
|
+
|
|
2218
|
+
x402 PAYMENTS:
|
|
2219
|
+
x402 <url> [--method GET|POST] [--body <json>] Make a paid API call
|
|
2220
|
+
|
|
2221
|
+
ENVIRONMENT:
|
|
2222
|
+
AGETHER_RPC_URL RPC endpoint (default: ${DEFAULT_RPC})
|
|
2223
|
+
AGETHER_BACKEND_URL Backend URL (default: ${DEFAULT_BACKEND})
|
|
2224
|
+
|
|
2225
|
+
EXAMPLE FLOW:
|
|
2226
|
+
agether init 0xYOUR_PRIVATE_KEY
|
|
2227
|
+
agether register --name "MyAgent"
|
|
2228
|
+
agether deposit --amount 0.05 --token WETH # ~$125 collateral
|
|
2229
|
+
agether borrow --amount 50 # Borrow $50 USDC
|
|
2230
|
+
agether status # Check positions
|
|
2231
|
+
agether repay --amount 50 # Repay when done
|
|
2232
|
+
agether withdraw --amount all --token WETH # Withdraw collateral
|
|
2233
|
+
`);
|
|
2234
|
+
}
|
|
2235
|
+
function parseArgs(args) {
|
|
2236
|
+
const command = args[0] || "help";
|
|
2237
|
+
const positional = [];
|
|
2238
|
+
const options = {};
|
|
2239
|
+
for (let i = 1; i < args.length; i++) {
|
|
2240
|
+
const arg = args[i];
|
|
2241
|
+
if (arg.startsWith("--")) {
|
|
2242
|
+
const key = arg.slice(2);
|
|
2243
|
+
const next = args[i + 1];
|
|
2244
|
+
if (next && !next.startsWith("--")) {
|
|
2245
|
+
options[key] = next;
|
|
2246
|
+
i++;
|
|
2247
|
+
} else {
|
|
2248
|
+
options[key] = true;
|
|
2249
|
+
}
|
|
2250
|
+
} else {
|
|
2251
|
+
positional.push(arg);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
return { command, positional, options };
|
|
2255
|
+
}
|
|
2256
|
+
async function main() {
|
|
2257
|
+
const { command, positional, options } = parseArgs(process.argv.slice(2));
|
|
2258
|
+
try {
|
|
2259
|
+
switch (command) {
|
|
2260
|
+
case "init":
|
|
2261
|
+
if (!positional[0]) {
|
|
2262
|
+
console.error("\u274C Private key required: agether init <private-key>");
|
|
2263
|
+
process.exit(1);
|
|
2264
|
+
}
|
|
2265
|
+
await cmdInit(positional[0], options["agent-id"]);
|
|
2266
|
+
break;
|
|
2267
|
+
case "register":
|
|
2268
|
+
await cmdRegister(options.name);
|
|
2269
|
+
break;
|
|
2270
|
+
case "balance":
|
|
2271
|
+
await cmdBalance();
|
|
2272
|
+
break;
|
|
2273
|
+
case "status":
|
|
2274
|
+
await cmdStatus();
|
|
2275
|
+
break;
|
|
2276
|
+
case "score":
|
|
2277
|
+
await cmdScore();
|
|
2278
|
+
break;
|
|
2279
|
+
case "markets":
|
|
2280
|
+
await cmdMarkets();
|
|
2281
|
+
break;
|
|
2282
|
+
case "deposit":
|
|
2283
|
+
if (!options.amount || !options.token) {
|
|
2284
|
+
console.error("\u274C --amount and --token required");
|
|
2285
|
+
console.error(" agether deposit --amount 0.05 --token WETH");
|
|
2286
|
+
process.exit(1);
|
|
2287
|
+
}
|
|
2288
|
+
await cmdDeposit(options.amount, options.token);
|
|
2289
|
+
break;
|
|
2290
|
+
case "borrow":
|
|
2291
|
+
if (!options.amount) {
|
|
2292
|
+
console.error("\u274C --amount required (in USD)");
|
|
2293
|
+
console.error(" agether borrow --amount 100");
|
|
2294
|
+
process.exit(1);
|
|
2295
|
+
}
|
|
2296
|
+
await cmdBorrow(options.amount, options.token);
|
|
2297
|
+
break;
|
|
2298
|
+
case "deposit-and-borrow":
|
|
2299
|
+
if (!options.amount || !options.token || !options.borrow) {
|
|
2300
|
+
console.error("\u274C --amount, --token, and --borrow required");
|
|
2301
|
+
console.error(" agether deposit-and-borrow --amount 0.05 --token WETH --borrow 100");
|
|
2302
|
+
process.exit(1);
|
|
2303
|
+
}
|
|
2304
|
+
await cmdDepositAndBorrow(
|
|
2305
|
+
options.amount,
|
|
2306
|
+
options.token,
|
|
2307
|
+
options.borrow
|
|
2308
|
+
);
|
|
2309
|
+
break;
|
|
2310
|
+
case "repay":
|
|
2311
|
+
if (!options.amount) {
|
|
2312
|
+
console.error("\u274C --amount required");
|
|
2313
|
+
process.exit(1);
|
|
2314
|
+
}
|
|
2315
|
+
await cmdRepay(options.amount, options.token);
|
|
2316
|
+
break;
|
|
2317
|
+
case "withdraw":
|
|
2318
|
+
if (!options.amount || !options.token) {
|
|
2319
|
+
console.error("\u274C --amount and --token required");
|
|
2320
|
+
console.error(" agether withdraw --amount 0.05 --token WETH");
|
|
2321
|
+
console.error(" agether withdraw --amount all --token WETH");
|
|
2322
|
+
process.exit(1);
|
|
2323
|
+
}
|
|
2324
|
+
await cmdWithdraw(options.amount, options.token);
|
|
2325
|
+
break;
|
|
2326
|
+
case "sponsor":
|
|
2327
|
+
if (!options.amount || !options.token) {
|
|
2328
|
+
console.error("\u274C --amount and --token required, plus --agent-id or --address");
|
|
2329
|
+
process.exit(1);
|
|
2330
|
+
}
|
|
2331
|
+
if (!options["agent-id"] && !options.address) {
|
|
2332
|
+
console.error("\u274C --agent-id or --address required");
|
|
2333
|
+
process.exit(1);
|
|
2334
|
+
}
|
|
2335
|
+
await cmdSponsor(
|
|
2336
|
+
options.amount,
|
|
2337
|
+
options.token,
|
|
2338
|
+
options["agent-id"],
|
|
2339
|
+
options.address
|
|
2340
|
+
);
|
|
2341
|
+
break;
|
|
2342
|
+
case "fund":
|
|
2343
|
+
if (!options.amount) {
|
|
2344
|
+
console.error("\u274C --amount required (USDC)");
|
|
2345
|
+
process.exit(1);
|
|
2346
|
+
}
|
|
2347
|
+
await cmdFund(options.amount);
|
|
2348
|
+
break;
|
|
2349
|
+
case "x402":
|
|
2350
|
+
if (!positional[0]) {
|
|
2351
|
+
console.error("\u274C URL required: agether x402 <url>");
|
|
2352
|
+
process.exit(1);
|
|
2353
|
+
}
|
|
2354
|
+
await cmdX402Call(
|
|
2355
|
+
positional[0],
|
|
2356
|
+
options.method || "GET",
|
|
2357
|
+
options.body
|
|
2358
|
+
);
|
|
2359
|
+
break;
|
|
2360
|
+
case "help":
|
|
2361
|
+
case "--help":
|
|
2362
|
+
case "-h":
|
|
2363
|
+
cmdHelp();
|
|
2364
|
+
break;
|
|
2365
|
+
default:
|
|
2366
|
+
console.error(`\u274C Unknown command: ${command}`);
|
|
2367
|
+
cmdHelp();
|
|
2368
|
+
process.exit(1);
|
|
2369
|
+
}
|
|
2370
|
+
} catch (e) {
|
|
2371
|
+
console.error(`\u274C Error: ${e.message}`);
|
|
2372
|
+
process.exit(1);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
main();
|