@agirails/sdk 2.2.3 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -23
- package/dist/ACTPClient.d.ts +7 -0
- package/dist/ACTPClient.d.ts.map +1 -1
- package/dist/ACTPClient.js +56 -1
- package/dist/ACTPClient.js.map +1 -1
- package/dist/abi/AgentRegistry.json +133 -0
- package/dist/adapters/X402Adapter.d.ts +34 -7
- package/dist/adapters/X402Adapter.d.ts.map +1 -1
- package/dist/adapters/X402Adapter.js +36 -8
- package/dist/adapters/X402Adapter.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/cli/commands/diff.d.ts +11 -0
- package/dist/cli/commands/diff.d.ts.map +1 -0
- package/dist/cli/commands/diff.js +115 -0
- package/dist/cli/commands/diff.js.map +1 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +51 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/publish.d.ts +11 -0
- package/dist/cli/commands/publish.d.ts.map +1 -0
- package/dist/cli/commands/publish.js +170 -0
- package/dist/cli/commands/publish.js.map +1 -0
- package/dist/cli/commands/pull.d.ts +12 -0
- package/dist/cli/commands/pull.d.ts.map +1 -0
- package/dist/cli/commands/pull.js +99 -0
- package/dist/cli/commands/pull.js.map +1 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/config/agirailsmd.d.ts +94 -0
- package/dist/config/agirailsmd.d.ts.map +1 -0
- package/dist/config/agirailsmd.js +209 -0
- package/dist/config/agirailsmd.js.map +1 -0
- package/dist/config/networks.d.ts +2 -0
- package/dist/config/networks.d.ts.map +1 -1
- package/dist/config/networks.js +10 -4
- package/dist/config/networks.js.map +1 -1
- package/dist/config/publishPipeline.d.ts +61 -0
- package/dist/config/publishPipeline.d.ts.map +1 -0
- package/dist/config/publishPipeline.js +192 -0
- package/dist/config/publishPipeline.js.map +1 -0
- package/dist/config/syncOperations.d.ts +67 -0
- package/dist/config/syncOperations.d.ts.map +1 -0
- package/dist/config/syncOperations.js +208 -0
- package/dist/config/syncOperations.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/level0/request.d.ts.map +1 -1
- package/dist/level0/request.js +23 -86
- package/dist/level0/request.js.map +1 -1
- package/dist/level1/Agent.d.ts +0 -11
- package/dist/level1/Agent.d.ts.map +1 -1
- package/dist/level1/Agent.js +15 -32
- package/dist/level1/Agent.js.map +1 -1
- package/dist/registry/AgentRegistryClient.d.ts +75 -0
- package/dist/registry/AgentRegistryClient.d.ts.map +1 -0
- package/dist/registry/AgentRegistryClient.js +160 -0
- package/dist/registry/AgentRegistryClient.js.map +1 -0
- package/dist/types/adapter.d.ts +39 -0
- package/dist/types/adapter.d.ts.map +1 -1
- package/dist/types/adapter.js +7 -0
- package/dist/types/adapter.js.map +1 -1
- package/dist/types/x402.d.ts +23 -0
- package/dist/types/x402.d.ts.map +1 -1
- package/dist/types/x402.js.map +1 -1
- package/dist/wallet/keystore.d.ts +16 -0
- package/dist/wallet/keystore.d.ts.map +1 -0
- package/dist/wallet/keystore.js +132 -0
- package/dist/wallet/keystore.js.map +1 -0
- package/package.json +2 -1
- package/src/ACTPClient.ts +63 -1
- package/src/abi/AgentRegistry.json +133 -0
- package/src/adapters/X402Adapter.ts +94 -16
- package/src/adapters/index.ts +9 -1
- package/src/cli/commands/diff.ts +141 -0
- package/src/cli/commands/init.ts +65 -4
- package/src/cli/commands/publish.ts +209 -0
- package/src/cli/commands/pull.ts +124 -0
- package/src/cli/index.ts +8 -0
- package/src/config/agirailsmd.ts +262 -0
- package/src/config/networks.ts +12 -4
- package/src/config/publishPipeline.ts +276 -0
- package/src/config/syncOperations.ts +279 -0
- package/src/index.ts +3 -0
- package/src/level0/request.ts +27 -88
- package/src/level1/Agent.ts +16 -32
- package/src/registry/AgentRegistryClient.ts +202 -0
- package/src/types/adapter.ts +14 -0
- package/src/types/x402.ts +32 -0
- package/src/wallet/keystore.ts +119 -0
package/src/level0/request.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { NoProviderFoundError, TimeoutError, ValidationError } from '../errors';
|
|
|
14
14
|
import { safeJSONParse, validateServiceName, isValidAddress } from '../utils/security';
|
|
15
15
|
import { Logger } from '../utils/Logger';
|
|
16
16
|
import { ethers } from 'ethers';
|
|
17
|
+
import { resolvePrivateKey } from '../wallet/keystore';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Request a service
|
|
@@ -61,7 +62,6 @@ export async function request(
|
|
|
61
62
|
service: string,
|
|
62
63
|
options: RequestOptions
|
|
63
64
|
): Promise<RequestResult> {
|
|
64
|
-
// SECURITY FIX (H-2): Validate service name to prevent injection
|
|
65
65
|
const validatedService = validateServiceName(service);
|
|
66
66
|
|
|
67
67
|
const logger = new Logger({ source: 'request' });
|
|
@@ -75,12 +75,8 @@ export async function request(
|
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
// SECURITY FIX (RPCURL): Use rpcUrl from options or fallback to network default
|
|
79
|
-
// This allows Level0 request() to work with testnet/mainnet without requiring
|
|
80
|
-
// explicit rpcUrl if user is okay with public RPC endpoints.
|
|
81
78
|
let rpcUrl = options.rpcUrl;
|
|
82
79
|
if (!rpcUrl && (options.network === 'testnet' || options.network === 'mainnet')) {
|
|
83
|
-
// Import getNetwork to get default rpcUrl from network config
|
|
84
80
|
const { getNetwork } = await import('../config/networks');
|
|
85
81
|
const networkName = options.network === 'testnet' ? 'base-sepolia' : 'base-mainnet';
|
|
86
82
|
const networkConfig = getNetwork(networkName);
|
|
@@ -88,27 +84,27 @@ export async function request(
|
|
|
88
84
|
logger.info(`Using default RPC URL for ${networkName}: ${rpcUrl}`);
|
|
89
85
|
}
|
|
90
86
|
|
|
91
|
-
|
|
87
|
+
const resolvedKey = await resolveKeyIfNeeded(options.wallet, options.network, options.stateDirectory);
|
|
88
|
+
const resolvedAddress = resolvedKey
|
|
89
|
+
? new ethers.Wallet(resolvedKey).address.toLowerCase()
|
|
90
|
+
: undefined;
|
|
91
|
+
|
|
92
92
|
const client = await ACTPClient.create({
|
|
93
93
|
mode: options.network === 'testnet' ? 'testnet' : options.network === 'mainnet' ? 'mainnet' : 'mock',
|
|
94
|
-
requesterAddress: getRequesterAddress(options.wallet),
|
|
94
|
+
requesterAddress: resolvedAddress || getRequesterAddress(options.wallet),
|
|
95
95
|
stateDirectory: options.stateDirectory,
|
|
96
|
-
privateKey: getPrivateKey(options.wallet),
|
|
96
|
+
privateKey: resolvedKey || getPrivateKey(options.wallet),
|
|
97
97
|
rpcUrl,
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
// Calculate deadline
|
|
101
100
|
const deadline = calculateDeadline(options.deadline, options.timeout);
|
|
102
|
-
|
|
103
|
-
// Create transaction with service metadata
|
|
104
101
|
const startTime = Date.now();
|
|
105
102
|
|
|
106
103
|
try {
|
|
107
|
-
const requesterAddress = getRequesterAddress(options.wallet);
|
|
104
|
+
const requesterAddress = resolvedAddress || getRequesterAddress(options.wallet);
|
|
108
105
|
const amountWei = (options.budget * 1_000_000).toString(); // Convert to USDC wei (6 decimals)
|
|
109
106
|
|
|
110
107
|
// In mock mode, ensure requester has enough funds
|
|
111
|
-
// This is a convenience feature for testing - won't exist in production
|
|
112
108
|
if (client.runtime && 'mintTokens' in client.runtime) {
|
|
113
109
|
const mockRuntime = client.runtime as any;
|
|
114
110
|
const balance = await mockRuntime.getBalance(requesterAddress);
|
|
@@ -122,30 +118,19 @@ export async function request(
|
|
|
122
118
|
}
|
|
123
119
|
}
|
|
124
120
|
|
|
125
|
-
// ARCHITECTURE FIX: Service metadata handling
|
|
126
|
-
// - MockRuntime: Store plaintext for provider matching and input extraction
|
|
127
|
-
// - BlockchainRuntime: Hashes plaintext internally before sending on-chain
|
|
128
|
-
//
|
|
129
|
-
// Provider needs plaintext to:
|
|
130
|
-
// 1. Match service name to handler (findServiceHandler)
|
|
131
|
-
// 2. Extract input data for job execution (extractJobInput)
|
|
132
|
-
//
|
|
133
|
-
// On-chain storage uses bytes32 hash (BlockchainRuntime.validateServiceHash handles this)
|
|
134
121
|
const serviceMetadata = JSON.stringify({
|
|
135
122
|
service: validatedService,
|
|
136
123
|
input: options.input,
|
|
137
124
|
timestamp: Date.now(),
|
|
138
125
|
});
|
|
139
126
|
|
|
140
|
-
// Create transaction with structured metadata
|
|
141
|
-
// MockRuntime stores as-is, BlockchainRuntime hashes for on-chain
|
|
142
127
|
const txId = await client.runtime.createTransaction({
|
|
143
128
|
provider,
|
|
144
129
|
requester: requesterAddress,
|
|
145
130
|
amount: amountWei,
|
|
146
131
|
deadline,
|
|
147
|
-
disputeWindow: options.disputeWindow ?? 172800,
|
|
148
|
-
serviceDescription: serviceMetadata,
|
|
132
|
+
disputeWindow: options.disputeWindow ?? 172800,
|
|
133
|
+
serviceDescription: serviceMetadata,
|
|
149
134
|
});
|
|
150
135
|
|
|
151
136
|
// Call onProgress if provided
|
|
@@ -187,12 +172,8 @@ export async function request(
|
|
|
187
172
|
attempts++;
|
|
188
173
|
}
|
|
189
174
|
|
|
190
|
-
// Check if we got a result
|
|
191
175
|
if (!tx || (tx.state !== 'DELIVERED' && tx.state !== 'SETTLED')) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// SECURITY FIX (H-3): Auto-cancel transaction on timeout if still in early state
|
|
195
|
-
// This prevents funds from being locked indefinitely if provider never responds
|
|
176
|
+
// Auto-cancel on timeout if still in early state
|
|
196
177
|
if (tx && (tx.state === 'INITIATED' || tx.state === 'COMMITTED')) {
|
|
197
178
|
try {
|
|
198
179
|
logger.warn('Transaction timed out, cancelling to release funds', {
|
|
@@ -200,8 +181,6 @@ export async function request(
|
|
|
200
181
|
state: tx.state,
|
|
201
182
|
});
|
|
202
183
|
|
|
203
|
-
// ACTUALLY CANCEL THE TRANSACTION
|
|
204
|
-
// Check if runtime has cancelTransaction method
|
|
205
184
|
if ('cancelTransaction' in client.runtime) {
|
|
206
185
|
await (client.runtime as any).cancelTransaction(txId);
|
|
207
186
|
logger.info('Transaction cancelled successfully', { txId });
|
|
@@ -211,7 +190,6 @@ export async function request(
|
|
|
211
190
|
(error as any).wasCancelled = true;
|
|
212
191
|
throw error;
|
|
213
192
|
} else {
|
|
214
|
-
// Fallback: Transition to CANCELLED state
|
|
215
193
|
await client.runtime.transitionState(txId, 'CANCELLED');
|
|
216
194
|
logger.info('Transaction cancelled successfully (via transitionState)', { txId });
|
|
217
195
|
|
|
@@ -222,7 +200,6 @@ export async function request(
|
|
|
222
200
|
}
|
|
223
201
|
} catch (cancelError) {
|
|
224
202
|
logger.error('Failed to cancel timed-out transaction', { txId }, cancelError as Error);
|
|
225
|
-
// Continue with original timeout error
|
|
226
203
|
}
|
|
227
204
|
}
|
|
228
205
|
|
|
@@ -232,13 +209,8 @@ export async function request(
|
|
|
232
209
|
throw error;
|
|
233
210
|
}
|
|
234
211
|
|
|
235
|
-
// SECURITY FIX (C-3): Safe JSON parsing with schema validation
|
|
236
|
-
// Extract result from delivery proof
|
|
237
212
|
let deliveredResult: any = {};
|
|
238
213
|
if (tx.deliveryProof) {
|
|
239
|
-
// Define expected schema for delivery proof
|
|
240
|
-
// NOTE: 'type' field with value 'delivery.proof' is the unique wrapper marker
|
|
241
|
-
// (set by ProofGenerator.generateDeliveryProof) that handlers won't naturally return
|
|
242
214
|
const DELIVERY_PROOF_SCHEMA: Record<string, string> = {
|
|
243
215
|
result: 'any',
|
|
244
216
|
data: 'any',
|
|
@@ -247,28 +219,18 @@ export async function request(
|
|
|
247
219
|
timestamp: 'number',
|
|
248
220
|
contentHash: 'string',
|
|
249
221
|
txId: 'string',
|
|
250
|
-
type: 'string',
|
|
222
|
+
type: 'string',
|
|
251
223
|
};
|
|
252
224
|
|
|
253
|
-
// Use safeJSONParse with schema validation which:
|
|
254
|
-
// 1. Validates JSON structure
|
|
255
|
-
// 2. Removes __proto__, constructor, prototype properties
|
|
256
|
-
// 3. Prevents prototype pollution attacks
|
|
257
|
-
// 4. Validates against expected schema
|
|
258
|
-
// 5. Checks size limits to prevent DoS
|
|
259
|
-
// 6. Returns null if parsing fails
|
|
260
225
|
const parsed = safeJSONParse(tx.deliveryProof, DELIVERY_PROOF_SCHEMA);
|
|
261
226
|
|
|
262
227
|
if (parsed !== null) {
|
|
263
228
|
deliveredResult = parsed;
|
|
264
229
|
} else {
|
|
265
|
-
// If parsing failed, treat as plain text (but don't execute or eval)
|
|
266
230
|
deliveredResult = { data: tx.deliveryProof };
|
|
267
231
|
logger.warn('Failed to parse delivery proof as JSON', { txId });
|
|
268
232
|
}
|
|
269
233
|
} else if (options.network === 'testnet' || options.network === 'mainnet') {
|
|
270
|
-
// KNOWN LIMITATION: BlockchainRuntime doesn't fetch deliveryProof from IPFS yet
|
|
271
|
-
// Result will be empty until this is implemented
|
|
272
234
|
logger.warn(
|
|
273
235
|
'Delivery proof retrieval not yet implemented for testnet/mainnet. ' +
|
|
274
236
|
'Result may be empty. Use ACTPClient with manual proof handling for production.',
|
|
@@ -276,17 +238,11 @@ export async function request(
|
|
|
276
238
|
);
|
|
277
239
|
}
|
|
278
240
|
|
|
279
|
-
// SECURITY FIX (CRITICAL-2): Release escrow only after proper validation
|
|
280
|
-
// For mock mode, auto-release is safe. For testnet/mainnet, require attestation.
|
|
281
241
|
if (tx.state === 'DELIVERED' && tx.escrowId) {
|
|
282
|
-
// Wait for dispute window to expire
|
|
283
242
|
const disputeWindowEnd = (tx.completedAt ?? 0) + tx.disputeWindow;
|
|
284
243
|
const currentTime = client.runtime.time.now();
|
|
285
244
|
|
|
286
245
|
if (currentTime >= disputeWindowEnd) {
|
|
287
|
-
// SECURITY FIX (CRITICAL-2): Only auto-release in mock mode
|
|
288
|
-
// For real networks, the requester should manually verify and release
|
|
289
|
-
// or use attestation-based verification
|
|
290
246
|
const isMockMode = options.network !== 'testnet' && options.network !== 'mainnet';
|
|
291
247
|
|
|
292
248
|
if (isMockMode) {
|
|
@@ -353,6 +309,20 @@ export async function request(
|
|
|
353
309
|
}
|
|
354
310
|
}
|
|
355
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Resolve private key from keystore if wallet is auto/undefined and network is testnet/mainnet.
|
|
314
|
+
* Returns undefined if wallet is explicitly set (caller should use getPrivateKey instead).
|
|
315
|
+
*/
|
|
316
|
+
async function resolveKeyIfNeeded(
|
|
317
|
+
wallet?: 'auto' | 'connect' | string | { privateKey: string },
|
|
318
|
+
network?: string,
|
|
319
|
+
stateDirectory?: string
|
|
320
|
+
): Promise<string | undefined> {
|
|
321
|
+
if (wallet && wallet !== 'auto') return undefined; // explicit wallet, skip auto-detect
|
|
322
|
+
if (network !== 'testnet' && network !== 'mainnet') return undefined;
|
|
323
|
+
return resolvePrivateKey(stateDirectory);
|
|
324
|
+
}
|
|
325
|
+
|
|
356
326
|
/**
|
|
357
327
|
* Find provider for service
|
|
358
328
|
*
|
|
@@ -381,23 +351,10 @@ function findProvider(
|
|
|
381
351
|
return providers[0];
|
|
382
352
|
}
|
|
383
353
|
|
|
384
|
-
/**
|
|
385
|
-
* Get requester address from wallet option
|
|
386
|
-
*
|
|
387
|
-
* SECURITY FIX (HIGH): Properly derive addresses from private keys using ethers
|
|
388
|
-
* Never fabricate addresses or use partial key slices as addresses.
|
|
389
|
-
*
|
|
390
|
-
* @param wallet - Wallet configuration
|
|
391
|
-
* @returns Ethereum address
|
|
392
|
-
* @throws {ValidationError} If address format is invalid
|
|
393
|
-
*/
|
|
394
354
|
function getRequesterAddress(
|
|
395
355
|
wallet?: 'auto' | 'connect' | string | { privateKey: string }
|
|
396
356
|
): string {
|
|
397
|
-
// For mock mode only: generate deterministic address
|
|
398
|
-
// This is only safe because mock mode doesn't involve real funds
|
|
399
357
|
if (!wallet || wallet === 'auto') {
|
|
400
|
-
// Create a valid Ethereum address (40 hex chars) - ONLY for mock mode
|
|
401
358
|
const hex = Buffer.from('requester').toString('hex');
|
|
402
359
|
return '0x' + hex.padEnd(40, '0');
|
|
403
360
|
}
|
|
@@ -407,15 +364,12 @@ function getRequesterAddress(
|
|
|
407
364
|
}
|
|
408
365
|
|
|
409
366
|
if (typeof wallet === 'string') {
|
|
410
|
-
// SECURITY FIX (HIGH): Validate address format
|
|
411
367
|
if (!isValidAddress(wallet)) {
|
|
412
368
|
throw new ValidationError('wallet', `Invalid Ethereum address format: ${wallet}`);
|
|
413
369
|
}
|
|
414
370
|
return wallet.toLowerCase();
|
|
415
371
|
}
|
|
416
372
|
|
|
417
|
-
// SECURITY FIX (HIGH): Derive address from private key using ethers
|
|
418
|
-
// This is the correct way to get address from a private key
|
|
419
373
|
try {
|
|
420
374
|
const walletInstance = new ethers.Wallet(wallet.privateKey);
|
|
421
375
|
return walletInstance.address.toLowerCase();
|
|
@@ -424,15 +378,6 @@ function getRequesterAddress(
|
|
|
424
378
|
}
|
|
425
379
|
}
|
|
426
380
|
|
|
427
|
-
/**
|
|
428
|
-
* Get private key from wallet option
|
|
429
|
-
*
|
|
430
|
-
* SECURITY FIX (HIGH): Validate private key format before use
|
|
431
|
-
*
|
|
432
|
-
* @param wallet - Wallet configuration
|
|
433
|
-
* @returns Private key or undefined
|
|
434
|
-
* @throws {ValidationError} If private key format is invalid
|
|
435
|
-
*/
|
|
436
381
|
function getPrivateKey(
|
|
437
382
|
wallet?: 'auto' | 'connect' | string | { privateKey: string }
|
|
438
383
|
): string | undefined {
|
|
@@ -440,12 +385,8 @@ function getPrivateKey(
|
|
|
440
385
|
return undefined;
|
|
441
386
|
}
|
|
442
387
|
|
|
443
|
-
// If wallet is a string that looks like a private key (0x + 64 hex chars), use it
|
|
444
|
-
// Otherwise treat it as an address and return undefined
|
|
445
388
|
if (typeof wallet === 'string') {
|
|
446
|
-
// Check if it looks like a private key (0x + 64 hex chars)
|
|
447
389
|
if (/^0x[0-9a-fA-F]{64}$/.test(wallet)) {
|
|
448
|
-
// Validate by trying to create a wallet
|
|
449
390
|
try {
|
|
450
391
|
new ethers.Wallet(wallet);
|
|
451
392
|
return wallet;
|
|
@@ -453,11 +394,9 @@ function getPrivateKey(
|
|
|
453
394
|
throw new ValidationError('wallet', 'Invalid private key format');
|
|
454
395
|
}
|
|
455
396
|
}
|
|
456
|
-
// It's an address, not a private key
|
|
457
397
|
return undefined;
|
|
458
398
|
}
|
|
459
399
|
|
|
460
|
-
// Validate private key format
|
|
461
400
|
if (wallet.privateKey) {
|
|
462
401
|
try {
|
|
463
402
|
new ethers.Wallet(wallet.privateKey);
|
package/src/level1/Agent.ts
CHANGED
|
@@ -13,6 +13,7 @@ import * as os from 'os';
|
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import { ethers } from 'ethers';
|
|
15
15
|
import { ACTPClient } from '../ACTPClient';
|
|
16
|
+
import { resolvePrivateKey } from '../wallet/keystore';
|
|
16
17
|
import { Job, JobHandler, JobContext } from './types/Job';
|
|
17
18
|
import { RequestOptions, RequestResult, NetworkOption } from './types/Options';
|
|
18
19
|
import { PricingStrategy } from './pricing/PricingStrategy';
|
|
@@ -386,12 +387,8 @@ export class Agent extends EventEmitter {
|
|
|
386
387
|
this.emit('starting');
|
|
387
388
|
|
|
388
389
|
try {
|
|
389
|
-
// SECURITY FIX (RPCURL): Use rpcUrl from config or fallback to network default
|
|
390
|
-
// This allows Agent to work with testnet/mainnet without requiring explicit rpcUrl
|
|
391
|
-
// if user is okay with public RPC endpoints.
|
|
392
390
|
let rpcUrl = this.config.rpcUrl;
|
|
393
391
|
if (!rpcUrl && (this.network === 'testnet' || this.network === 'mainnet')) {
|
|
394
|
-
// Import getNetwork to get default rpcUrl from network config
|
|
395
392
|
const { getNetwork } = await import('../config/networks');
|
|
396
393
|
const networkName = this.network === 'testnet' ? 'base-sepolia' : 'base-mainnet';
|
|
397
394
|
const networkConfig = getNetwork(networkName);
|
|
@@ -399,16 +396,14 @@ export class Agent extends EventEmitter {
|
|
|
399
396
|
this.logger.info(`Using default RPC URL for ${networkName}: ${rpcUrl}`);
|
|
400
397
|
}
|
|
401
398
|
|
|
402
|
-
// Initialize ACTP client
|
|
403
399
|
this._client = await ACTPClient.create({
|
|
404
400
|
mode: this.network === 'testnet' ? 'testnet' : this.network === 'mainnet' ? 'mainnet' : 'mock',
|
|
405
|
-
requesterAddress: this.address || this.generateAddress(),
|
|
401
|
+
requesterAddress: this.address || await this.generateAddress(),
|
|
406
402
|
stateDirectory: this.config.stateDirectory,
|
|
407
|
-
privateKey: this.getPrivateKey(),
|
|
403
|
+
privateKey: await this.getPrivateKey(),
|
|
408
404
|
rpcUrl,
|
|
409
405
|
});
|
|
410
406
|
|
|
411
|
-
// Start polling for jobs
|
|
412
407
|
this.startPolling();
|
|
413
408
|
|
|
414
409
|
this._status = 'running';
|
|
@@ -1371,15 +1366,8 @@ export class Agent extends EventEmitter {
|
|
|
1371
1366
|
}
|
|
1372
1367
|
}
|
|
1373
1368
|
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
*
|
|
1377
|
-
* SECURITY FIX (HIGH): For testnet/mainnet, MUST derive from private key.
|
|
1378
|
-
* For mock mode, can use deterministic address for convenience.
|
|
1379
|
-
*/
|
|
1380
|
-
private generateAddress(): string {
|
|
1381
|
-
// If wallet has private key, ALWAYS derive address from it
|
|
1382
|
-
const privateKey = this.getPrivateKey();
|
|
1369
|
+
private async generateAddress(): Promise<string> {
|
|
1370
|
+
const privateKey = await this.getPrivateKey();
|
|
1383
1371
|
if (privateKey) {
|
|
1384
1372
|
try {
|
|
1385
1373
|
const wallet = new ethers.Wallet(privateKey);
|
|
@@ -1389,33 +1377,31 @@ export class Agent extends EventEmitter {
|
|
|
1389
1377
|
}
|
|
1390
1378
|
}
|
|
1391
1379
|
|
|
1392
|
-
// For non-mock networks, require a valid private key or address
|
|
1393
1380
|
if (this.network === 'testnet' || this.network === 'mainnet') {
|
|
1394
1381
|
throw new ValidationError(
|
|
1395
1382
|
'wallet',
|
|
1396
|
-
`${this.network} mode requires a valid private key or address in wallet configuration`
|
|
1383
|
+
`${this.network} mode requires a valid private key or address in wallet configuration.\n` +
|
|
1384
|
+
'Run "actp init" to generate a keystore, or set ACTP_PRIVATE_KEY env var.'
|
|
1397
1385
|
);
|
|
1398
1386
|
}
|
|
1399
1387
|
|
|
1400
|
-
// For mock mode only: generate deterministic address from agent name
|
|
1401
|
-
// This is safe because mock mode doesn't involve real funds
|
|
1402
1388
|
return `0x${Buffer.from(this.name).toString('hex').padEnd(40, '0').slice(0, 40)}`;
|
|
1403
1389
|
}
|
|
1404
1390
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1391
|
+
private async getPrivateKey(): Promise<string | undefined> {
|
|
1392
|
+
if (!this.config.wallet || this.config.wallet === 'auto') {
|
|
1393
|
+
if (this.network === 'testnet' || this.network === 'mainnet') {
|
|
1394
|
+
return resolvePrivateKey(this.config.stateDirectory);
|
|
1395
|
+
}
|
|
1396
|
+
return undefined;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
if (this.config.wallet === 'connect') {
|
|
1412
1400
|
return undefined;
|
|
1413
1401
|
}
|
|
1414
1402
|
|
|
1415
1403
|
if (typeof this.config.wallet === 'string') {
|
|
1416
|
-
// Check if it looks like a private key (0x + 64 hex chars)
|
|
1417
1404
|
if (/^0x[0-9a-fA-F]{64}$/.test(this.config.wallet)) {
|
|
1418
|
-
// Validate by trying to create a wallet
|
|
1419
1405
|
try {
|
|
1420
1406
|
new ethers.Wallet(this.config.wallet);
|
|
1421
1407
|
return this.config.wallet;
|
|
@@ -1423,11 +1409,9 @@ export class Agent extends EventEmitter {
|
|
|
1423
1409
|
throw new ValidationError('wallet', 'Invalid private key format');
|
|
1424
1410
|
}
|
|
1425
1411
|
}
|
|
1426
|
-
// It's an address, not a private key
|
|
1427
1412
|
return undefined;
|
|
1428
1413
|
}
|
|
1429
1414
|
|
|
1430
|
-
// Validate private key format
|
|
1431
1415
|
if (this.config.wallet.privateKey) {
|
|
1432
1416
|
try {
|
|
1433
1417
|
new ethers.Wallet(this.config.wallet.privateKey);
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentRegistryClient - Thin wrapper for AgentRegistry v2 config operations
|
|
3
|
+
*
|
|
4
|
+
* Provides methods for:
|
|
5
|
+
* - Publishing AGIRAILS.md config (CID + hash) on-chain
|
|
6
|
+
* - Managing launchpad listing visibility
|
|
7
|
+
* - Reading config state for any agent
|
|
8
|
+
*
|
|
9
|
+
* @module registry/AgentRegistryClient
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Contract, Signer, Provider } from 'ethers';
|
|
13
|
+
import AgentRegistryABI from '../abi/AgentRegistry.json';
|
|
14
|
+
import { ValidationError, TransactionRevertedError } from '../errors';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export interface AgentConfigState {
|
|
21
|
+
configHash: string;
|
|
22
|
+
configCID: string;
|
|
23
|
+
listed: boolean;
|
|
24
|
+
isActive: boolean;
|
|
25
|
+
updatedAt: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PublishConfigResult {
|
|
29
|
+
txHash: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface GasOptions {
|
|
33
|
+
maxFeePerGas?: bigint;
|
|
34
|
+
maxPriorityFeePerGas?: bigint;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Client
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
export class AgentRegistryClient {
|
|
42
|
+
private contract: Contract;
|
|
43
|
+
private readonlyContract: Contract;
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
private readonly registryAddress: string,
|
|
47
|
+
private readonly signer: Signer,
|
|
48
|
+
private readonly gasSettings?: GasOptions
|
|
49
|
+
) {
|
|
50
|
+
this.contract = new Contract(registryAddress, AgentRegistryABI, signer);
|
|
51
|
+
this.readonlyContract = this.contract;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a read-only client (no signer required)
|
|
56
|
+
*/
|
|
57
|
+
static readOnly(registryAddress: string, provider: Provider): AgentRegistryClient {
|
|
58
|
+
// Create a minimal signer-like object for the contract
|
|
59
|
+
// but only the readonly contract will be used
|
|
60
|
+
const contract = new Contract(registryAddress, AgentRegistryABI, provider);
|
|
61
|
+
const client = Object.create(AgentRegistryClient.prototype);
|
|
62
|
+
client.registryAddress = registryAddress;
|
|
63
|
+
client.readonlyContract = contract;
|
|
64
|
+
client.contract = null; // Write operations will throw
|
|
65
|
+
return client;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ========== WRITE OPERATIONS ==========
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Publish config (AGIRAILS.md) on-chain
|
|
72
|
+
*
|
|
73
|
+
* @param cid - IPFS CID pointing to the AGIRAILS.md file
|
|
74
|
+
* @param hash - keccak256 of canonical AGIRAILS.md content (bytes32)
|
|
75
|
+
* @returns Transaction hash
|
|
76
|
+
*/
|
|
77
|
+
async publishConfig(cid: string, hash: string): Promise<PublishConfigResult> {
|
|
78
|
+
if (!this.contract) {
|
|
79
|
+
throw new Error('Write operations require a signer. Use AgentRegistryClient constructor, not readOnly().');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!cid || cid.length === 0) {
|
|
83
|
+
throw new ValidationError('cid', 'IPFS CID is required');
|
|
84
|
+
}
|
|
85
|
+
if (cid.length > 128) {
|
|
86
|
+
throw new ValidationError('cid', 'CID too long (max 128 characters)');
|
|
87
|
+
}
|
|
88
|
+
if (!hash || hash === '0x' + '0'.repeat(64)) {
|
|
89
|
+
throw new ValidationError('hash', 'Config hash is required (cannot be zero)');
|
|
90
|
+
}
|
|
91
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(hash)) {
|
|
92
|
+
throw new ValidationError('hash', 'Config hash must be a valid bytes32 hex string');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const estimatedGas = await this.contract.publishConfig.estimateGas(cid, hash);
|
|
97
|
+
const gasLimit = (estimatedGas * 120n) / 100n; // 20% buffer
|
|
98
|
+
|
|
99
|
+
const txOptions: Record<string, unknown> = { gasLimit };
|
|
100
|
+
if (this.gasSettings?.maxFeePerGas) {
|
|
101
|
+
txOptions.maxFeePerGas = this.gasSettings.maxFeePerGas;
|
|
102
|
+
}
|
|
103
|
+
if (this.gasSettings?.maxPriorityFeePerGas) {
|
|
104
|
+
txOptions.maxPriorityFeePerGas = this.gasSettings.maxPriorityFeePerGas;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const tx = await this.contract.publishConfig(cid, hash, txOptions);
|
|
108
|
+
const receipt = await tx.wait();
|
|
109
|
+
|
|
110
|
+
return { txHash: receipt.hash };
|
|
111
|
+
} catch (error: unknown) {
|
|
112
|
+
if (error instanceof Error && error.message.includes('Not registered')) {
|
|
113
|
+
throw new TransactionRevertedError(
|
|
114
|
+
'Agent not registered. Register first using the AgentRegistry before publishing config.',
|
|
115
|
+
'publishConfig'
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Set launchpad listing visibility
|
|
124
|
+
*
|
|
125
|
+
* @param listed - Whether agent should be visible on launchpad
|
|
126
|
+
* @returns Transaction hash
|
|
127
|
+
*/
|
|
128
|
+
async setListed(listed: boolean): Promise<string> {
|
|
129
|
+
if (!this.contract) {
|
|
130
|
+
throw new Error('Write operations require a signer. Use AgentRegistryClient constructor, not readOnly().');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const estimatedGas = await this.contract.setListed.estimateGas(listed);
|
|
135
|
+
const gasLimit = (estimatedGas * 115n) / 100n; // 15% buffer
|
|
136
|
+
|
|
137
|
+
const txOptions: Record<string, unknown> = { gasLimit };
|
|
138
|
+
if (this.gasSettings?.maxFeePerGas) {
|
|
139
|
+
txOptions.maxFeePerGas = this.gasSettings.maxFeePerGas;
|
|
140
|
+
}
|
|
141
|
+
if (this.gasSettings?.maxPriorityFeePerGas) {
|
|
142
|
+
txOptions.maxPriorityFeePerGas = this.gasSettings.maxPriorityFeePerGas;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const tx = await this.contract.setListed(listed, txOptions);
|
|
146
|
+
const receipt = await tx.wait();
|
|
147
|
+
|
|
148
|
+
return receipt.hash;
|
|
149
|
+
} catch (error: unknown) {
|
|
150
|
+
if (error instanceof Error && error.message.includes('Not registered')) {
|
|
151
|
+
throw new TransactionRevertedError(
|
|
152
|
+
'Agent not registered. Register first before setting listing status.',
|
|
153
|
+
'setListed'
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ========== READ OPERATIONS ==========
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get config state for an agent
|
|
164
|
+
*
|
|
165
|
+
* @param agentAddress - Agent's Ethereum address
|
|
166
|
+
* @returns Config state (hash, CID, listed, isActive, updatedAt)
|
|
167
|
+
*/
|
|
168
|
+
async getConfig(agentAddress: string): Promise<AgentConfigState> {
|
|
169
|
+
const contract = this.readonlyContract || this.contract;
|
|
170
|
+
const profile = await contract.getAgent(agentAddress);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
configHash: profile.configHash,
|
|
174
|
+
configCID: profile.configCID,
|
|
175
|
+
listed: profile.listed,
|
|
176
|
+
isActive: profile.isActive,
|
|
177
|
+
updatedAt: Number(profile.updatedAt),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Check if an agent is listed on the launchpad
|
|
183
|
+
*
|
|
184
|
+
* @param agentAddress - Agent's Ethereum address
|
|
185
|
+
* @returns true if agent is listed
|
|
186
|
+
*/
|
|
187
|
+
async isListed(agentAddress: string): Promise<boolean> {
|
|
188
|
+
const config = await this.getConfig(agentAddress);
|
|
189
|
+
return config.listed;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get the on-chain config hash for an agent
|
|
194
|
+
*
|
|
195
|
+
* @param agentAddress - Agent's Ethereum address
|
|
196
|
+
* @returns Config hash (bytes32) or zero hash if not published
|
|
197
|
+
*/
|
|
198
|
+
async getConfigHash(agentAddress: string): Promise<string> {
|
|
199
|
+
const config = await this.getConfig(agentAddress);
|
|
200
|
+
return config.configHash;
|
|
201
|
+
}
|
|
202
|
+
}
|
package/src/types/adapter.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { z } from 'zod';
|
|
14
|
+
import type { X402FeeBreakdown } from './x402';
|
|
14
15
|
|
|
15
16
|
// ============================================================================
|
|
16
17
|
// AdapterMetadata - Describes adapter capabilities
|
|
@@ -249,6 +250,12 @@ export interface UnifiedPayResult {
|
|
|
249
250
|
* Use with ReputationReporter.reportSettlement() after release.
|
|
250
251
|
*/
|
|
251
252
|
erc8004AgentId?: string;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Fee breakdown for x402 payments routed through X402Relay.
|
|
256
|
+
* Present only when relay is configured and payment used the relay path.
|
|
257
|
+
*/
|
|
258
|
+
feeBreakdown?: X402FeeBreakdown;
|
|
252
259
|
}
|
|
253
260
|
|
|
254
261
|
/**
|
|
@@ -268,6 +275,13 @@ export const UnifiedPayResultSchema = z.object({
|
|
|
268
275
|
requester: z.string().min(1),
|
|
269
276
|
deadline: z.string().min(1),
|
|
270
277
|
erc8004AgentId: z.string().optional(),
|
|
278
|
+
feeBreakdown: z.object({
|
|
279
|
+
grossAmount: z.string(),
|
|
280
|
+
providerNet: z.string(),
|
|
281
|
+
platformFee: z.string(),
|
|
282
|
+
feeBps: z.number(),
|
|
283
|
+
estimated: z.literal(true),
|
|
284
|
+
}).optional(),
|
|
271
285
|
});
|
|
272
286
|
|
|
273
287
|
// ============================================================================
|
package/src/types/x402.ts
CHANGED
|
@@ -194,6 +194,38 @@ export class X402Error extends Error {
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// Fee Types
|
|
199
|
+
// ============================================================================
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Fee breakdown for x402 payments routed through X402Relay.
|
|
203
|
+
*
|
|
204
|
+
* Shows how the gross amount was split between provider and platform.
|
|
205
|
+
* Fee = max(grossAmount * feeBps / 10000, MIN_FEE).
|
|
206
|
+
*
|
|
207
|
+
* NOTE: This is a client-side **estimate** computed from the configured
|
|
208
|
+
* platformFeeBps. The on-chain X402Relay contract is the source of truth.
|
|
209
|
+
* If an admin updates the relay's fee rate, this estimate may diverge
|
|
210
|
+
* from the actual on-chain split until the SDK config is refreshed.
|
|
211
|
+
*/
|
|
212
|
+
export interface X402FeeBreakdown {
|
|
213
|
+
/** Total amount from the 402 header (USDC wei, 6 decimals) */
|
|
214
|
+
grossAmount: string;
|
|
215
|
+
|
|
216
|
+
/** Estimated amount provider received: grossAmount - platformFee */
|
|
217
|
+
providerNet: string;
|
|
218
|
+
|
|
219
|
+
/** Estimated amount treasury received: max(feeBps%, $0.05) */
|
|
220
|
+
platformFee: string;
|
|
221
|
+
|
|
222
|
+
/** Fee rate used for estimate (basis points, e.g. 100 = 1%) */
|
|
223
|
+
feeBps: number;
|
|
224
|
+
|
|
225
|
+
/** True — this is a client-side estimate, not read from chain */
|
|
226
|
+
estimated: true;
|
|
227
|
+
}
|
|
228
|
+
|
|
197
229
|
// ============================================================================
|
|
198
230
|
// Type Guards
|
|
199
231
|
// ============================================================================
|