@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
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Operations - Pull + Diff for AGIRAILS.md
|
|
3
|
+
*
|
|
4
|
+
* Terraform-style sync: compare local AGIRAILS.md with on-chain state.
|
|
5
|
+
* Never auto-overwrites — shows diff and requires explicit confirmation.
|
|
6
|
+
*
|
|
7
|
+
* @module config/syncOperations
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
11
|
+
import { Provider } from 'ethers';
|
|
12
|
+
import { computeConfigHash, parseAgirailsMd, serializeAgirailsMd } from './agirailsmd';
|
|
13
|
+
import { validateCID } from '../utils/validation';
|
|
14
|
+
import { AgentRegistryClient } from '../registry/AgentRegistryClient';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Constants
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/** Public IPFS gateways for read-only access (no credentials needed) */
|
|
21
|
+
const IPFS_GATEWAYS = [
|
|
22
|
+
'https://ipfs.io/ipfs/',
|
|
23
|
+
'https://dweb.link/ipfs/',
|
|
24
|
+
'https://cloudflare-ipfs.com/ipfs/',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Types
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
export interface DiffResult {
|
|
32
|
+
/** Whether local and on-chain are in sync */
|
|
33
|
+
inSync: boolean;
|
|
34
|
+
/** Local config hash (or null if no local file) */
|
|
35
|
+
localHash: string | null;
|
|
36
|
+
/** On-chain config hash (or zero hash if not published) */
|
|
37
|
+
onChainHash: string;
|
|
38
|
+
/** On-chain IPFS CID (or empty if not published) */
|
|
39
|
+
onChainCID: string;
|
|
40
|
+
/** Whether on-chain has a published config */
|
|
41
|
+
hasOnChainConfig: boolean;
|
|
42
|
+
/** Whether local file exists */
|
|
43
|
+
hasLocalFile: boolean;
|
|
44
|
+
/** Human-readable status message */
|
|
45
|
+
status: 'in-sync' | 'local-ahead' | 'remote-ahead' | 'diverged' | 'no-local' | 'no-remote';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PullResult {
|
|
49
|
+
/** Whether a file was written */
|
|
50
|
+
written: boolean;
|
|
51
|
+
/** The pulled content (if any) */
|
|
52
|
+
content?: string;
|
|
53
|
+
/** IPFS CID that was fetched */
|
|
54
|
+
cid?: string;
|
|
55
|
+
/** Status message */
|
|
56
|
+
status: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DiffOptions {
|
|
60
|
+
/** Path to local AGIRAILS.md */
|
|
61
|
+
path: string;
|
|
62
|
+
/** Agent address to check on-chain */
|
|
63
|
+
agentAddress: string;
|
|
64
|
+
/** AgentRegistry contract address */
|
|
65
|
+
registryAddress: string;
|
|
66
|
+
/** Provider for reading on-chain state */
|
|
67
|
+
provider: Provider;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface PullOptions extends DiffOptions {
|
|
71
|
+
/** Force overwrite without confirmation */
|
|
72
|
+
force?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Diff
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
const ZERO_HASH = '0x' + '0'.repeat(64);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Compare local AGIRAILS.md with on-chain config state.
|
|
83
|
+
*
|
|
84
|
+
* @param options - Diff configuration
|
|
85
|
+
* @returns Diff result showing sync status
|
|
86
|
+
*/
|
|
87
|
+
export async function diff(options: DiffOptions): Promise<DiffResult> {
|
|
88
|
+
const { path, agentAddress, registryAddress, provider } = options;
|
|
89
|
+
|
|
90
|
+
// Read on-chain state
|
|
91
|
+
const registryClient = AgentRegistryClient.readOnly(registryAddress, provider);
|
|
92
|
+
const onChainState = await registryClient.getConfig(agentAddress);
|
|
93
|
+
|
|
94
|
+
const hasOnChainConfig = onChainState.configHash !== ZERO_HASH && onChainState.configCID !== '';
|
|
95
|
+
const onChainHash = onChainState.configHash;
|
|
96
|
+
const onChainCID = onChainState.configCID;
|
|
97
|
+
|
|
98
|
+
// Read local state
|
|
99
|
+
const hasLocalFile = existsSync(path);
|
|
100
|
+
let localHash: string | null = null;
|
|
101
|
+
|
|
102
|
+
if (hasLocalFile) {
|
|
103
|
+
const content = readFileSync(path, 'utf-8');
|
|
104
|
+
const { configHash } = computeConfigHash(content);
|
|
105
|
+
localHash = configHash;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Determine status
|
|
109
|
+
let status: DiffResult['status'];
|
|
110
|
+
let inSync: boolean;
|
|
111
|
+
|
|
112
|
+
if (!hasLocalFile && !hasOnChainConfig) {
|
|
113
|
+
status = 'no-local';
|
|
114
|
+
inSync = true; // both empty = in sync
|
|
115
|
+
} else if (!hasLocalFile && hasOnChainConfig) {
|
|
116
|
+
status = 'remote-ahead';
|
|
117
|
+
inSync = false;
|
|
118
|
+
} else if (hasLocalFile && !hasOnChainConfig) {
|
|
119
|
+
status = 'no-remote';
|
|
120
|
+
inSync = false;
|
|
121
|
+
} else if (localHash === onChainHash) {
|
|
122
|
+
status = 'in-sync';
|
|
123
|
+
inSync = true;
|
|
124
|
+
} else {
|
|
125
|
+
// Both exist but hashes differ. Use the stored config_hash in frontmatter
|
|
126
|
+
// to determine directionality:
|
|
127
|
+
// - config_hash matches on-chain → user edited locally after last publish → local-ahead
|
|
128
|
+
// - config_hash doesn't match on-chain → remote was updated too → diverged
|
|
129
|
+
// - no config_hash → never published from this file → local-ahead
|
|
130
|
+
status = 'diverged';
|
|
131
|
+
inSync = false;
|
|
132
|
+
|
|
133
|
+
if (hasLocalFile) {
|
|
134
|
+
try {
|
|
135
|
+
const content = readFileSync(path, 'utf-8');
|
|
136
|
+
const { frontmatter } = parseAgirailsMd(content);
|
|
137
|
+
if (!frontmatter.config_hash) {
|
|
138
|
+
// Never published — local is the only source
|
|
139
|
+
status = 'local-ahead';
|
|
140
|
+
} else if (frontmatter.config_hash === onChainHash) {
|
|
141
|
+
// Last publish matches on-chain, so local edits are newer
|
|
142
|
+
status = 'local-ahead';
|
|
143
|
+
}
|
|
144
|
+
// else: frontmatter.config_hash !== onChainHash → remote updated → diverged
|
|
145
|
+
} catch {
|
|
146
|
+
// Parse error — keep as diverged
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
inSync,
|
|
153
|
+
localHash,
|
|
154
|
+
onChainHash,
|
|
155
|
+
onChainCID,
|
|
156
|
+
hasOnChainConfig,
|
|
157
|
+
hasLocalFile,
|
|
158
|
+
status,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// IPFS Fetch (public gateway, no credentials needed)
|
|
164
|
+
// ============================================================================
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Fetch content from IPFS using public gateways (no Filebase credentials needed).
|
|
168
|
+
* Tries multiple gateways with fallback.
|
|
169
|
+
*
|
|
170
|
+
* @param cid - IPFS CID to fetch (validated before use)
|
|
171
|
+
* @returns Raw content as string
|
|
172
|
+
* @throws InvalidCIDError if CID format is invalid
|
|
173
|
+
* @throws Error if all gateways fail
|
|
174
|
+
*/
|
|
175
|
+
async function fetchFromIPFS(cid: string): Promise<string> {
|
|
176
|
+
// Validate CID format before hitting any gateway
|
|
177
|
+
validateCID(cid, 'onChainCID');
|
|
178
|
+
|
|
179
|
+
const errors: string[] = [];
|
|
180
|
+
|
|
181
|
+
for (const gateway of IPFS_GATEWAYS) {
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetch(`${gateway}${cid}`, {
|
|
184
|
+
signal: AbortSignal.timeout(15_000),
|
|
185
|
+
});
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
errors.push(`${gateway}: HTTP ${response.status}`);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
return await response.text();
|
|
191
|
+
} catch (err) {
|
|
192
|
+
errors.push(`${gateway}: ${err instanceof Error ? err.message : String(err)}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Failed to fetch CID ${cid} from all IPFS gateways:\n${errors.map(e => ` - ${e}`).join('\n')}`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ============================================================================
|
|
202
|
+
// Pull
|
|
203
|
+
// ============================================================================
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Pull on-chain config to local AGIRAILS.md.
|
|
207
|
+
*
|
|
208
|
+
* Downloads from IPFS via public gateways (no Filebase credentials needed),
|
|
209
|
+
* verifies integrity against on-chain configHash, then writes locally.
|
|
210
|
+
*
|
|
211
|
+
* @param options - Pull configuration
|
|
212
|
+
* @returns Pull result
|
|
213
|
+
*/
|
|
214
|
+
export async function pull(options: PullOptions): Promise<PullResult> {
|
|
215
|
+
const { path, agentAddress, registryAddress, provider, force = false } = options;
|
|
216
|
+
|
|
217
|
+
// First, run diff
|
|
218
|
+
const diffResult = await diff({ path, agentAddress, registryAddress, provider });
|
|
219
|
+
|
|
220
|
+
if (!diffResult.hasOnChainConfig) {
|
|
221
|
+
return {
|
|
222
|
+
written: false,
|
|
223
|
+
status: 'No config published on-chain for this agent.',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (diffResult.inSync) {
|
|
228
|
+
return {
|
|
229
|
+
written: false,
|
|
230
|
+
status: 'Already in sync. No changes needed.',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Fetch raw AGIRAILS.md from IPFS (public gateway, no credentials)
|
|
235
|
+
const content = await fetchFromIPFS(diffResult.onChainCID);
|
|
236
|
+
|
|
237
|
+
// Integrity verification: hash downloaded content and compare with on-chain hash
|
|
238
|
+
const { configHash: downloadedHash } = computeConfigHash(content);
|
|
239
|
+
if (downloadedHash !== diffResult.onChainHash) {
|
|
240
|
+
return {
|
|
241
|
+
written: false,
|
|
242
|
+
cid: diffResult.onChainCID,
|
|
243
|
+
status: `Integrity check failed! Downloaded content hash (${downloadedHash}) does not match on-chain hash (${diffResult.onChainHash}). The IPFS content may have been tampered with.`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check if local file exists and we're not forcing
|
|
248
|
+
if (diffResult.hasLocalFile && !force) {
|
|
249
|
+
return {
|
|
250
|
+
written: false,
|
|
251
|
+
content,
|
|
252
|
+
cid: diffResult.onChainCID,
|
|
253
|
+
status: `Remote config differs from local. Use --force to overwrite. CID: ${diffResult.onChainCID}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Stamp on-chain metadata into frontmatter so diff heuristic can detect
|
|
258
|
+
// future remote changes (without this, pulled files have no config_hash
|
|
259
|
+
// and diff would always report "local-ahead" instead of "diverged")
|
|
260
|
+
const { frontmatter, body } = parseAgirailsMd(content);
|
|
261
|
+
const stamped = serializeAgirailsMd(
|
|
262
|
+
{
|
|
263
|
+
...frontmatter,
|
|
264
|
+
config_hash: diffResult.onChainHash,
|
|
265
|
+
config_cid: diffResult.onChainCID,
|
|
266
|
+
},
|
|
267
|
+
body
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Write the stamped file
|
|
271
|
+
writeFileSync(path, stamped, 'utf-8');
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
written: true,
|
|
275
|
+
content: stamped,
|
|
276
|
+
cid: diffResult.onChainCID,
|
|
277
|
+
status: `Pulled and verified config from IPFS (${diffResult.onChainCID}) → ${path}`,
|
|
278
|
+
};
|
|
279
|
+
}
|
package/src/index.ts
CHANGED
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);
|