@agirails/sdk 2.2.2 → 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/runtime/MockRuntime.d.ts.map +1 -1
- package/dist/runtime/MockRuntime.js +3 -1
- package/dist/runtime/MockRuntime.js.map +1 -1
- 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/runtime/MockRuntime.ts +3 -1
- package/src/types/adapter.ts +14 -0
- package/src/types/x402.ts +32 -0
- package/src/wallet/keystore.ts +119 -0
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
|
+
}
|
|
@@ -646,7 +646,9 @@ export class MockRuntime implements IACTPRuntime {
|
|
|
646
646
|
if (newState === 'DELIVERED') {
|
|
647
647
|
tx.completedAt = currentTime;
|
|
648
648
|
// SECURITY FIX (PROOF-PARAM): Store delivery proof if provided
|
|
649
|
-
if (
|
|
649
|
+
// Only set if not already populated (Agent sets deliveryProof before transitioning,
|
|
650
|
+
// and passes disputeWindowProof as the proof param — don't overwrite the real proof)
|
|
651
|
+
if (proof && !tx.deliveryProof) {
|
|
650
652
|
tx.deliveryProof = proof;
|
|
651
653
|
}
|
|
652
654
|
}
|
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
|
// ============================================================================
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keystore auto-resolution for ACTP wallets.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* 1. ACTP_PRIVATE_KEY env var (backward compat, highest priority)
|
|
6
|
+
* 2. .actp/keystore.json decrypted with ACTP_KEY_PASSWORD
|
|
7
|
+
* 3. undefined (caller decides what to do)
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { Wallet } from 'ethers';
|
|
12
|
+
|
|
13
|
+
// Cache keyed by resolved keystorePath to support multiple stateDirectories
|
|
14
|
+
const _cache = new Map<string, { key: string; address: string }>();
|
|
15
|
+
|
|
16
|
+
// Separate cache for env-var-resolved key (no path dependency)
|
|
17
|
+
let _envCache: { key: string; address: string } | null = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate that stateDirectory doesn't escape expected boundaries.
|
|
21
|
+
* Guards against path traversal when stateDirectory comes from user input.
|
|
22
|
+
*/
|
|
23
|
+
function validateStateDirectory(stateDirectory: string): void {
|
|
24
|
+
if (stateDirectory.includes('\0')) {
|
|
25
|
+
throw new Error('Invalid stateDirectory: null byte detected');
|
|
26
|
+
}
|
|
27
|
+
// Reject raw '..' in the input (before normalization resolves it)
|
|
28
|
+
// Catches both relative traversal (../../etc) and embedded traversal (/tmp/../../etc)
|
|
29
|
+
if (stateDirectory.includes('..')) {
|
|
30
|
+
throw new Error('Invalid stateDirectory: path traversal detected (..)');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate and normalize a raw private key string.
|
|
36
|
+
* Trims whitespace and verifies 0x-prefixed 64-char hex format.
|
|
37
|
+
*/
|
|
38
|
+
function validateRawKey(raw: string, source: string): string {
|
|
39
|
+
const trimmed = raw.trim();
|
|
40
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(trimmed)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Invalid private key from ${source}: expected 0x-prefixed 64-char hex string`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return trimmed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Auto-resolve private key: env var → keystore → undefined.
|
|
50
|
+
* Never logs or prints the key itself.
|
|
51
|
+
*/
|
|
52
|
+
export async function resolvePrivateKey(
|
|
53
|
+
stateDirectory?: string
|
|
54
|
+
): Promise<string | undefined> {
|
|
55
|
+
// 1. Env var (highest priority, backward compat)
|
|
56
|
+
if (process.env.ACTP_PRIVATE_KEY) {
|
|
57
|
+
if (_envCache) return _envCache.key;
|
|
58
|
+
|
|
59
|
+
const key = validateRawKey(process.env.ACTP_PRIVATE_KEY, 'ACTP_PRIVATE_KEY env var');
|
|
60
|
+
const address = new Wallet(key).address;
|
|
61
|
+
_envCache = { key, address };
|
|
62
|
+
return key;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. Resolve keystore path
|
|
66
|
+
if (stateDirectory) {
|
|
67
|
+
validateStateDirectory(stateDirectory);
|
|
68
|
+
}
|
|
69
|
+
const actpDir = stateDirectory
|
|
70
|
+
? path.join(stateDirectory, '.actp')
|
|
71
|
+
: path.join(process.cwd(), '.actp');
|
|
72
|
+
const keystorePath = path.resolve(actpDir, 'keystore.json');
|
|
73
|
+
|
|
74
|
+
// 3. Cache hit (keyed by resolved path)
|
|
75
|
+
const cached = _cache.get(keystorePath);
|
|
76
|
+
if (cached) return cached.key;
|
|
77
|
+
|
|
78
|
+
// 4. Keystore file
|
|
79
|
+
if (!fs.existsSync(keystorePath)) return undefined;
|
|
80
|
+
|
|
81
|
+
const password = process.env.ACTP_KEY_PASSWORD;
|
|
82
|
+
if (!password) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
'Keystore found at ' + keystorePath + ' but ACTP_KEY_PASSWORD is not set.\n' +
|
|
85
|
+
'Set it: export ACTP_KEY_PASSWORD="your-password"'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const keystore = fs.readFileSync(keystorePath, 'utf-8');
|
|
90
|
+
const wallet = await Wallet.fromEncryptedJson(keystore, password);
|
|
91
|
+
|
|
92
|
+
_cache.set(keystorePath, { key: wallet.privateKey, address: wallet.address });
|
|
93
|
+
return wallet.privateKey;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get cached address from last resolvePrivateKey() call.
|
|
98
|
+
* Works for both env-var and keystore resolution paths.
|
|
99
|
+
*/
|
|
100
|
+
export function getCachedAddress(stateDirectory?: string): string | undefined {
|
|
101
|
+
// Env var path
|
|
102
|
+
if (_envCache) return _envCache.address;
|
|
103
|
+
|
|
104
|
+
// Keystore path — look up by resolved path
|
|
105
|
+
const actpDir = stateDirectory
|
|
106
|
+
? path.join(stateDirectory, '.actp')
|
|
107
|
+
: path.join(process.cwd(), '.actp');
|
|
108
|
+
const keystorePath = path.resolve(actpDir, 'keystore.json');
|
|
109
|
+
return _cache.get(keystorePath)?.address;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Clear all cached keys and addresses (for testing).
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
export function _clearCache(): void {
|
|
117
|
+
_cache.clear();
|
|
118
|
+
_envCache = null;
|
|
119
|
+
}
|