@agether/sdk 2.13.0 → 2.14.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.d.ts +29 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +252 -21
- package/dist/clients/AgentIdentityClient.d.ts +200 -0
- package/dist/clients/AgentIdentityClient.d.ts.map +1 -0
- package/dist/clients/AgentIdentityClient.js +351 -0
- package/dist/clients/AgetherClient.d.ts +242 -0
- package/dist/clients/AgetherClient.d.ts.map +1 -0
- package/dist/clients/AgetherClient.js +736 -0
- package/dist/clients/MorphoClient.d.ts +572 -0
- package/dist/clients/MorphoClient.d.ts.map +1 -0
- package/dist/clients/MorphoClient.js +1974 -0
- package/dist/clients/ScoringClient.d.ts +103 -0
- package/dist/clients/ScoringClient.d.ts.map +1 -0
- package/dist/clients/ScoringClient.js +112 -0
- package/dist/clients/X402Client.d.ts +198 -0
- package/dist/clients/X402Client.d.ts.map +1 -0
- package/dist/clients/X402Client.js +438 -0
- package/dist/index.d.mts +83 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +252 -21
- package/dist/index.mjs +252 -21
- package/dist/types/index.d.ts +132 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +46 -0
- package/dist/utils/abis.d.ts +29 -0
- package/dist/utils/abis.d.ts.map +1 -0
- package/dist/utils/abis.js +138 -0
- package/dist/utils/config.d.ts +36 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +168 -0
- package/dist/utils/format.d.ts +44 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +75 -0
- package/package.json +1 -1
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgetherClient — Primary protocol client for AI agent operations
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Registration (mint ERC-8004 NFT + deploy Safe account)
|
|
6
|
+
* - Account management (create, query, fund)
|
|
7
|
+
* - Balance queries (EOA + Safe, including collateral tokens)
|
|
8
|
+
* - Withdrawals from Safe to EOA (ERC-20 and ETH, via UserOps)
|
|
9
|
+
* - Agent-to-agent sponsorship
|
|
10
|
+
* - Identity checks (KYA, existence, ownership)
|
|
11
|
+
* - Credit scoring queries
|
|
12
|
+
*
|
|
13
|
+
* For Morpho Blue lending operations, use MorphoClient.
|
|
14
|
+
*
|
|
15
|
+
* Architecture (v2 — Safe + Safe7579):
|
|
16
|
+
* 1. Agent registers via ERC-8004 → gets agentId
|
|
17
|
+
* 2. Agether4337Factory.createAccount(agentId) → Safe proxy with Safe7579 adapter
|
|
18
|
+
* 3. Withdrawals from Safe execute via ERC-4337 UserOps
|
|
19
|
+
* 4. Use MorphoClient for lending (supply, borrow, repay, withdraw collateral)
|
|
20
|
+
*/
|
|
21
|
+
import { ethers, Contract } from 'ethers';
|
|
22
|
+
import { AgetherError, ChainId, } from '../types';
|
|
23
|
+
import { AGETHER_4337_FACTORY_ABI, IDENTITY_REGISTRY_ABI, AGETHER_8004_SCORER_ABI, AGETHER_8004_VALIDATION_MODULE_ABI, ERC20_ABI, ENTRYPOINT_V07_ABI, SAFE7579_ACCOUNT_ABI, } from '../utils/abis';
|
|
24
|
+
import { getDefaultConfig } from '../utils/config';
|
|
25
|
+
// ── ERC-7579 Execution Mode Constants ──
|
|
26
|
+
/** Single call: callType=0x00, execType=0x00, rest zero-padded to 32 bytes */
|
|
27
|
+
const MODE_SINGLE = '0x0000000000000000000000000000000000000000000000000000000000000000';
|
|
28
|
+
// ── ERC20 interface for encoding ──
|
|
29
|
+
const erc20Iface = new ethers.Interface(ERC20_ABI);
|
|
30
|
+
// ── Well-known tokens per chain ──
|
|
31
|
+
const KNOWN_TOKENS = {
|
|
32
|
+
[ChainId.Base]: {
|
|
33
|
+
WETH: { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH', decimals: 18 },
|
|
34
|
+
wstETH: { address: '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452', symbol: 'wstETH', decimals: 18 },
|
|
35
|
+
cbETH: { address: '0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22', symbol: 'cbETH', decimals: 18 },
|
|
36
|
+
},
|
|
37
|
+
[ChainId.Ethereum]: {
|
|
38
|
+
WETH: { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', symbol: 'WETH', decimals: 18 },
|
|
39
|
+
wstETH: { address: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0', symbol: 'wstETH', decimals: 18 },
|
|
40
|
+
cbETH: { address: '0xBe9895146f7AF43049ca1c1AE358B0541Ea49704', symbol: 'cbETH', decimals: 18 },
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
// ── Client ──
|
|
44
|
+
export class AgetherClient {
|
|
45
|
+
constructor(options) {
|
|
46
|
+
this.config = options.config;
|
|
47
|
+
this.signer = options.signer;
|
|
48
|
+
this.agentId = options.agentId;
|
|
49
|
+
this._rpcUrl = options.config.rpcUrl;
|
|
50
|
+
if (options._privateKey) {
|
|
51
|
+
this._privateKey = options._privateKey;
|
|
52
|
+
this._useExternalSigner = false;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
this._useExternalSigner = true;
|
|
56
|
+
}
|
|
57
|
+
const provider = options.signer.provider;
|
|
58
|
+
if (!provider)
|
|
59
|
+
throw new AgetherError('Signer must have a provider', 'NO_PROVIDER');
|
|
60
|
+
// Cache EOA address if synchronously available
|
|
61
|
+
if ('address' in options.signer && typeof options.signer.address === 'string') {
|
|
62
|
+
this._eoaAddress = options.signer.address;
|
|
63
|
+
}
|
|
64
|
+
const c = options.config.contracts;
|
|
65
|
+
this.agether4337Factory = new Contract(c.agether4337Factory, AGETHER_4337_FACTORY_ABI, options.signer);
|
|
66
|
+
this.identityRegistry = new Contract(c.identityRegistry, IDENTITY_REGISTRY_ABI, options.signer);
|
|
67
|
+
this.agether8004Scorer = new Contract(c.agether8004Scorer, AGETHER_8004_SCORER_ABI, provider);
|
|
68
|
+
this.validationModule = new Contract(c.erc8004ValidationModule, AGETHER_8004_VALIDATION_MODULE_ABI, provider);
|
|
69
|
+
this.entryPoint = new Contract(c.entryPoint, ENTRYPOINT_V07_ABI, options.signer);
|
|
70
|
+
}
|
|
71
|
+
static fromPrivateKey(privateKey, agentIdOrChain, chainIdOrConfig) {
|
|
72
|
+
let agentId;
|
|
73
|
+
let config;
|
|
74
|
+
if (typeof agentIdOrChain === 'bigint') {
|
|
75
|
+
// (pk, agentId, chainIdOrConfig)
|
|
76
|
+
agentId = agentIdOrChain;
|
|
77
|
+
config =
|
|
78
|
+
typeof chainIdOrConfig === 'number'
|
|
79
|
+
? getDefaultConfig(chainIdOrConfig)
|
|
80
|
+
: chainIdOrConfig;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// (pk, chainIdOrConfig) — no agentId
|
|
84
|
+
config =
|
|
85
|
+
typeof agentIdOrChain === 'number'
|
|
86
|
+
? getDefaultConfig(agentIdOrChain)
|
|
87
|
+
: agentIdOrChain;
|
|
88
|
+
}
|
|
89
|
+
const provider = new ethers.JsonRpcProvider(config.rpcUrl);
|
|
90
|
+
const signer = new ethers.Wallet(privateKey, provider);
|
|
91
|
+
return new AgetherClient({ config, signer, agentId, _privateKey: privateKey });
|
|
92
|
+
}
|
|
93
|
+
static async fromSigner(signer, agentIdOrChain, chainIdOrConfig) {
|
|
94
|
+
if (!signer.provider) {
|
|
95
|
+
throw new AgetherError('Signer must be connected to a provider', 'NO_PROVIDER');
|
|
96
|
+
}
|
|
97
|
+
let agentId;
|
|
98
|
+
let config;
|
|
99
|
+
if (typeof agentIdOrChain === 'bigint') {
|
|
100
|
+
agentId = agentIdOrChain;
|
|
101
|
+
config =
|
|
102
|
+
typeof chainIdOrConfig === 'number'
|
|
103
|
+
? getDefaultConfig(chainIdOrConfig)
|
|
104
|
+
: chainIdOrConfig;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
config =
|
|
108
|
+
typeof agentIdOrChain === 'number'
|
|
109
|
+
? getDefaultConfig(agentIdOrChain)
|
|
110
|
+
: agentIdOrChain;
|
|
111
|
+
}
|
|
112
|
+
// Validate signer is on the correct chain
|
|
113
|
+
const network = await signer.provider.getNetwork();
|
|
114
|
+
const actualChainId = Number(network.chainId);
|
|
115
|
+
if (actualChainId !== config.chainId) {
|
|
116
|
+
throw new AgetherError(`Chain mismatch: signer is on chain ${actualChainId} but config expects chain ${config.chainId}`, 'CHAIN_MISMATCH');
|
|
117
|
+
}
|
|
118
|
+
return new AgetherClient({ config, signer, agentId });
|
|
119
|
+
}
|
|
120
|
+
// ════════════════════════════════════════════════════════
|
|
121
|
+
// Registration
|
|
122
|
+
// ════════════════════════════════════════════════════════
|
|
123
|
+
/**
|
|
124
|
+
* Register: create ERC-8004 identity + Safe account in one flow.
|
|
125
|
+
* If already registered, returns existing state.
|
|
126
|
+
*
|
|
127
|
+
* Sets `this.agentId` on success so subsequent operations work immediately.
|
|
128
|
+
*
|
|
129
|
+
* @param options.name Agent display name (stored in on-chain metadata URI)
|
|
130
|
+
* @param options.description Agent description (defaults to 'AI agent registered via @agether/sdk')
|
|
131
|
+
*/
|
|
132
|
+
async register(options) {
|
|
133
|
+
const eoaAddr = await this._getSignerAddress();
|
|
134
|
+
// Check if we already have an agentId and account exists
|
|
135
|
+
if (this.agentId !== undefined) {
|
|
136
|
+
const exists = await this.agether4337Factory.accountExists(this.agentId);
|
|
137
|
+
if (exists) {
|
|
138
|
+
const acct = await this.agether4337Factory.getAccount(this.agentId);
|
|
139
|
+
this.accountAddress = acct;
|
|
140
|
+
const kyaRequired = await this.isKyaRequired();
|
|
141
|
+
return {
|
|
142
|
+
agentId: this.agentId.toString(),
|
|
143
|
+
address: eoaAddr,
|
|
144
|
+
agentAccount: acct,
|
|
145
|
+
alreadyRegistered: true,
|
|
146
|
+
kyaRequired,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Mint new identity or reuse existing
|
|
151
|
+
let agentId;
|
|
152
|
+
if (this.agentId !== undefined) {
|
|
153
|
+
// We have an agentId but no account yet — check if identity exists
|
|
154
|
+
const balance = await this.identityRegistry.balanceOf(eoaAddr);
|
|
155
|
+
if (balance > 0n) {
|
|
156
|
+
agentId = this.agentId;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
agentId = await this._mintNewIdentity(options?.name, options?.description);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// No agentId — always mint a new identity
|
|
164
|
+
agentId = await this._mintNewIdentity(options?.name, options?.description);
|
|
165
|
+
}
|
|
166
|
+
this.agentId = agentId;
|
|
167
|
+
// Create Safe account if needed
|
|
168
|
+
const acctExists = await this.agether4337Factory.accountExists(agentId);
|
|
169
|
+
let txHash;
|
|
170
|
+
if (!acctExists) {
|
|
171
|
+
const tx = await this.agether4337Factory.createAccount(agentId);
|
|
172
|
+
const receipt = await tx.wait();
|
|
173
|
+
this._refreshSigner();
|
|
174
|
+
txHash = receipt.hash;
|
|
175
|
+
}
|
|
176
|
+
const acctAddr = await this.agether4337Factory.getAccount(agentId);
|
|
177
|
+
this.accountAddress = acctAddr;
|
|
178
|
+
// Update on-chain URI with the Safe account address and real agentId
|
|
179
|
+
if (!acctExists) {
|
|
180
|
+
const updatedMeta = JSON.stringify({
|
|
181
|
+
type: 'https://eips.ethereum.org/EIPS/eip-8004#registration-v1',
|
|
182
|
+
name: options?.name || 'Unnamed Agent',
|
|
183
|
+
description: options?.description || 'AI agent registered via @agether/sdk',
|
|
184
|
+
active: true,
|
|
185
|
+
wallet: `eip155:${this.config.chainId}:${acctAddr}`,
|
|
186
|
+
registrations: [{
|
|
187
|
+
agentId: Number(agentId),
|
|
188
|
+
agentRegistry: `eip155:${this.config.chainId}:${this.config.contracts.identityRegistry}`,
|
|
189
|
+
}],
|
|
190
|
+
});
|
|
191
|
+
const finalURI = `data:application/json;base64,${Buffer.from(updatedMeta).toString('base64')}`;
|
|
192
|
+
const uriTx = await this.identityRegistry.setAgentURI(agentId, finalURI);
|
|
193
|
+
await uriTx.wait();
|
|
194
|
+
this._refreshSigner();
|
|
195
|
+
}
|
|
196
|
+
const kyaRequired = await this.isKyaRequired();
|
|
197
|
+
return {
|
|
198
|
+
agentId: agentId.toString(),
|
|
199
|
+
address: eoaAddr,
|
|
200
|
+
agentAccount: acctAddr,
|
|
201
|
+
alreadyRegistered: acctExists,
|
|
202
|
+
kyaRequired,
|
|
203
|
+
tx: txHash,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** Mint a new ERC-8004 identity and return the agentId.
|
|
207
|
+
* Builds a JSON metadata document per ERC-8004 spec and encodes it as a data: URI.
|
|
208
|
+
*/
|
|
209
|
+
async _mintNewIdentity(name, description) {
|
|
210
|
+
// Build ERC-8004 registration metadata
|
|
211
|
+
const registrationFile = JSON.stringify({
|
|
212
|
+
type: 'https://eips.ethereum.org/EIPS/eip-8004#registration-v1',
|
|
213
|
+
name: name || 'Agether Agent',
|
|
214
|
+
description: description || 'AI agent registered via @agether/sdk',
|
|
215
|
+
active: true,
|
|
216
|
+
registrations: [{
|
|
217
|
+
agentId: 0,
|
|
218
|
+
agentRegistry: `eip155:${this.config.chainId}:${this.config.contracts.identityRegistry}`,
|
|
219
|
+
}],
|
|
220
|
+
});
|
|
221
|
+
const agentURI = `data:application/json;base64,${Buffer.from(registrationFile).toString('base64')}`;
|
|
222
|
+
const regTx = await this.identityRegistry['register(string)'](agentURI);
|
|
223
|
+
const regReceipt = await regTx.wait();
|
|
224
|
+
this._refreshSigner();
|
|
225
|
+
let agentId = 0n;
|
|
226
|
+
for (const log of regReceipt.logs) {
|
|
227
|
+
try {
|
|
228
|
+
const parsed = this.identityRegistry.interface.parseLog({
|
|
229
|
+
topics: log.topics,
|
|
230
|
+
data: log.data,
|
|
231
|
+
});
|
|
232
|
+
if (parsed?.name === 'Transfer') {
|
|
233
|
+
agentId = parsed.args[2];
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
console.warn('[agether] parseLog skip:', e instanceof Error ? e.message : e);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (agentId === 0n) {
|
|
243
|
+
throw new AgetherError('Failed to parse agentId from registration', 'PARSE_ERROR');
|
|
244
|
+
}
|
|
245
|
+
return agentId;
|
|
246
|
+
}
|
|
247
|
+
// ════════════════════════════════════════════════════════
|
|
248
|
+
// Account Management
|
|
249
|
+
// ════════════════════════════════════════════════════════
|
|
250
|
+
/**
|
|
251
|
+
* Deploy a Safe account smart wallet for the agent.
|
|
252
|
+
* The caller must own the ERC-8004 NFT.
|
|
253
|
+
*/
|
|
254
|
+
async createAccount() {
|
|
255
|
+
const id = this._requireAgentId();
|
|
256
|
+
const tx = await this.agether4337Factory.createAccount(id);
|
|
257
|
+
const receipt = await tx.wait();
|
|
258
|
+
this._refreshSigner();
|
|
259
|
+
const event = receipt.logs
|
|
260
|
+
.map((log) => {
|
|
261
|
+
try {
|
|
262
|
+
return this.agether4337Factory.interface.parseLog(log);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
.find((e) => e?.name === 'AccountCreated');
|
|
269
|
+
if (event) {
|
|
270
|
+
this.accountAddress = event.args.safeAccount;
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
this.accountAddress = await this.agether4337Factory.getAccount(id);
|
|
274
|
+
}
|
|
275
|
+
return this.accountAddress;
|
|
276
|
+
}
|
|
277
|
+
/** Get the Safe account address for the current agent. Cached after first call. */
|
|
278
|
+
async getAccountAddress() {
|
|
279
|
+
if (this.accountAddress)
|
|
280
|
+
return this.accountAddress;
|
|
281
|
+
const id = this._requireAgentId();
|
|
282
|
+
const addr = await this.agether4337Factory.getAccount(id);
|
|
283
|
+
if (addr === ethers.ZeroAddress) {
|
|
284
|
+
throw new AgetherError('No account found. Create one with createAccount() or register().', 'NO_ACCOUNT');
|
|
285
|
+
}
|
|
286
|
+
this.accountAddress = addr;
|
|
287
|
+
return addr;
|
|
288
|
+
}
|
|
289
|
+
/** Check whether the Safe account has been deployed. */
|
|
290
|
+
async accountExists() {
|
|
291
|
+
const id = this._requireAgentId();
|
|
292
|
+
return this.agether4337Factory.accountExists(id);
|
|
293
|
+
}
|
|
294
|
+
// ════════════════════════════════════════════════════════
|
|
295
|
+
// Balances
|
|
296
|
+
// ════════════════════════════════════════════════════════
|
|
297
|
+
/**
|
|
298
|
+
* Get ETH, USDC, and collateral token balances for EOA and Safe account.
|
|
299
|
+
*
|
|
300
|
+
* Collateral tokens are resolved from a built-in registry of well-known
|
|
301
|
+
* tokens per chain (WETH, wstETH, cbETH).
|
|
302
|
+
*/
|
|
303
|
+
async getBalances() {
|
|
304
|
+
const provider = this.signer.provider;
|
|
305
|
+
const eoaAddr = await this._getSignerAddress();
|
|
306
|
+
const usdc = new Contract(this.config.contracts.usdc, ERC20_ABI, provider);
|
|
307
|
+
const ethBal = await provider.getBalance(eoaAddr);
|
|
308
|
+
const usdcBal = await usdc.balanceOf(eoaAddr);
|
|
309
|
+
// Fetch collateral token balances for EOA
|
|
310
|
+
const knownTokens = KNOWN_TOKENS[this.config.chainId] ?? {};
|
|
311
|
+
const eoaCollateral = {};
|
|
312
|
+
for (const [symbol, info] of Object.entries(knownTokens)) {
|
|
313
|
+
try {
|
|
314
|
+
const token = new Contract(info.address, ERC20_ABI, provider);
|
|
315
|
+
const bal = await token.balanceOf(eoaAddr);
|
|
316
|
+
eoaCollateral[symbol] = ethers.formatUnits(bal, info.decimals);
|
|
317
|
+
}
|
|
318
|
+
catch (e) {
|
|
319
|
+
console.warn(`[agether] EOA collateral fetch failed for ${symbol}:`, e instanceof Error ? e.message : e);
|
|
320
|
+
eoaCollateral[symbol] = '0';
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const result = {
|
|
324
|
+
agentId: this.agentId !== undefined ? this.agentId.toString() : '?',
|
|
325
|
+
address: eoaAddr,
|
|
326
|
+
eth: ethers.formatEther(ethBal),
|
|
327
|
+
usdc: ethers.formatUnits(usdcBal, 6),
|
|
328
|
+
collateral: eoaCollateral,
|
|
329
|
+
};
|
|
330
|
+
try {
|
|
331
|
+
const acctAddr = await this.getAccountAddress();
|
|
332
|
+
const acctEth = await provider.getBalance(acctAddr);
|
|
333
|
+
const acctUsdc = await usdc.balanceOf(acctAddr);
|
|
334
|
+
const acctCollateral = {};
|
|
335
|
+
for (const [symbol, info] of Object.entries(knownTokens)) {
|
|
336
|
+
try {
|
|
337
|
+
const token = new Contract(info.address, ERC20_ABI, provider);
|
|
338
|
+
const bal = await token.balanceOf(acctAddr);
|
|
339
|
+
acctCollateral[symbol] = ethers.formatUnits(bal, info.decimals);
|
|
340
|
+
}
|
|
341
|
+
catch (e) {
|
|
342
|
+
console.warn(`[agether] Account collateral fetch failed for ${symbol}:`, e instanceof Error ? e.message : e);
|
|
343
|
+
acctCollateral[symbol] = '0';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
result.agentAccount = {
|
|
347
|
+
address: acctAddr,
|
|
348
|
+
eth: ethers.formatEther(acctEth),
|
|
349
|
+
usdc: ethers.formatUnits(acctUsdc, 6),
|
|
350
|
+
collateral: acctCollateral,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
if (e instanceof AgetherError && (e.code === 'NO_ACCOUNT' || e.code === 'NO_AGENT_ID')) {
|
|
355
|
+
// No account or agentId yet — leave agentAccount undefined
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
console.warn('[agether] getBalances: failed to fetch Safe account data:', e.message ?? e);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Fund the Safe account with USDC from EOA.
|
|
365
|
+
* This is a simple ERC-20 transfer (does NOT require a UserOp).
|
|
366
|
+
*/
|
|
367
|
+
async fundAccount(usdcAmount) {
|
|
368
|
+
const acctAddr = await this.getAccountAddress();
|
|
369
|
+
const usdc = new Contract(this.config.contracts.usdc, ERC20_ABI, this.signer);
|
|
370
|
+
const amount = ethers.parseUnits(usdcAmount, 6);
|
|
371
|
+
const tx = await usdc.transfer(acctAddr, amount);
|
|
372
|
+
const receipt = await tx.wait();
|
|
373
|
+
this._refreshSigner();
|
|
374
|
+
return {
|
|
375
|
+
txHash: receipt.hash,
|
|
376
|
+
blockNumber: receipt.blockNumber,
|
|
377
|
+
status: receipt.status === 1 ? 'success' : 'failed',
|
|
378
|
+
gasUsed: receipt.gasUsed,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
// ════════════════════════════════════════════════════════
|
|
382
|
+
// Withdrawals (Safe → EOA via UserOps)
|
|
383
|
+
// ════════════════════════════════════════════════════════
|
|
384
|
+
/**
|
|
385
|
+
* Withdraw an ERC-20 token from Safe account to EOA.
|
|
386
|
+
* Executes a transfer via Safe UserOp.
|
|
387
|
+
*
|
|
388
|
+
* @param tokenSymbol - Token to withdraw (e.g. 'USDC', 'WETH', 'wstETH') or 0x address
|
|
389
|
+
* @param amount - Amount to withdraw (human-readable, e.g. '100', or 'all')
|
|
390
|
+
*/
|
|
391
|
+
async withdrawToken(tokenSymbol, amount) {
|
|
392
|
+
const acctAddr = await this.getAccountAddress();
|
|
393
|
+
const eoaAddr = await this._getSignerAddress();
|
|
394
|
+
const tokenInfo = await this._resolveToken(tokenSymbol);
|
|
395
|
+
const tokenContract = new Contract(tokenInfo.address, ERC20_ABI, this.signer.provider);
|
|
396
|
+
let weiAmount;
|
|
397
|
+
if (amount === 'all') {
|
|
398
|
+
weiAmount = await tokenContract.balanceOf(acctAddr);
|
|
399
|
+
if (weiAmount === 0n) {
|
|
400
|
+
throw new AgetherError(`No ${tokenInfo.symbol} in Safe account`, 'INSUFFICIENT_BALANCE');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
weiAmount = ethers.parseUnits(amount, tokenInfo.decimals);
|
|
405
|
+
}
|
|
406
|
+
const data = erc20Iface.encodeFunctionData('transfer', [eoaAddr, weiAmount]);
|
|
407
|
+
const receipt = await this._exec(tokenInfo.address, data);
|
|
408
|
+
const actualAmount = amount === 'all'
|
|
409
|
+
? ethers.formatUnits(weiAmount, tokenInfo.decimals)
|
|
410
|
+
: amount;
|
|
411
|
+
return { tx: receipt.hash, token: tokenInfo.symbol, amount: actualAmount, destination: eoaAddr };
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Withdraw ETH from Safe account to EOA.
|
|
415
|
+
* Executes a native ETH transfer via Safe UserOp.
|
|
416
|
+
*
|
|
417
|
+
* @param amount - ETH amount (e.g. '0.01' or 'all')
|
|
418
|
+
*/
|
|
419
|
+
async withdrawEth(amount) {
|
|
420
|
+
const acctAddr = await this.getAccountAddress();
|
|
421
|
+
const eoaAddr = await this._getSignerAddress();
|
|
422
|
+
let weiAmount;
|
|
423
|
+
if (amount === 'all') {
|
|
424
|
+
weiAmount = await this.signer.provider.getBalance(acctAddr);
|
|
425
|
+
if (weiAmount === 0n)
|
|
426
|
+
throw new AgetherError('No ETH in Safe account', 'INSUFFICIENT_BALANCE');
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
weiAmount = ethers.parseEther(amount);
|
|
430
|
+
}
|
|
431
|
+
// Empty calldata + value = plain ETH transfer
|
|
432
|
+
const receipt = await this._exec(eoaAddr, '0x', weiAmount);
|
|
433
|
+
const actualAmount = amount === 'all' ? ethers.formatEther(weiAmount) : amount;
|
|
434
|
+
return { tx: receipt.hash, token: 'ETH', amount: actualAmount, destination: eoaAddr };
|
|
435
|
+
}
|
|
436
|
+
// ════════════════════════════════════════════════════════
|
|
437
|
+
// Sponsorship
|
|
438
|
+
// ════════════════════════════════════════════════════════
|
|
439
|
+
/**
|
|
440
|
+
* Send tokens to another agent's Safe account (or any address).
|
|
441
|
+
* Transfers from EOA (does NOT require a UserOp).
|
|
442
|
+
*
|
|
443
|
+
* @param target - `{ agentId: '42' }` or `{ address: '0x...' }`
|
|
444
|
+
* @param tokenSymbol - Token to send (e.g. 'WETH', 'USDC') or 0x address
|
|
445
|
+
* @param amount - Amount to send (human-readable)
|
|
446
|
+
*/
|
|
447
|
+
async sponsor(target, tokenSymbol, amount) {
|
|
448
|
+
const tokenInfo = await this._resolveToken(tokenSymbol);
|
|
449
|
+
let targetAddr;
|
|
450
|
+
if (target.address) {
|
|
451
|
+
targetAddr = target.address;
|
|
452
|
+
}
|
|
453
|
+
else if (target.agentId) {
|
|
454
|
+
targetAddr = await this.agether4337Factory.getAccount(BigInt(target.agentId));
|
|
455
|
+
if (targetAddr === ethers.ZeroAddress) {
|
|
456
|
+
throw new AgetherError('Target agent has no account', 'NO_ACCOUNT');
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
throw new AgetherError('Provide agentId or address', 'INVALID_TARGET');
|
|
461
|
+
}
|
|
462
|
+
const weiAmount = ethers.parseUnits(amount, tokenInfo.decimals);
|
|
463
|
+
const tokenContract = new Contract(tokenInfo.address, ERC20_ABI, this.signer);
|
|
464
|
+
const tx = await tokenContract.transfer(targetAddr, weiAmount);
|
|
465
|
+
const receipt = await tx.wait();
|
|
466
|
+
this._refreshSigner();
|
|
467
|
+
return { tx: receipt.hash, targetAccount: targetAddr, targetAgentId: target.agentId };
|
|
468
|
+
}
|
|
469
|
+
// ════════════════════════════════════════════════════════
|
|
470
|
+
// Identity & Validation
|
|
471
|
+
// ════════════════════════════════════════════════════════
|
|
472
|
+
/**
|
|
473
|
+
* Check if the KYA gate is active on the validation module.
|
|
474
|
+
* If validationRegistry is not set, all txs pass (KYA disabled).
|
|
475
|
+
*/
|
|
476
|
+
async isKyaRequired() {
|
|
477
|
+
try {
|
|
478
|
+
const registryAddr = await this.validationModule.validationRegistry();
|
|
479
|
+
return registryAddr !== ethers.ZeroAddress;
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Check if this agent's code is KYA-approved.
|
|
487
|
+
* Uses the ERC8004ValidationModule.isKYAApproved(account) view.
|
|
488
|
+
*/
|
|
489
|
+
async isKyaApproved() {
|
|
490
|
+
try {
|
|
491
|
+
const acctAddr = await this.getAccountAddress();
|
|
492
|
+
return this.validationModule.isKYAApproved(acctAddr);
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/** Check whether the agent's ERC-8004 NFT exists. */
|
|
499
|
+
async identityExists() {
|
|
500
|
+
const id = this._requireAgentId();
|
|
501
|
+
try {
|
|
502
|
+
await this.identityRegistry.ownerOf(id);
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
catch (e) {
|
|
506
|
+
console.warn('[agether] identityExists check failed:', e instanceof Error ? e.message : e);
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/** Get the owner address of the ERC-8004 NFT. */
|
|
511
|
+
async getIdentityOwner() {
|
|
512
|
+
const id = this._requireAgentId();
|
|
513
|
+
return this.identityRegistry.ownerOf(id);
|
|
514
|
+
}
|
|
515
|
+
// ════════════════════════════════════════════════════════
|
|
516
|
+
// Reputation
|
|
517
|
+
// ════════════════════════════════════════════════════════
|
|
518
|
+
/** Read the agent's current credit score from the Agether8004Scorer contract. */
|
|
519
|
+
async getCreditScore() {
|
|
520
|
+
const id = this._requireAgentId();
|
|
521
|
+
return this.agether8004Scorer.getCreditScore(id);
|
|
522
|
+
}
|
|
523
|
+
/** Check if the score is fresh (within MAX_ORACLE_AGE). */
|
|
524
|
+
async isScoreFresh() {
|
|
525
|
+
const id = this._requireAgentId();
|
|
526
|
+
const [fresh, age] = await this.agether8004Scorer.isScoreFresh(id);
|
|
527
|
+
return { fresh, age };
|
|
528
|
+
}
|
|
529
|
+
/** Check if the agent meets a minimum score threshold. */
|
|
530
|
+
async isEligible(minScore = 500n) {
|
|
531
|
+
const id = this._requireAgentId();
|
|
532
|
+
const [eligible, currentScore] = await this.agether8004Scorer.isEligible(id, minScore);
|
|
533
|
+
return { eligible, currentScore };
|
|
534
|
+
}
|
|
535
|
+
// ════════════════════════════════════════════════════════
|
|
536
|
+
// Getters
|
|
537
|
+
// ════════════════════════════════════════════════════════
|
|
538
|
+
get chainId() {
|
|
539
|
+
return this.config.chainId;
|
|
540
|
+
}
|
|
541
|
+
get contracts() {
|
|
542
|
+
return this.config.contracts;
|
|
543
|
+
}
|
|
544
|
+
get currentAccountAddress() {
|
|
545
|
+
return this.accountAddress;
|
|
546
|
+
}
|
|
547
|
+
getSigner() {
|
|
548
|
+
return this.signer;
|
|
549
|
+
}
|
|
550
|
+
getAgentId() {
|
|
551
|
+
return this._requireAgentId();
|
|
552
|
+
}
|
|
553
|
+
// ════════════════════════════════════════════════════════
|
|
554
|
+
// Private Helpers
|
|
555
|
+
// ════════════════════════════════════════════════════════
|
|
556
|
+
/** Require agentId to be set, throw a helpful error otherwise. */
|
|
557
|
+
_requireAgentId() {
|
|
558
|
+
if (this.agentId === undefined) {
|
|
559
|
+
throw new AgetherError('agentId not set. Call register() first.', 'NO_AGENT_ID');
|
|
560
|
+
}
|
|
561
|
+
return this.agentId;
|
|
562
|
+
}
|
|
563
|
+
/** Resolve EOA signer address (async, cached). */
|
|
564
|
+
async _getSignerAddress() {
|
|
565
|
+
if (!this._eoaAddress) {
|
|
566
|
+
this._eoaAddress = await this.signer.getAddress();
|
|
567
|
+
}
|
|
568
|
+
return this._eoaAddress;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Resolve a token symbol or address to { address, symbol, decimals }.
|
|
572
|
+
*
|
|
573
|
+
* Supports:
|
|
574
|
+
* - `'USDC'` → from chain config
|
|
575
|
+
* - Well-known symbols (`'WETH'`, `'wstETH'`, `'cbETH'`) → built-in per-chain registry
|
|
576
|
+
* - `'0x...'` address → reads decimals and symbol onchain
|
|
577
|
+
*/
|
|
578
|
+
async _resolveToken(symbolOrAddress) {
|
|
579
|
+
// USDC from config
|
|
580
|
+
if (symbolOrAddress.toUpperCase() === 'USDC') {
|
|
581
|
+
return { address: this.config.contracts.usdc, symbol: 'USDC', decimals: 6 };
|
|
582
|
+
}
|
|
583
|
+
// Well-known tokens for this chain
|
|
584
|
+
const chainTokens = KNOWN_TOKENS[this.config.chainId] ?? {};
|
|
585
|
+
const bySymbol = chainTokens[symbolOrAddress] || chainTokens[symbolOrAddress.toUpperCase()];
|
|
586
|
+
if (bySymbol)
|
|
587
|
+
return bySymbol;
|
|
588
|
+
// Address: read decimals and symbol onchain
|
|
589
|
+
if (symbolOrAddress.startsWith('0x') && symbolOrAddress.length === 42) {
|
|
590
|
+
try {
|
|
591
|
+
const token = new Contract(symbolOrAddress, ['function decimals() view returns (uint8)', 'function symbol() view returns (string)'], this.signer.provider);
|
|
592
|
+
const [decimals, symbol] = await Promise.all([token.decimals(), token.symbol()]);
|
|
593
|
+
return { address: symbolOrAddress, symbol, decimals: Number(decimals) };
|
|
594
|
+
}
|
|
595
|
+
catch (e) {
|
|
596
|
+
throw new AgetherError(`Failed to read token at ${symbolOrAddress}: ${e instanceof Error ? e.message : e}`, 'UNKNOWN_TOKEN');
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
throw new AgetherError(`Unknown token: ${symbolOrAddress}. Use a known symbol (USDC, WETH, wstETH, cbETH) or a 0x address.`, 'UNKNOWN_TOKEN');
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Refresh signer and rebind contracts for fresh nonce.
|
|
603
|
+
*
|
|
604
|
+
* For the privateKey path: recreates provider + wallet.
|
|
605
|
+
* For external signers: just rebinds contract instances.
|
|
606
|
+
*/
|
|
607
|
+
_refreshSigner() {
|
|
608
|
+
if (!this._useExternalSigner && this._privateKey) {
|
|
609
|
+
const provider = new ethers.JsonRpcProvider(this._rpcUrl);
|
|
610
|
+
const wallet = new ethers.Wallet(this._privateKey, provider);
|
|
611
|
+
this.signer = wallet;
|
|
612
|
+
this._eoaAddress = wallet.address;
|
|
613
|
+
}
|
|
614
|
+
const c = this.config.contracts;
|
|
615
|
+
this.agether4337Factory = new Contract(c.agether4337Factory, AGETHER_4337_FACTORY_ABI, this.signer);
|
|
616
|
+
this.identityRegistry = new Contract(c.identityRegistry, IDENTITY_REGISTRY_ABI, this.signer);
|
|
617
|
+
this.agether8004Scorer = new Contract(c.agether8004Scorer, AGETHER_8004_SCORER_ABI, this.signer.provider);
|
|
618
|
+
this.validationModule = new Contract(c.erc8004ValidationModule, AGETHER_8004_VALIDATION_MODULE_ABI, this.signer.provider);
|
|
619
|
+
this.entryPoint = new Contract(c.entryPoint, ENTRYPOINT_V07_ABI, this.signer);
|
|
620
|
+
}
|
|
621
|
+
// ────────────────────────────────────────────────────────
|
|
622
|
+
// ERC-4337 UserOp helpers (Safe + Safe7579 + EntryPoint v0.7)
|
|
623
|
+
// ────────────────────────────────────────────────────────
|
|
624
|
+
/**
|
|
625
|
+
* Pack two uint128 values into a single bytes32:
|
|
626
|
+
* bytes32 = (hi << 128) | lo
|
|
627
|
+
*/
|
|
628
|
+
_packUint128(hi, lo) {
|
|
629
|
+
return ethers.zeroPadValue(ethers.toBeHex((hi << 128n) | lo), 32);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Build, sign and submit a PackedUserOperation through EntryPoint.handleOps.
|
|
633
|
+
*/
|
|
634
|
+
async _submitUserOp(callData) {
|
|
635
|
+
const sender = await this.getAccountAddress();
|
|
636
|
+
const eoaAddr = await this._getSignerAddress();
|
|
637
|
+
// Nonce key = validator module address << 32
|
|
638
|
+
const validatorAddr = this.config.contracts.erc8004ValidationModule;
|
|
639
|
+
const nonceKey = BigInt(validatorAddr) << 32n;
|
|
640
|
+
const nonce = await this.entryPoint.getNonce(sender, nonceKey);
|
|
641
|
+
// Gas prices
|
|
642
|
+
const feeData = await this.signer.provider.getFeeData();
|
|
643
|
+
const maxFeePerGas = feeData.maxFeePerGas ?? ethers.parseUnits('0.5', 'gwei');
|
|
644
|
+
const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ?? ethers.parseUnits('0.1', 'gwei');
|
|
645
|
+
const verificationGasLimit = 500000n;
|
|
646
|
+
const callGasLimit = 800000n;
|
|
647
|
+
const preVerificationGas = 100000n;
|
|
648
|
+
const accountGasLimits = this._packUint128(verificationGasLimit, callGasLimit);
|
|
649
|
+
const gasFees = this._packUint128(maxPriorityFeePerGas, maxFeePerGas);
|
|
650
|
+
// Auto-fund Safe account for gas
|
|
651
|
+
const requiredPrefund = (verificationGasLimit + callGasLimit + preVerificationGas) * maxFeePerGas;
|
|
652
|
+
const accountBalance = await this.signer.provider.getBalance(sender);
|
|
653
|
+
if (accountBalance < requiredPrefund) {
|
|
654
|
+
const topUp = requiredPrefund - accountBalance;
|
|
655
|
+
const topUpWithBuffer = (topUp * 120n) / 100n;
|
|
656
|
+
const fundTx = await this.signer.sendTransaction({
|
|
657
|
+
to: sender,
|
|
658
|
+
value: topUpWithBuffer,
|
|
659
|
+
});
|
|
660
|
+
await fundTx.wait();
|
|
661
|
+
this._refreshSigner();
|
|
662
|
+
}
|
|
663
|
+
// Build PackedUserOperation
|
|
664
|
+
const userOp = {
|
|
665
|
+
sender,
|
|
666
|
+
nonce,
|
|
667
|
+
initCode: '0x',
|
|
668
|
+
callData,
|
|
669
|
+
accountGasLimits,
|
|
670
|
+
preVerificationGas,
|
|
671
|
+
gasFees,
|
|
672
|
+
paymasterAndData: '0x',
|
|
673
|
+
signature: '0x',
|
|
674
|
+
};
|
|
675
|
+
// Sign
|
|
676
|
+
const userOpHash = await this.entryPoint.getUserOpHash(userOp);
|
|
677
|
+
const signature = await this.signer.signMessage(ethers.getBytes(userOpHash));
|
|
678
|
+
userOp.signature = signature;
|
|
679
|
+
// Submit
|
|
680
|
+
const tx = await this.entryPoint.handleOps([userOp], eoaAddr);
|
|
681
|
+
const receipt = await tx.wait();
|
|
682
|
+
this._refreshSigner();
|
|
683
|
+
// Verify inner UserOp execution succeeded
|
|
684
|
+
const epIface = new ethers.Interface(ENTRYPOINT_V07_ABI);
|
|
685
|
+
for (const log of receipt.logs) {
|
|
686
|
+
try {
|
|
687
|
+
const parsed = epIface.parseLog({ topics: log.topics, data: log.data });
|
|
688
|
+
if (parsed?.name === 'UserOperationEvent' && !parsed.args.success) {
|
|
689
|
+
let revertMsg = 'UserOp inner execution reverted';
|
|
690
|
+
for (const rLog of receipt.logs) {
|
|
691
|
+
try {
|
|
692
|
+
const rParsed = epIface.parseLog({ topics: rLog.topics, data: rLog.data });
|
|
693
|
+
if (rParsed?.name === 'UserOperationRevertReason') {
|
|
694
|
+
const reason = rParsed.args.revertReason;
|
|
695
|
+
try {
|
|
696
|
+
if (reason.length >= 10 && reason.slice(0, 10) === '0x08c379a0') {
|
|
697
|
+
const decoded = ethers.AbiCoder.defaultAbiCoder().decode(['string'], '0x' + reason.slice(10));
|
|
698
|
+
revertMsg = `UserOp reverted: ${decoded[0]}`;
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
revertMsg = `UserOp reverted with data: ${reason}`;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
revertMsg = `UserOp reverted with data: ${reason}`;
|
|
706
|
+
}
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
throw new AgetherError(revertMsg, 'USEROP_EXECUTION_FAILED');
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
catch (e) {
|
|
718
|
+
if (e instanceof AgetherError)
|
|
719
|
+
throw e;
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return receipt;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Execute a single call via Safe7579 account (ERC-7579 single mode)
|
|
727
|
+
* through an ERC-4337 UserOperation.
|
|
728
|
+
*/
|
|
729
|
+
async _exec(target, data, value = 0n) {
|
|
730
|
+
const valueHex = ethers.zeroPadValue(ethers.toBeHex(value), 32);
|
|
731
|
+
const executionCalldata = ethers.concat([target, valueHex, data]);
|
|
732
|
+
const safe7579Iface = new ethers.Interface(SAFE7579_ACCOUNT_ABI);
|
|
733
|
+
const callData = safe7579Iface.encodeFunctionData('execute', [MODE_SINGLE, executionCalldata]);
|
|
734
|
+
return this._submitUserOp(callData);
|
|
735
|
+
}
|
|
736
|
+
}
|